Skip to content

fix(824): publish ILGlobals opcode tables from an explicit static constructor - #833

Merged
drmoisan merged 4 commits into
epic/review-residuals-2026-09-08-integrationfrom
bug/ilglobals-loadopcodes-unsynchronised-static-race-824-exec
Sep 9, 2026
Merged

drmoisan merged 4 commits into
epic/review-residuals-2026-09-08-integrationfrom
bug/ilglobals-loadopcodes-unsynchronised-static-race-824-exec

Conversation

@drmoisan

@drmoisan drmoisan commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Summary

Removes the unsynchronised static-initialisation race in ILGlobals.LoadOpCodes().

Both opcode tables become public static readonly with no initializer. An explicit private static constructor builds each OpCode[0x100] in a local, runs the reflection loop to completion against those locals, and assigns each field exactly once after the loop. LoadOpCodes() is retained, with the single statement RuntimeHelpers.RunClassConstructor(typeof(ILGlobals).TypeHandle);.

The previous implementation reassigned a shared static table to a freshly allocated all-default array and then filled it element by element, with no lock, no Lazy<T>, no volatile and no static constructor. Under [assembly: Parallelize(Workers = 0, Scope = ExecutionScope.ClassLevel)], a reader on one worker could observe a table between the reassignment and the fill. The CLR's type-initialization guarantee now serialises concurrent triggering accesses, so a thread that observes a non-null field observes the completed array, and any future assignment outside the static constructor is a compile-time CS0198 rather than a runtime race.

Closes #824.

This pull request targets the epic integration branch epic/review-residuals-2026-09-08-integration, not the default branch, so the closing keyword above will not fire automatically; closure follows when the integration branch reaches main.

Changed files

Two source files:

  • UtilitiesCS/NewtonsoftHelpers/SDIL Reader/ILGlobals.cs
  • UtilitiesCS.Test/NewtonsoftHelpers/SDILReader/ILGlobals_Tests.cs

Plus this feature's plan, spec, 51 evidence artifacts and 3 audit artifacts. No project file was edited, which is the preferred AC10 outcome: the new tests went into the already-registered ILGlobals_Tests.cs. Four further files were owned by this feature so no sibling would edit them concurrently, and all four end the change unchanged: MethodBodyReader.cs at 299 lines, MethodBodyReader_Tests.cs at 489 lines against the 500-line cap, UtilitiesCS.Test/Properties/AssemblyInfo.cs, and UtilitiesCS.Test/UtilitiesCS.Test.csproj.

Tests

The two gates are deterministic and were red before the fix and green after. No sleep, delay, retry, timing tolerance, extra thread or repeated-run count is used anywhere, which the repository unit-test policy requires and which matters especially here because the defect is a race.

  • LoadOpCodes_DoesNotRepublishPublishedTables captures both fields into locals, calls LoadOpCodes(), and asserts reference identity with FluentAssertions BeSameAs. Fail-before run: exit 1, TOTAL=13, FAILED=1, failing at ReferenceTypeAssertions.BeSameAs.
  • SingleByteOpCodes_FieldIsInitOnly and MultiByteOpCodes_FieldIsInitOnly resolve each field by reflection and assert FieldInfo.IsInitOnly. Fail-before run: exit 1, TOTAL=15, FAILED=3.
  • Pass-after run: exit 0, TOTAL=15, PASSED=15, FAILED=0.

The pinned totals are discovery-count controls, so a run in which a named test merely failed to appear would not satisfy the condition.

OpCodeTables_ContainEveryOpCodeDeclaredOnOpCodes replaces two weak spot checks with an exhaustive enumeration. It passes on the unfixed tree as well, so it is a supporting test and not a gate for this defect, and it is labelled as such in the source and in every artifact that mentions it.

The four existing LoadOpCodes_* tests were reworked: two renamed to SingleByteOpCodes_IsPublishedWithFullLength and MultiByteOpCodes_IsPublishedWithFullLength with their LoadOpCodes() Act removed so that the first read of the static field is what triggers publication, and two spot checks deleted.

Toolchain

Full C# toolchain, in repository order, one loop restart (CSharpier reflowed this change's own new code on the first pass, so the loop restarted at step 1 as required):

  1. dotnet tool run csharpier format . then dotnet tool run csharpier check . — exit 0, second pass rewrote nothing.
  2. msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true — exit 0. Non-vacuity measured, not assumed: 0 skipped CoreCompile, 12 CoreCompile lines, 0 diagnostics.
  3. msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true — exit 0. 0 skipped CoreCompile, 13 CoreCompile lines, zero CS86xx errors and zero CS86xx warnings.
  4. Coverage-enabled test run — exit 0. Total 7212, passed 7212, failed 0, skipped 0.

Step 3 discharges a risk the spec recorded as unverified: a non-nullable static readonly field definitely assigned in the static constructor does satisfy the compiler's null-state analysis, so no CS8618 is raised and no null! suppression is needed.

Coverage

Repository line coverage 85.6204% before, 85.6241% after; branch 79.8129%. Both the 80% floor in CLAUDE.md and the 85% floor in the general unit-test rule are met, as is the 75% branch floor.

Per class, which is what the acceptance criterion actually compares:

Class Line before Line after Branch before Branch after
SDILReader.ILGlobals 0.945946 0.950000 0.875000 0.875000
SDILReader.MethodBodyReader 0.973262 0.973262 0.894737 0.894737

Changed-code coverage is 9 of 9 instrumented added lines. The only uncovered lines in the class are the unreachable Invalid OpCode. throw, which was uncovered at baseline too.

No coverage threshold, exclusion list or analyzer severity was lowered, weakened or deleted, and no production file was added to an exclusion list. No [ExcludeFromCodeCoverage] attribute was added.

Acceptance criteria

Work mode is full-bug, so spec.md is the sole acceptance-criteria source. All 12 of AC1..AC12 are checked off, verified against the committed tree rather than accepted from a report. An independent feature review evaluated all 12 and returned 12 PASS, 0 PARTIAL, 0 FAIL, 0 UNVERIFIED, with zero blocking findings.

Known items carried forward, not silently dropped

  1. The doc comment introduced by this change at ILGlobals.cs:172-173 says LoadOpCodes() is retained for "the five existing call sites". That count was correct before this change; the test rework in AC7 removed three of the four call sites in ILGlobals_Tests.cs, so exactly two remain. It affects no behaviour and no acceptance criterion, and the method is retained regardless of caller count. It was left unfixed deliberately rather than overlooked: editing production source after the recorded final clean toolchain pass would make that evidence no longer describe the delivered tree, which would require re-running all five gates for a comment.

  2. ILGlobals.Cache and ILGlobals.modules remain unsynchronised public mutable statics. Both are recorded as explicit non-goals in the spec, with a local follow-up record under the feature's evidence tree. A real issue is still owed for them, because feature-folder prose does not survive archival.

  3. The outcome still needs reporting to issue 811, whose acceptance criterion AC4 is blocked by this race. That is deliberately deferred until the fix reaches main. This pull request does not close 811.

  4. The approved plan's base-anchor decision anchors every diff to the merge base with origin/main. For an epic child based on an integration branch that merge base sits behind every already-merged sibling, so the anchored diff attributes inherited sibling paths to this feature. One plan task is left unchecked on exactly that ground rather than falsely ticked. The change footprint was instead measured against the true branch base and is exactly the owned write set. The defect is in the planning decision, not the delivery, and belongs upstream.

🤖 Generated with Claude Code

https://claude.ai/code/session_01URyGon15RvDXcUEo96tvyh

drmoisan and others added 4 commits September 9, 2026 15:42
Records the policy audit, code review and feature audit for the ILGlobals
static-publication fix. Zero blocking findings; all twelve acceptance
criteria evaluated PASS against the committed evidence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01URyGon15RvDXcUEo96tvyh
@drmoisan
drmoisan merged commit 96fd3dd into epic/review-residuals-2026-09-08-integration Sep 9, 2026
drmoisan added a commit that referenced this pull request Sep 9, 2026
… gate

Feature 824 merged as PR #833 (merge commit 96fd3dd), verified against
gh pr view and the remote integration tip; the fan-in introduced zero
deletions. The integration CI run dispatched after 823's merge concluded
success, and a sixth run is dispatched against 96fd3dd.

Feature 825 is the last of wave 0 and the load-bearing dependency for the
single wave-1 feature, so wave 1 does not open until its merge is durably
confirmed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01URyGon15RvDXcUEo96tvyh
@drmoisan
drmoisan deleted the bug/ilglobals-loadopcodes-unsynchronised-static-race-824-exec branch September 12, 2026 13:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant