fix(scriptanalysis): compile gate reads diagnostics only and never caches a faulted verdict (review 2026-10-21 04/M1, 04/C1)

RoslynScriptCompiler.Compile now uses Script.GetCompilation().GetDiagnostics()
with one shared, never-loading InteractiveAssemblyLoader instead of
Script.Compile(), which emitted and loaded a non-collectible assembly (and a
fresh AssemblyLoadContext) per distinct script. The catch-all that turned any
exception into a 'Compilation exception' diagnostic is gone; TryCompile and
CheckExpressionSyntax report a fault for that call only, uncached.
This commit is contained in:
Joseph Doherty
2026-10-22 06:15:35 -04:00
parent 75149e83b0
commit ae1282ed9d
8 changed files with 274 additions and 68 deletions
+25 -7
View File
@@ -167,13 +167,31 @@ generations of 2048 entries. New verdicts land in the *hot* generation; a hit in
*cold* is promoted back into hot; on overflow hot becomes the new cold and only the
old cold — the half nothing has touched for a full generation — is dropped.
The bound must never be enforced by clearing the cache outright. That reads as
harmless ("a verdict is cheap to recompute") but is not: a recompute is a fresh
Roslyn compile, and every script compile loads an assembly through a
**non-collectible `InteractiveAssemblyLoader`**. Bounding that leak is the entire
reason this cache exists, so dropping every hot entry at the 4096th distinct script
would re-open it for the whole working set at once. Segmenting retains anything in
active use across an eviction while keeping the same ceiling.
The bound must never be enforced by clearing the cache outright: a recompute is a
fresh trust analysis plus Roslyn compile against the full reference set (tens of ms
each), so dropping every hot entry at the 4096th distinct script would re-pay that
for the whole working set at once. Segmenting retains anything in active use across
an eviction while keeping the same ceiling.
**The gate never emits or loads an assembly (review 2026-10-21 04/M1).**
`RoslynScriptCompiler.Compile` reads `Script.GetCompilation().GetDiagnostics()` —
declaration and method-body errors, no PE emit — and passes every
`CSharpScript.Create` one shared `InteractiveAssemblyLoader` that never loads
anything. (It used to call `Script.Compile()`, which builds the executor: emit, then a
load through a fresh **non-collectible** `AssemblyLoadContext` per *distinct* script —
and `CSharpScript.Create` with no loader argument registers a new ALC even without the
load.) Pinned by a test that N distinct compiles leave `AssemblyLoadContext.All` and
the loaded-assembly count flat. The verdict cache therefore bounds CPU, not memory.
The site runtime's `ScriptCompilationService` is unaffected — it needs an executable
`Script` and keeps its own compile.
**Only diagnostic verdicts are cached (review 2026-10-21 04/C1).** `Compile` no longer
catches exceptions and returns them as a `"Compilation exception: …"` message; an
unexpected fault (metadata I/O, out-of-memory, …) propagates. The cache never stores a
verdict whose factory threw, and `ScriptCompiler.TryCompile` / `CheckExpressionSyntax`
turn the fault into an uncached failure for that call alone ("could not be checked —
the compile gate faulted … retry"), so a one-off fault can no longer become a sticky,
process-wide "failed to compile" on one central node until restart.
#### Parity guard