diff --git a/src/typstsharp.tests/Tests.cs b/src/typstsharp.tests/Tests.cs
index 4a5b967..1bf8aa6 100644
--- a/src/typstsharp.tests/Tests.cs
+++ b/src/typstsharp.tests/Tests.cs
@@ -910,6 +910,26 @@ public async Task WarningsFromADocumentCannotBeMutatedByCallers()
await Assert.That(document.Warnings is string[]).IsFalse();
}
+ ///
+ /// The warning-free case had no coverage. This pins the public contract rather than the
+ /// allocation, so it holds whichever way the empty list is produced: empty, and not a mutable
+ /// array handed out to callers.
+ ///
+ [Test]
+ public async Task WarningsFromACleanCompileAreEmptyAndStillImmutable()
+ {
+ using var compiler = TypstCompiler.FromSource("= No warnings here");
+ var document = compiler.CompileToDocument();
+
+ await Assert.That(document.Warnings.Count).IsEqualTo(0);
+ await Assert.That(document.Warnings is string[]).IsFalse();
+
+ document.Dispose();
+
+ // Warnings are copied out of native memory eagerly, so they outlive the document.
+ await Assert.That(document.Warnings.Count).IsEqualTo(0);
+ }
+
private const string TwoPageSource = """
First page
#pagebreak()
diff --git a/src/typstsharp/TypstDocument.cs b/src/typstsharp/TypstDocument.cs
index 5813764..73326ce 100644
--- a/src/typstsharp/TypstDocument.cs
+++ b/src/typstsharp/TypstDocument.cs
@@ -53,7 +53,8 @@ internal unsafe TypstDocument(CsBindgen.CompileResult native)
}
// Warnings are small and are copied eagerly so that they stay usable after disposal.
- var warnings = new string[warningCount];
+ // A clean compile is the common case, and Array.Empty spares it the only allocation here.
+ var warnings = warningCount == 0 ? Array.Empty() : new string[warningCount];
for (int i = 0; i < warnings.Length; i++)
{
var warning = native.warnings[i];