Repository navigation
feat: leg 5 of the #890 canary — the pristine-COMPILER control (compiler=PRIVATE-COPY-EMITS / THREW) - #4165
Conversation
Legs 1–4 all execute the one shared Microsoft.CodeAnalysis*, so none can separate "the process cannot emit" from "the shared copy of Roslyn cannot emit"; BELOW-ROSLYN has been read on every occurrence from evidence that only ever showed "not the references" and "not the source". The 2026-09-12 check on #890 named this control as the decisive one still unbuilt. PrivateRoslynCopy.Emit loads Microsoft.CodeAnalysis and Microsoft.CodeAnalysis.CSharp a second time from fresh bytes into a collectible AssemblyLoadContext (the two serve each other; every other dependency falls through to the default context), drives the canary source through that private copy by reflection against an image-backed CoreLib the private copy builds itself, and unloads. compiler=PRIVATE-COPY-EMITS ⇒ the broken state lives IN the shared compiler, not below it; PRIVATE-COPY-THREW@frame ⇒ the process cannot emit at all and BELOW-ROSLYN is earned; UNAVAILABLE ⇒ the leg could not run, stated rather than folded. It rides the same two verdicts legs 3 and 4 ride (BELOW-ROSLYN, DIVERGENT), on the already-failing path only. EmitCanaryPrivateCompilerLegTest pins that a healthy process reads PRIVATE-COPY-EMITS twice in a row (~1 s incl. both loads) — a control that cannot run would retire the discriminator silently — and that both verdict shapes render the compiler= reading, name its absence, and report a faulting probe as UNAVAILABLE. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BRLdhAiX5DjdZC2WUfPg9E
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved moderate issues remain in integration, cancellation, probe classification, and test coverage.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds leg 5 of the emit canary: a collectible, private Roslyn compiler probe for diagnosing shared compiler failures.
Changes:
- Implements isolated Roslyn loading and reflective emission.
- Integrates
compiler=readings into emit-failure verdicts. - Adds tests, architecture documentation, and a What's New entry.
File summaries
| File | Summary |
|---|---|
test/MeshWeaver.Compiler.Pipeline.Test/EmitCanaryPrivateCompilerLegTest.cs |
Tests private emission and verdict rendering. |
src/MeshWeaver.Documentation/Data/WhatsNew/2026-09-13-a-failed-compile-now-checks-a-fresh-copy-of-the-compiler.md |
Documents the feature announcement. |
src/MeshWeaver.Documentation/Data/Architecture/NodeTypeCompilation.md |
Documents leg 5 behavior and diagnostic readings. |
src/MeshWeaver.Compiler/PrivateRoslynCopy.cs |
Implements private Roslyn loading and emit probing. |
src/MeshWeaver.Compiler/EmitPipeline.cs |
Integrates the new canary leg into the emit pipeline. |
Review details
Suppressed comments (10)
src/MeshWeaver.Compiler/EmitPipeline.cs:446
- This wires leg 5 only into
ProbeSharedEmitState, which is reached from the disk-emission catch. The in-memory path (EmitToMemory) still callscompilation.Emitdirectly without invoking the canary, soEnableDiskCache=falsedeployments/tests can still lose a compiler throw without anycompiler=reading. Route the same probe through the memory emit path while preserving the existing failure funnel.
() => PrivateRoslynCopy.Emit(EmitCanarySource));
src/MeshWeaver.Compiler/EmitPipeline.cs:867
- This text repeats the same overstrong inference: a fresh copy emitting only shows that this loaded/compiler-code path differs from the shared one; it does not rule out a CLR/JIT failure because both copies execute on the same runtime. The existing Leg 5 residual discussion later in this document already acknowledges that distinction, so this wording would send triage in the wrong direction.
+ "🚨 THEN READ compiler=: it is the one leg that does not run the shared Roslyn — "
+ "PRIVATE-COPY-EMITS means a fresh copy of the compiler emits this very source in this "
+ "very process, so the fault is in the shared copy's state or code and NOT below "
+ "Roslyn after all; PRIVATE-COPY-THREW at the same frame is what earns this verdict. "
src/MeshWeaver.Compiler/EmitPipeline.cs:870
- The new leg is appended to a
canary=BELOW-ROSLYNresult, butIsProcessEmitFailureonly checks that prefix (EmitPipeline.cs:284-287). Thuscompiler=PRIVATE-COPY-EMITS,PRIVATE-COPY-DIAGNOSTICS, orPRIVATE-COPY-UNAVAILABLEstill causesRunCompileto logPROCESS CANNOT EMITand stamp the compile unavailable, contrary to the leg's stated semantics. The classifier and its tests must consume the compiler reading before treating this as a process-wide failure.
+ Dissection(dissect)
+ " " + Flatness(flat, sharedSite)
+ " " + PrivateRoslynCopy.Render(compiler);
src/MeshWeaver.Compiler/PrivateRoslynCopy.cs:62
PrivateRoslynCopy.Emithas no cancellation input, so this reflection path supplies defaults to the privateEmitcall (includingCancellationToken.Nonewhen that overload has a token) and cannot observe the active compile cancellation. Because it runs synchronously while handling the emit exception, a hung file read or private emit can outliveRoslynCompileTimeoutand leave a dedicated compile thread/ALC pinned per occurrence. Thread the active cancellation token through this diagnostic or otherwise put the probe behind a bounded owner.
internal static string Emit(string source)
src/MeshWeaver.Compiler/PrivateRoslynCopy.cs:99
- The empty catch hides a failed collectible-context unload and still returns the compiler result as if cleanup succeeded. This probe runs for every qualifying failure, so an unload failure can retain the private Roslyn context and its assembly graph across repeated occurrences with no diagnostic evidence. Surface the cleanup failure separately (or log it) instead of silently discarding it.
try { context.Unload(); }
catch { /* best-effort: a copy that will not unload costs memory, not correctness */ }
src/MeshWeaver.Compiler/PrivateRoslynCopy.cs:229
- The fallback for a parameter without a declared default manufactures
default(T)/nulleven when the parameter is required. If a Roslyn upgrade adds a required argument to one of these reflected APIs, the probe can invoke it with an invented value and report a misleading emit/diagnostics result instead of becoming unavailable; reject unsupported required parameters so the healthy-process guard cannot silently validate a changed probe.
: p.ParameterType.IsValueType
? Activator.CreateInstance(p.ParameterType)
: null;
src/MeshWeaver.Compiler/PrivateRoslynCopy.cs:173
- A successful emit from a fresh collectible ALC does not prove the fault is not below Roslyn: both copies still share the CLR/JIT, while the private copy simply gets a separately JIT-compiled method. A runtime/JIT defect affecting the already-JITed shared method can therefore produce this result, so this message must not direct triage away from
dotnet/runtime.
return $"{Prefix}PRIVATE-COPY-EMITS — a copy of Roslyn loaded from fresh bytes into its own "
+ "collectible context, never executed in this process before, emits the same source in "
+ "the same process on the same CLR ⇒ the broken state lives IN the shared compiler (a "
+ "static, a cache, a miscompiled method), not below it; scope the shared compiler out "
+ "per generation, and do not file this against the runtime";
src/MeshWeaver.Documentation/Data/Architecture/NodeTypeCompilation.md:1380
- The documented output shape does not match
PrivateRoslynCopy.Emit: the implementation emitscompiler=PRIVATE-COPY-THREW {Type} at {Frame}: {Message}, notcompiler=PRIVATE-COPY-THREW@<frame>. Since this page is the triage contract, document the actual token so readers and parsers do not search for a string that is never produced.
or `compiler=PRIVATE-COPY-THREW@<frame>` on the same line as `canary=`, `dissect=` and `flat=`.
src/MeshWeaver.Documentation/Data/Architecture/NodeTypeCompilation.md:1361
- This table says
PRIVATE-COPY-EMITSrules out the CLR/runtime, but the private and shared copies still share the CLR/JIT and the private copy starts with different JIT state. That contradicts the residual caveat in the existing Leg 5 section atNodeTypeCompilation.md:1621-1626and can misdirect the incident response; describe this as evidence about the shared Roslyn load/code path, not proof against a runtime/JIT cause.
| `compiler=PRIVATE-COPY-EMITS` | a never-before-executed Roslyn emits the source the shared one cannot, same process, same CLR | 🚨 **the broken state lives IN the shared copy** — a static, a cache, a miscompiled method — not below it. `BELOW-ROSLYN` was NOT earned. The remedy is to scope the shared compiler out (a per-generation compiler context); a `dotnet/runtime` report is not what the evidence supports |
test/MeshWeaver.Compiler.Pipeline.Test/EmitCanaryPrivateCompilerLegTest.cs:28
- This test only checks the returned token, so it would still pass if
Emitwere changed to use the default Roslyn assemblies and return the same success text; it never verifies that the emitted types belong to a non-default collectible load context. The test name promises that invariant, so add an observable identity/context assertion or an internal seam that makes the private assembly identity testable.
var verdict = PrivateRoslynCopy.Emit(
"public class MwEmitCanary<T> { public class Inner<U> { public class Leaf<V> "
+ "{ public T A; public U B; public V C; } } }");
output.WriteLine(verdict);
verdict.Should().StartWith(PrivateRoslynCopy.Prefix + "PRIVATE-COPY-EMITS",
- Files reviewed: 5/5 changed files
- Comments generated: 4
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| catch (TargetInvocationException reflective) when (reflective.InnerException is { } inner) | ||
| { | ||
| return $"{Prefix}PRIVATE-COPY-THREW {inner.GetType().Name} at {EmitPipeline.ThrowSite(inner)}: {inner.Message}"; | ||
| } |
| var ids = new List<string>(); | ||
| if (result.GetType().GetProperty("Diagnostics")?.GetValue(result) is IEnumerable diagnostics) | ||
| { | ||
| foreach (var diagnostic in diagnostics) | ||
| { | ||
| var severity = diagnostic.GetType().GetProperty("Severity")?.GetValue(diagnostic)?.ToString(); | ||
| if (severity != nameof(DiagnosticSeverity.Error)) | ||
| continue; | ||
| var id = diagnostic.GetType().GetProperty("Id")?.GetValue(diagnostic)?.ToString(); | ||
| if (id is not null && !ids.Contains(id)) | ||
| ids.Add(id); | ||
| if (ids.Count == 5) | ||
| break; | ||
| } | ||
| } |
| private readonly Dictionary<string, Assembly> own = new(StringComparer.OrdinalIgnoreCase); | ||
|
|
||
| public Assembly LoadPrivate(string simpleName, byte[] image) | ||
| { | ||
| using var stream = new MemoryStream(image, writable: false); | ||
| var assembly = LoadFromStream(stream); | ||
| own[simpleName] = assembly; |
| #### Leg 5 — the pristine-COMPILER control (`compiler=`, 2026-09-13) | ||
|
|
||
| Legs 1–4 have one ceiling in common that none of them can see over: **all four execute the same | ||
| `Microsoft.CodeAnalysis*.dll`** — the same mapped image, the same native code, the same statics. | ||
| Leg 2 varies the references, leg 4 the source, leg 3 the call site; nothing varies the compiler. |
Test Results (shard 1)508 tests 508 ✅ 38s ⏱️ Results for commit 2e6e661. ♻️ This comment has been updated with latest results. |
Test Results (shard 0)261 tests 261 ✅ 2m 18s ⏱️ Results for commit 2e6e661. ♻️ This comment has been updated with latest results. |
Test Results (shard 3)390 tests 390 ✅ 42s ⏱️ Results for commit 2e6e661. ♻️ This comment has been updated with latest results. |
Test Results (shard 2)1 333 tests 1 331 ✅ 4m 3s ⏱️ Results for commit 2e6e661. ♻️ This comment has been updated with latest results. |
Test Results (shard 4)2 036 tests 2 036 ✅ 5m 10s ⏱️ Results for commit 2e6e661. ♻️ This comment has been updated with latest results. |
…ompiler= format; memory emit stamps the canary Copilot review on #4165: - A throw from parse/reference/options/create is compiler=UNAVAILABLE(stage=… threw …) — the control was never asked to emit; PRIVATE-COPY-THREW is reserved for the emit stage. - ImmutableList / ImmutableDictionary in place of the mutable accumulators. - The doc's 2026-09-10 specification section is the canonical leg-5 contract; the implementation section now points at its table, tokens aligned (UNAVAILABLE(…) / NOT-RUN), DIAGNOSTICS added. - The EMITS wording follows the spec: the fault travels with this process's copy of the compiler (image, mapping, native code), not with the CLR heap — the tiering residual is restated. - EmitToMemory stamps the canary verdict on an emit-phase throw like the disk path does; a deployment emitting to memory used to lose every #890 reading. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BRLdhAiX5DjdZC2WUfPg9E
|
Addressed in the follow-up commit, all four inline and the two suppressed:
|
Test Results (shard 5) 6 files 6 suites 9m 2s ⏱️ Results for commit 2e6e661. |
Test Results 17 files 17 suites 21m 56s ⏱️ Results for commit 2e6e661. |
…ide of a coin toss CI (queue build 34750238233, shard 1) failed the BuildupAction row with "No exception was thrown". Its own output says why: TEST START …065 → "retiring this activation" …067 (+2 ms) → FAILED …078. The activation faulted and retired BEFORE the request was posted, so the request activated a fresh hub and was SERVED — which is exactly what step 3 of the same test asserts as the contract. Both orderings are legal; the assertion was written for one of them, and which one occurs was decided by how long GetClient()'s own hub construction took against an immediate fault. Only BuildupAction faults immediately (a DataContext's init is slower), which is why only that row failed. NOT a #4165/#4151 interaction: the BuildupAction host has no AddData and therefore no SynchronizationStream — its output block carries ZERO SYNC_STREAM lines where the passing DataContext row carries five — so #4151's FaultStore/GetStream change cannot reach it. The fence: the first init attempt parks until the test releases it, and the test releases only once the request is provably in flight AT that activation. Step 3 already pins the other ordering, so the test now states the whole contract instead of one side of it. 🚨 The first fence was wrong, and #4154's new report is what showed it: target pump now: host/1 RunLevel=Starting turn=InitializeHubRequest running 35004ms buffer=1 deferred=0 … draining=True openGates=1 A BuildupAction runs ON the init turn, so the pump is busy and the request sits in the MAIN queue, never reaching the deferral decision; a DataContext initialises OFF the turn, so the request parks in the DEFERRED queue. InFlightAt accepts either queue, at that hub. Verified on origin/main merged in (bd4314b, with #4151 and #4165): 4/4 green, MeshWeaver.Data.Test 512/512, MeshWeaver.Messaging.Hub.Test 432/432. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Builds the control #890 has been waiting for since its 2026-09-10 specification and its 2026-09-12 supersession check ("the decisive control is still not implemented"). Does not close #890 — the closing condition is the READING of this leg on an occurrence.
What it is
PrivateRoslynCopy.Emit— leg 5 of the emit canary, the pristine-COMPILER control. Legs 1–4 all execute the one sharedMicrosoft.CodeAnalysis*.dll(same mapped image, same native code, same statics), so none of them can separate "the process cannot emit" from "the shared copy of Roslyn cannot emit" — which is whyBELOW-ROSLYNhas been reading "not the references and not the source" as "not Roslyn" on every occurrence.On the same two verdicts legs 3 and 4 ride (
BELOW-ROSLYN,DIVERGENT), it loadsMicrosoft.CodeAnalysis+Microsoft.CodeAnalysis.CSharpa second time — from freshly read bytes into a collectibleAssemblyLoadContextthat serves the two to each other and lets every other dependency fall through to the default context — drivesEmitCanarySourcethrough that private copy by reflection against an image-backed CoreLib the private copy builds itself, and unloads.compiler=PRIVATE-COPY-EMITSPRIVATE-COPY-THREW … at …BELOW-ROSLYNearned; the private copy's frame is the repro to filePRIVATE-COPY-DIAGNOSTICS(…)PRIVATE-COPY-UNAVAILABLE(…)/NOT-RUNCost: two loads (~15 MB read), one tiny emit, one
Unload(), on the already-failing terminal path only. Measured ~1 s on a healthy process including both loads.Test
EmitCanaryPrivateCompilerLegTest: on a healthy process the reading isPRIVATE-COPY-EMITS, twice in a row (the context is re-created per run) — a control that cannot run would retire the discriminator silently, and a Roslyn upgrade that reshapesCreate/Emitnow reds this test instead of turning the next occurrence intoUNAVAILABLE. Both verdict shapes render thecompiler=reading; its absence is named; a faulting probe reportsUNAVAILABLE. The existing canary suites (EmitCanaryControlTest,EmitCanaryFlatLegTest,EmitCanaryDissectionTest) stay green.The reflection binds to the private copy's own types by full name and fills every optional parameter with its declared default, so it names nothing of Roslyn's optional-parameter surface.
Doc:
Doc/Architecture/NodeTypeCompilation→ "Leg 5 — the pristine-COMPILER control". What's New: Feature.Pairs-with: none — a new
internalclass and an optional parameter on aninternalmethod.🤖 Generated with Claude Code
https://claude.ai/code/session_01BRLdhAiX5DjdZC2WUfPg9E