Fix #2392: consistent package order for <Program> TypeDef and MethodDef row planning#2393
Merged
Merged
Conversation
…ef row planning The emitter's <Program> TypeDef loop always emits the entry-point/ globals-host package's TypeDef first (issue #191 requires this to keep its FieldDef range monotone), but MethodDef row planning walked `packages` in raw first-seen-syntax-tree order. ECMA-335 requires TypeDef.MethodList to be non-decreasing across the TypeDef table, since method ownership ranges are derived from consecutive MethodList values. Whenever the entry-point package was not already packages[0] (a function of build-tool/file enumeration order, not package identity), forcing its TypeDef first while its rows were planned last produced a non-monotone MethodList sequence, silently attributing the synthesized entry point, hoisted top-level local functions, and non-capturing top-level lambdas to a sibling package's <Program> -- surfacing as ilverify FieldAccess/MethodAccess failures when that sibling has no accessibility into the entry-point package's private members. Reorders `packages` once, before row planning, so every package-ordered loop (row planning, method-body emission, and the TypeDef loop) shares one entry-point-first order. Adds two regression tests covering a 2-package and 3-package shape where the entry-point package is not first in syntax-tree order, asserting method ownership via PE metadata, ilverify, and runtime output. Audited nested-type ownership (closures, SM classes) which use explicit NestedClass metadata rows keyed by TypeDefinitionHandle, not the implicit MethodList-range mechanism -- confirmed unaffected by this class of bug. Verified against the real Oahu.Diagnostics migration corpus (pre-existing translated .gs sources, compiled directly with gsc since cs2gs's native top-level-statement translation from #2382 is not yet on main): the previously-stable 13 ilverify FieldAccess/MethodAccess findings (4 distinct fingerprints) are gone in both original and reordered file-argument orders. Full suites: Core.Tests 6298/6298 (1 skip), Compiler.Tests 3110/3110, InternalAnalyzers.Tests 7/7, Cs2Gs.Tests (serialized) 1410/1410. Closes #2392
…ogram-package-holder
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #2392: multi-file top-level-program members (
<Main>$, hoisted top-level local functions, and non-capturing top-level lambdas) emitted into the wrong package's<Program>holder whenever the package-less ("Default") top-level-statement (TLS) file was not the first syntax tree the binder saw.Root cause
ReflectionMetadataEmitter's "Phase A" TypeDef loop always emits the entry-point/globals-host package's<Program>TypeDef first in the TypeDef table — issue #191 requires this so the entry package'sFieldDefrange (top-levelvar/let/constglobals) stays monotone relative to every other package's (empty) field range.MethodDef row planning, however, walked
packagesin raw first-seen-syntax-tree order (packageByTree's first-occurrence order, itself a function of build-tool/file enumeration order, not package identity):ECMA-335 §II.22.37 requires
TypeDef.MethodListto be non-decreasing down theTypeDeftable — the CLR (andilverify/decompilers) derive eachTypeDef's owned method range from consecutiveMethodListvalues, not an explicit foreign key. Whenever the entry-point package was not alreadypackages[0], forcing its<Program>TypeDef to the front of the table while its method rows were planned last (or in the middle) produced a non-monotoneMethodListsequence. The rows that are really the entry-point package's<Main>$/ hoisted func / lambda then silently attribute — via the same range-lookup logic every metadata reader uses — to a sibling package's<Program>, which has no accessibility into the entry-point package's private/internal members. That surfaces asilverifyFieldAccess/MethodAccessfailures.This was discovered migrating the real Oahu.Diagnostics app after #2382 added native top-level-statement translation:
Program.cshas no C# namespace (→ package-less "Default"), while the siblingChecks/*.csfiles (→ packageOahu.Diagnostics.Checks) andDiagnosticResult.cs/DiagnosticRunner.cs(→ packageOahu.Diagnostics) are bound first, soDefaultwas neverpackages[0].Fix
Reorder
packagesonce, right before row planning, so every package-ordered loop below it (row planning, method-body emission, and the TypeDef-emission loop itself) shares one entry-point-first order:This is a no-op (identical output) whenever the entry-point package already happens to be
packages[0](the common case, and every existing passing scenario), and is also a no-op for library compilations / programs with no top-level statements (entryPointPackagefalls back topackages[0]itself).Audit of other synthesized entry-point-adjacent members
Per the issue's request, I audited every other synthesized type/member that could plausibly share this bug class:
<Program>via explicitNestedClassmetadata rows (Metadata.AddNestedType(nestedHandle, enclosingHandle)), looked up fromprogramTypeDefHandlesby dictionary key (PackageSymbolreference), not by row-range. These are immune to the ordering bug — confirmed by readingReflectionMetadataEmitter.cslines ~3383-3415.<Program>row range — unaffected.functionsByPackagebucket: the entry point, ordinary top-level funcs, extension functions, and non-capturing top-level lambda literals promoted to static<Program>methods) rely on the implicit range-based ownership mechanism this bug breaks. All of them are covered by the fix and the new regression tests.Tests
Added
test/Compiler.Tests/Emit/Issue2392TopLevelProgramPackageHolderEmitTests.cs:TwoPackages_EntryPointPackageSecond_HostsEntryPointHoistedFuncAndLambdaOnItsOwnProgram— 2 packages, package-less TLS file (with a globallet, a top-level func, and a non-capturing top-level lambda calling it) bound second. Asserts viaTypeDefinition.GetMethods()(the same range-based ownership resolution a real CLR consumer uses) that<Main>$/the top-level func land onDefault's<Program>and notPackageA's, thatPackageA's own func still lands correctly on its own<Program>, plusilverifyand runtime-output checks.ThreePackages_EntryPointPackageMiddle_HostsEntryPointOnItsOwnProgram— 3 packages with the package-less TLS file bound in the middle, proving the fix reorders by package identity rather than special-casing "first" or "last".Both tests fail before the fix (verified by temporarily reverting the emitter change and rebuilding — the 2-package repro produced
ilverifyCallCtor/StackUnexpected/ThisUninitReturnerrors) and pass after it.Full suite results (rebased on
origin/main@358e3e20, incorporating #2382/#2386 native top-level-statement translation, #2383, #2384, #2385, #2386, #2387)--filter FullyQualifiedName~Issue2392)RunConfiguration.DisableParallelization=true)Issue2385NullableSameCompilationStructGenericArgEmitTests.cs, now merged in — no regressions)Real Oahu.Diagnostics end-to-end pipeline verification (now unblocked by #2386)
With #2382/#2386 (native top-level-statement translation) merged to
main, the fullcs2gs migratepipeline can now translate real Oahu C# source directly, superseding the standalone-gsc-only verification from the original PR description below. Ran directly against the real Oahu repository (read-only; no Oahu files modified):Result:
This is the definitive real-world confirmation: translate → compile → ilverify → test-parity all pass end-to-end for Oahu.Diagnostics with this fix + #2386 both applied — the exact scenario #2392 was filed against.
git status --porcelainin the Oahu working tree remained empty (only the two pre-existing untrackedilverify.allow-unsafewaiver files present) throughout this verification.Real Oahu.Diagnostics corpus verification (original, pre-#2386, kept for history)
cs2gs's native top-level-statement translation (#2382) was not yet onmainat the time this PR was opened, socs2gs migratecould not re-translateProgram.csfrom this branch. I verified directly against the already-translated.gscorpus captured during #2382's investigation (13 files:Program.gs,DiagnosticResult.gs,DiagnosticRunner.gs, 10 files underChecks/), compiling it standalone with this branch'sgsc.dllagainst the same reference set used by the realdotnet build(Oahu.Data.dll,Oahu.Decrypt.dll,Oahu.Foundation.dll,System.CommandLine.dll, EF Core, etc.):ilverifyreported 13 errors (2×FieldAccess×5 offsets + 1×MethodAccesson<lambda10>, and the same on<Main>$) — reproducing the exact 4 fingerprints from gsc: multi-file top-level program members emit into the wrong package holder #2392, landing on whichever sibling package (Oahu.DiagnosticsorOahu.Diagnostics.Checks, depending on file-argument order) happened to bepackages[0].ilverifyreports "All Classes and Methods ... Verified." in both the original argument order and a reordered (Checks/*first,Program.gslast) order.No changes were made to the Oahu repository or any of its tracked/untracked files.
Deferred / follow-up
None identified beyond this fix — the audit above found no other synthesized member class sharing this bug. The full end-to-end
cs2gs migrate --app Oahu.Diagnosticspipeline confirmation (previously deferred pending #2382/#2386) is now complete and passing, as shown above.