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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user