Blocker map: strings & bytes test suites (Phase-4 packages #3–4 work order)
Read-only scout, 2026-07-17, against master
1fb3eae1c(post/vNfix;testing.AllocsPerRunwas still in flight). Verified by construction, not hypothesized: the scout iteratively scratch-patched every blocker until both packages compiled and ran through the real test host — end state strings 57/69 PASS, bytes 74/82 PASS — so each fix sketch below was demonstrated to clear its blocker. The tree was fully restored afterward.
STATUS (2026-07-22): CLOSED. Every row below is resolved — build blockers B1–B10 (B10a re-bucketed into B9) and runtime blockers R1–R14 — and both packages validated 2026-07-18: bytes #3 (81 tests, 7 disclosed-divergent) and strings #4 (68 tests, 4 disclosed-divergent), test sources banked per the validated-package policy. Sections below are in merge order, so an earlier section’s “still open” note may be superseded by a later one — trust the later section. Kept as the worked example of a complete package arc: scout → build blockers → runtime blockers → differential → disclosed-divergence ruling → bank.
Open spin-offs this map produced (tracked in
Phase4-Autonomous-Loop-Charter.md, not here): (1)reflect.Kind()/Elem()of adapter types still report the adapter class — folded into the reflection-bridge chip (charter §3 Tier-0 #2 / §6.1); (2) the@stringperformance cliff — strings’TestCompareStringsruns ~109 s in the C# runtime on theunsafeString→@stringcopy path; never given a row, and the reason §1’s pipeline command carries-test-timeout 10m(charter §9); (3) golib slice nil-identity adjacent gaps (zero-arg variadic, named-slice wrapper== nil,NilType’sISlicearm) — recorded indocs/ConversionStrategies-Reference.md; (4) cosmetic: the tests-csproj template’s<OutDir>override defeats its ownBaseOutputPath=bin\tests\— align when next touching the template.
The Step-3 sweep’s census reproduces exactly: strings 64/68 tests included (3 × AllocsPerRun,
1 × AllocsPerRun+CoverMode), bytes 81/88 (7 × AllocsPerRun). The sweep-era “CS0234
go.unicode ×64 vs CS0050 abi” environment-dependence is gone/superseded — both symptom sets
were downstream of the same two graph defects (B1 + B2b) under differing go2csPath origins. No
abi-accessibility errors exist under the current absolute-path pipeline.
Build blockers
| # | Blocker | Pkg | First error | Root cause (evidence) | Minimal fix | Size |
|---|---|---|---|---|---|---|
| B1 | -tests regenerates the production csproj with raw core\ refs, clobbering the committed go-src-converted\ shape |
both | CS0246 storm in src/core/errors (600 errors) | main.go:877 runs the full production conversion whose ref writer (main.go:1782) emits $(go2csPath)core\<pkg>; the graph reaches it via internal/testenv → internal.testenv.csproj:128 back-ref to strings.csproj. utf8 masked this (its production project refs only golib). Several core\ targets don’t even exist in the stub (unicode, internal/bytealg, internal/stringslite) |
Route production-csproj stdlib refs through the same F15 mapping as resolveTestProjectReference when the output root is the go-src-converted tree | S |
| B2 | Name-collision analysis diverges between production emission and test-variant emission | strings | CS0102 strings_package already contains Replacer + CS0246 ΔReplacer |
export_test.go adds a method Replacer → the test-variant analysis (whole variant universe) renamed the type to ΔReplacer, but the production .cs on disk (production-only universe) kept Replacer. Two halves of one assembly disagree |
Pin production symbol names as immutable in test-variant analyses; collisions resolve by renaming the test-side declarator (method → ΔReplacer). Validated by hand-patch |
M |
| B2b | In-namespace alias using io = io_package; collides (CS0576) when any referenced assembly contributes child namespace go.io |
strings (bytes precluded) | CS0576 ×~10 + CS0234/CS0535/CS0539 cascade in .g.cs | Transitive project refs flow internal/testenv → io/fs (namespace go.io) into the test compilation. The Δ-alias machinery (cf. bytes’ Δunicode) is computed against the production import closure only. Generator fallout: field types become error types, so the generator pastes raw io.Writer instead of global::go.io_package.Writer |
One line in the embedded tests-csproj template: <DisableTransitiveProjectReferences>true</DisableTransitiveProjectReferences> — the compile view becomes exactly the direct refs the test converter computed. Validated |
XS |
| B3 | package_test_info.cs attrs can’t see test-package types | both | CS0246 errWriter (strings), negativeReader/panicReader/TestReaderCopyNothing_just* (bytes) |
The seeded info file has using static go.<pkg>_package; only; GoImplement attrs referencing types nested in <pkg>_test_package don’t resolve |
Add using static go.<pkg>_test_package; in the seeding writer (or subsumed by B4/B5’s split) |
XS |
| B4/B5 | ImplementGenerator anchors all adapters to the FIRST class in the attr-bearing file — test assemblies have two consumer packages | both | CS1929 on errWriter adapter; CS0246 strings_BuilderжWriter, bytes_BufferжWriter, strings_ReaderжReader, os_FileжWriter; CS0120/CS0034 in TestReaderCopyNothing_just* adapters |
ImplementGenerator.cs:126 GetFirstClassName(compilationUnit) → strings_package (declared first in the info file), so test-introduced casts generate local-named adapters in the wrong class, while the converter emits consumer-perspective names hosted in the test class. Foreign adapters are by design hosted in the consuming package — the merged single info file destroys the two-consumer distinction |
Converter-only fix: emit test-introduced GoImplement/ImplicitConv attrs into a SEPARATE compilation unit whose first class is the test package class (production-seeded attrs stay anchored to the production class). Avoids touching go2cs-gen and its full-suite+corpus gate. Validated by hand-hosting the adapters | M |
| B6 | core/testing shim missing compile-surface | both | CS1061 ReportAllocs/SetBytes/ResetTimer/StartTimer/StopTimer/Errorf/Fatal/Fatalf on B; AllocsPerRun; CoverMode | Benchmark bodies and capability-excluded tests still COMPILE (exclusion gates the run list, not emission) | Add no-op B members (+ explicit ж<B> overloads for the params ones), CoverMode() => "", and AllocsPerRun (landed separately). All validated |
XS–S |
| B7a | Go int constant > int32 emitted as bare L literal |
both (1 site each) | CS1503 long → nint | math.MaxInt64/4 in the SplitN tables → 2305843009213693951L with no cast |
Constant renderer wraps int-typed constants exceeding int32 in (nint) |
S |
| B7b | Func literals lose their declared result type | strings ×3, bytes ×2 | CS1503 Func<int, UntypedInt> / CS8917 |
var maxRune = (rune r) => Δunicode.MaxRune; — body returns an untyped constant (or mixed paths) so C# natural-type inference fails |
convFuncLit: emit explicit lambda return type (var f = rune (rune r) => …) whenever the Go literal declares a result type |
S |
| B8 | Cross-file dynamic-struct resolution failure | bytes | CS1526 + ~170 parser-cascade errors from ONE site | foreach+heap over compareTests (anonymous []struct{a,b []byte; i int} declared in compare_test.go) emitted raw Go type text at bytes_test.cs:64 — the synthesized compareTestsᴛ1 exists in compare_test.cs:15 but isn’t found cross-file. Known ToDo class, now with a reproducer. Site is inside an AllocsPerRun-excluded test — excluded tests still block builds |
Dynamic-struct registry must unify anonymous struct types across files of the (test-)package before emission | M |
Runtime blockers (found by actually running the hosts)
| # | Blocker | Failing tests | Root cause (evidence) | Fix sketch | Size |
|---|---|---|---|---|---|
| R1 | []T(nil) conversion throws |
strings cctor cascade (~30 tests) | append([]string(nil), …) → slice<@string>(default!) → builtin.cs:1624 slice<T>(T[]) throws ArgumentNull on null; Go says nil→nil slice |
golib: null array → default (validated; cleared the cascade) | XS |
| R2 | internal/godebug not operational | strings cctor (via math/rand) | godebug.cs:170 Value() → setting.value atomic pointer only populated by Go-runtime update hooks that never run → nil deref |
Hand-owned minimal Value() (parse %GODEBUG% or return “” = Go’s unset default) |
S |
| R3 | runtime_rand linkname stub | strings TestIndexRandom + cctor | rand.cs:375 PartialStubGenerator → NotImplementedException | rand_impl.cs companion supplying a real RNG (validated) | XS |
| R4 | len(string) = UTF-16 char count |
strings TestIndexAny/TestLastIndexAny/TestLastIndexByte | Tables use len("a☺b☻") where the literal stayed a plain C# string (u8 suppressed per-arg via u8StringArgOK, convExprList.go:89) → builtin.cs:1144 returns .Length = 4, Go = 8 |
golib: Encoding.UTF8.GetByteCount (validated) + audit other System.String-accepting golib APIs with length/index semantics | XS + M audit |
| R5 | reflect.DeepEqual → converted unsafe.Pointer NRE | strings ×4, bytes ×2 | deepequal.cs:74 → unsafe.cs:261 Pointer.op_Implicit on null managed slot | reflect/unsafe managed-slot null handling | M |
| R6 | MakeNoZero throws .NET OverflowException, not a Go panic | TestRepeatCatchesOverflow (both) | bytealg_impl.cs:9 hand-owned impl; recover() only catches go.PanicException | Validate n and throw panic("runtime: makeslice: len out of range") |
XS |
| R7 | string([]rune) with invalid runes throws |
strings TestCaseConsistency | ToUTF8Bytes (builtin.cs:299) rejects surrogates; Go encodes U+FFFD | golib: invalid rune → RuneError bytes | XS |
| R8 | array<T> zero-value enumeration NRE |
strings TestFinderCreation/Next | [256]int default array |
golib array |
S |
| R9 | ж<T>.ToString() pointer-print crash |
strings TestClone | PrintPointer → PinnedBuffer[index] IndexOutOfRange | golib PrintPointer bounds handling | S |
| R10 | %T prints adapter class name | strings TestPickAlgorithm | Prints strings.byteReplacerжreplacer, Go wants *strings.byteReplacer |
TestFormat/golib: unwrap IжAdapter for %T | S |
| R11 | Identity-Map copies (unsafe.StringData identity) | strings TestMap | Zero-copy fast-path identity not preserved through @string | Semantics decision — possibly an acceptable-difference disclosure | ? |
| R12 | Nil-receiver method derefs before the nil guard | bytes TestNil | Go’s (*Buffer).String() checks b == nil first; emitted preamble ref var b = ref Ꮡb.Value (buffer.cs:70) derefs unconditionally |
Converter: receiver preamble must not precede a reachable nil-receiver guard | M |
| R13 | nil-vs-empty slice distinction | bytes TestClone/TestTrim/TestTrimFunc | TrimRight("a","a") must return nil, Clone([]) non-nil empty — golib doesn’t preserve the distinction |
golib slice nil-identity semantics (subtle; interacts with R1) | M |
Capability enumeration
- testing.CoverMode — exactly one user across both packages: strings_test.go:325 (TestIndexRune):
if allocs != 0 && testing.CoverMode() == "". A constant-""shim member is exactly correct — Go returns""when coverage is off, and the test then takes the same path as an uncoveredgo testrun. - t.Parallel — zero uses in the included sets. All four bytes sites live in boundary_test.go, which
is
//go:build linux→ platform-excluded by the census on windows/amd64. No host gap for #3–4. - testdata/fixtures — neither package has a testdata/ dir.
- internal/testenv surface — only
testenv.Builder()(strings TestCompareStrings — passed) andtestenv.SkipIfOptimizationOff(t)(bytes TestNewBufferShallow, AllocsPerRun-excluded anyway). Both work through the converted testenv; no TB-surface gap for these two packages.
Sequencing recommendation
- B1 + B2b + B3 + B6 (all XS/S) get both packages to the interesting errors immediately; B2b is one template line and precludes the whole CS0576 class for every future package whose test deps drag nested stdlib packages.
- B4/B5 is the structural decision (converter-side two-anchor split — avoids the go2cs-gen full-suite+corpus gate).
- B7a/B7b/B8 are self-contained converter emission fixes; each deserves a behavioral guard test.
- Runtime: R1+R2+R3+R4 alone took strings from 23→57 PASS in the scout run — highest leverage. R5 (DeepEqual) clears 6 tests across both packages. The tail (R7–R13) is per-test polish.
Status updates (2026-07-17 evening)
- B1 + B2b + B3: FIXED (master
3ef721665, chip commit9400f3680) —resolveProductionProjectReferenceroutes-testsproduction-csproj refs through the F15 mapping (pass-through otherwise, CNR byte-identical ×399); the tests-csproj template setsDisableTransitiveProjectReferences;appendExternalTestPackageClasswidens the info file’susing staticscope to the external test class. Three converter guards. utf8 re-validates 14/14 through the changed pipeline; sort’s production csproj survives a-testsrun with only the intended IP-4 exclusion diff. - B6: FIXED (master
21dd3da1c) — compile-onlyBsurface (ReportAllocs/SetBytes/ResetTimer/StartTimer/StopTimer/Errorf/Fatal/Fatalf) +CoverMode() => ""; discriminating guardBenchmarkCompileSurfaceIsNoOpAndCoverModeReportsCoverageOff.testing.CoverModecensus inclusion followed as a coordinator commit, so strings censuses 68/68 included once it builds. - NEW — B2c (exposed by B2b’s fix; 1 × CS0234 in sort): the seeded global alias
reflectliteꓸKind = go.@internal.abi_package.ΔKind(package_test_info.cs) targetsinternal/abi, which sort reaches only transitively (sort → reflectlite → abi) — now hidden from the test compile view byDisableTransitiveProjectReferences. Ruling: the converter must emit direct F15-mapped project references for every assembly a seeded alias targets (it knows the alias set it seeds); NOT subsumed by B4/B5’s anchoring split — the alias survives in whichever compilation unit hosts the production-seeded attributes. Folded into the B4/B5 chip. - Sort’s wall after these fixes: 1 × CS0234 (B2c) + 10 × CS0246 adapter anchoring (B4/B5) — exactly the documented next tier; runtime rows R1-R13 still unreached.
- B8: FIXED (worktree branch
claude/eloquent-mestorf-c5e017) — the general type-name renderers (getTypeName/getFullTypeName) now resolve a non-empty anonymous struct/interface through the shared dynamic-type registry / deferred marker (deferredDynamicTypeName) instead of falling through to rawt.String()Go text; the«DYNTYPE:…»marker payload is now the HEX-ENCODED signature so it survives the string-transform passes (convertToCSTypeNamerewrites[/]→</>, which corrupted a raw-signature marker before post-barrier resolution). Guard:AnonStructCrossFile(three files, both visit-order directions, heap-boxed range var; CS1526 without the fix). CNR byte-identical ×399. Bytes probe after the fix: the ~170-error parser cascade is gone —bytes.tests.csprojis down to exactly 2 × CS0246 in ONE generated adapter (negativeReader→ io.Reader ptr impl inbytes_package’s .g.cs), i.e. the B4/B5 anchoring class. Bytes’ next wall = B4/B5 (+ B7a/B7b sites at their lines), then the runtime rows. - B7a + B7b: FIXED (2026-07-17, worktree branch
claude/modest-ramanujan-87ebb2— coordinator gates the merge). B7a: the typed-intsigned constant fold (overflowingConstLiteral) now carries its own(nint)(…L)cast, parenthesized so the assignment path’snativeIntConstCastTyperecognizes it and does not re-wrap — zero corpus drift (NativeIntWideConstAssignbyte-identical). B7b:convFuncLitstates an explicit lambda return type when a single-result BASIC-numeric literal in assignment position has a named untyped-const return arm (targeted predicate, not the uniform fallback — no golden churn; argument-position and literal-only-arm literals keep the plain form). Guards:NativeIntWideConstElement+FuncLitUntypedConstReturn, both discriminating — reverted-fix runs fail with exactly the mapped errors (CS1503 long→nint; CS1503Func<int, UntypedInt>). - B4/B5 + B2c: FIXED (worktree branch
claude/trusting-engelbart-161588) — converter-only, as ruled. (a) The EXTERNAL variant’s GoImplement/GoImplicitConv records now split across TWO anchors: test-anchored records (bare test-local impls, every non-production ж pointer adapter, adapter-class-marked ᴠ pairs) land in a NEW compilation unitpackage_info_external_test.cs(namedpackage_info_test.cswhen this report was written) whose first — and only — class is the test package class (bare partial, no[GoPackage]— that stays onpackage_test_info.cs’s appended block, CS0579), so the generators host their output where test-file cast sites resolve it; production-qualified records keep the production anchor. The_test.cssuffix free-rides the committed*_test.csproduction exclusions — no shared-csproj-template edit (which would churn every behavioral csproj). The unit is only written when the variant records test-anchored attrs — utf8’s committed shape is byte-identical (re-validated 14/14, git clean). (b) Same-assembly naming coherence: the production-under-test package’s pairs are pre-loaded from its seeded package_info.cs, so production-type pointer casts reference the seeded adapter through the aliased qualifier (sort.XжIface, not the never-generatedsort_XжIface), value casts fall through to the plain emission the partial-struct route implements (sort_IntSliceᴠInterfaceᴠ-adapters are never generated for same-assembly types), and interface-source adapters compose unprefixed — in every case matching what ImplementGenerator (foreign = containing-ASSEMBLY) actually emits. (c) B2c:using-alias lines in the final test metadata are scanned forgo.-rooted_packagenamespace tokens, reverse-mapped through the transitive import closure (the same/vN-collapsing renderer that emitted them), and any target not directly referenced gets a direct F15-mapped project reference (sort:internal/abi); manifest dependencies stay import-derived. Guards:TestExternalVariantRecordPartitionAnchors,TestWriteExternalVariantMetadataSplitsAnchors,TestAliasReferenceImportsAddsTransitiveAliasTargets. CNR byte-identical ×399. - Sort’s wall after B4/B5 + B2c (wave 2 — method-body errors the declaration errors had
MASKED; Roslyn skips method-body binding while declaration errors exist, so these were
invisible to every earlier probe): 23 errors, sort still does NOT build. New rows:
- B9 (new, M?): 14 × CS1501 — sort_test.go dot-imports sort AND example_keys_test.go
declares a METHOD
Sorton itsBytype; Go keeps those namespaces separate, but the converted method becomes a staticSort(this By, …)member ofsort_test_package, and C# member lookup prefers the enclosing class’s method group overusing staticimports — every dot-importedSort(x)call binds the wrong group. Name-collision family (B2’s cousin: production symbols must also be pinned against test-package METHOD names under dot-import). - B10 (new, S-M): 6 × CS1503 — delegate-typed argument mismatches: named func type
Bypassed where the emitted parameter is the rawFunc<ж<Planet>, ж<Planet>, bool>(example_keys_test.cs:29), and method groups passed whereAction<sort_package.Interface>is expected (5 sites, sort_test.cs 569/784/821/829 + one more in the same family). - B7a-family: 2 × CS1503 numeric-constant typing (
long→nintsort_test.cs:769 = B7a exactly;double→nintsearch_test.cs:49 is a float-typed sibling). - B6-gap (XS): 1 × CS1929 — the shim’s compile-only
Bsurface lacksSkip(sort_test.cs:791b.Skip(…)); add the no-op + ж<B> overload beside the existing eight. Sort’s dir was fully restored after measurement (validated-package policy: only a validating package commits its test sources).
- B9 (new, M?): 14 × CS1501 — sort_test.go dot-imports sort AND example_keys_test.go
declares a METHOD
- B2 + B9: FIXED (worktree branch
claude/friendly-archimedes-833505— coordinator gates the merge). One shared mechanism, as ruled: production symbol names are IMMUTABLE in a test-variant analysis; a collision a test file introduces Δ-renames the TEST-side method declarator.performNameCollisionAnalysisnow tracks per-name declaration origin — (B2) an element-vs-method collision whose methods are all test-declared over a production element pins the element (no nameCollisions entry, no exported alias) and renames the method; (B9) a test-declared method matching a dot-imported foreign function the variant references UNQUALIFIED (Sel-excluded AST scan over the whole universe — qualified refs never trigger) renames the same way. The registry (testMethodRenames) is OBJECT-keyed (same-named production symbols keep their plain emission) and SESSION-scoped (one per -tests run, not per variant — both variants share one load, so the external variant’stc.r.ΔReplacer()resolves the internal pass’s rename by object identity). Declaration renames in visitFuncDecl; every reference follows via convIdent’s isMethod arm; the RecvGenerator ж-overloads follow from the emitted name. Guards:TestTestVariantPinsProductionTypeAgainstTestMethodCollision+TestTestVariantRenamesTestMethodShadowingDotImportedFunction, both discriminating (neutered-fix runs fail on the mapped symptoms). CNR byte-identical ×402; utf8 re-validates 14/14, git-clean. - Sort’s wall after B2/B9 (probe 2026-07-17): ALL 14 CS1501 GONE — and so are the 5
method-group CS1503 sites wave 2 attributed to B10: they were B9-DOWNSTREAM (the wrong method
group failing delegate conversion; another Roslyn-masking layer — B10’s real count was measured
with B9 present). Remaining: exactly 3 × CS1503 — sort_test.cs:769
long→nint(B7a exactly) + search_test.cs:49double→nint(B7a float sibling) + example_keys_test.cs:29By→ rawFunc<ж<Planet>, ж<Planet>, bool>(the one true B10 site). Runtime rows unreached; dir fully restored after measurement. - Strings’ wall after B2/B9 (probe 2026-07-17): B2’s CS0102/CS0246 GONE — strings now BUILDS
and runs through the test host end-to-end (first time). Wall moved to the runtime rows as
mapped: C# host runs 72 → 23 pass / 7 fail / 42 infrastructure-error, the infra bucket
dominated by the R1 cctor cascade (
slice array reference is nullviaNewReplacerin thestrings_test_packagecctor — the[]T(nil)signature). R1–R4 remain the next chip’s scope; dir fully restored after measurement. - B10 + B7-family remainder: FIXED / RE-BUCKETED (2026-07-17 late, worktree branch
claude/pensive-torvalds-58cda0— coordinator gates the merge; base masterb642642a5). Four defects fixed, one re-bucketed:- B10b FIXED — the composite-literal walk gains the MIRROR arm the call-site rule already
had: a STRUCTURAL func-type field receiving a value that renders as a named delegate wraps in
the synthesized structural delegate (
by: new Func<ж<Planet>, ж<Planet>, bool>(by), example_keys_test’s planetSorter). Guard:NamedFuncTypeStructuralField(method-group field control stays bare). - B7a-family
long→nint(sort_test.cs:769) FIXED — a NEW shape, not B7a exactly: the whole value ofmaxswap: 1<<31 - 1fits int32 (no whole-expression fold), but the untyped inner shift folds to a bare2147483648L, widening the rendering tolong; neither the argument path nornativeIntConstCastType(whole-value-out-of-range gate) narrows it.convBinaryExprnow wraps the emission itself:(nint)(2147483648L - 1), shape-restricted to folded OPERATOR operands (maxInt - maxIntwrapper refs stay unwrapped). Guard:NativeIntWideConstElementextension. - Float-const
double→nint(search_test.cs:49) FIXED — integer contexts now propagate throughmarkUntypedConstContexts(division excluded: exact-rational vs truncating/), and a float literal under an integer context renders its exact integer form:1e9 - 7→1000000000 - 7. Guard:NativeIntWideConstElementextension. - B7b-GAP (bytes_test.cs:1160) FIXED —
returnArmKeepsUntypedWrapperwidens the B7b predicate to constant paren/unary/binary arms containing a named untyped-const ref (bytes TestMap’sreturn utf8.MaxRune + 1), unless a constant fold rewrites the arm to a plain literal. Guard:FuncLitUntypedConstReturnextension (both must-stay-plain controls green). - B10a RE-BUCKETED into B9 — root-caused against the real emission: all five method-group
CS1503 sites pass
Sort, andsort_test_packagedeclares the convertedBy.Sortaspublic static void Sort(this By, slice<Planet>), so C# simple-name lookup binds the enclosing class’s method group and never reaches theusing staticproductionSort—Stable×6 andHeapsort×1 at the SAMEfunc(Interface)helper parameters convert cleanly, proving the method-group machinery itself is sound. These are the argument-position manifestation of B9’s dot-import shadowing (CS1503 instead of CS1501); the B9 pinning fix clears all 19. No separate converter defect exists. - Post-fix probe walls: sort = exactly 19 errors, all
Sortshadowing (14 CS1501 + 5 CS1503 = B9); bytes BUILDS CLEAN (0 errors) and its differential RAN — the C# host dies in thego.bytes_test_packagetype initializer (“The type initializer for ‘go.bytes_test_package’ threw an exception”, exit 2), so every included test reportsGo="pass" C#=""— the expected R-row wall (R1[]T(nil)et al.); runtime rows remain untouched per the chip split.
- B10b FIXED — the composite-literal walk gains the MIRROR arm the call-site rule already
had: a STRUCTURAL func-type field receiving a value that renders as a named delegate wraps in
the synthesized structural delegate (
Sort’s first full differential (2026-07-18, master 9c620008b)
Sort BUILDS clean and runs the complete differential: 53/63 included tests agree with
go test. Every divergence is root-caused and owned:
| Bucket | Tests | Root cause | Owner |
|---|---|---|---|
| runtime_rand stub (R3) | 7 (CountSortOps, CountStableOps, HeapsortBM, SortBM, SortLarge_Random, Stability, StableBM) | math/rand/v2 runtime_rand PartialStub NotImplemented |
R1-R4 chip |
| Embed-override dispatch | 2 (ReverseSortIntSlice, Float64s) | dispatch through reverse{Interface} does not call the overriding Less — reverse sorts ascend; NaN order breaks |
dispatch chip |
| reflectlite Swapper NRE | 1 (TestSlice) | nil ж deref at abi.Kind via reflectlite (R5 family) |
dispatch chip |
R14 (FIXED, master 9c620008b): --json now implies Verbose() — go test -json implies
-v (cmd/go passes -test.v), so the Go side of every differential runs verbose; the host
mirrored false, making every Verbose-gated test (sort’s countOps pair) a guaranteed skip-vs-pass
mismatch. Post-fix those tests RUN and surface their true wall (R3) — the honest progression.
utf8 unaffected (still 0-skipped both sides).
- R1 + R2 + R3 + R4: FIXED (2026-07-17, worktree branch
claude/youthful-newton-4156e6— coordinator gates the merge; no converter change, so CNR is trivially byte-identical). R1: golibslice<T>T[]-taking ctors map a null source to the nil slice (this = default) per Go[]T(nil); the bounded forms allow it only while every index stays at zero (Go’s legalnil[0:0]). R4: golibbuiltin.len(string)returnsEncoding.UTF8.GetByteCount— Go counts UTF-8 bytes,.Lengthcounted UTF-16 chars; audit of every other raw-string-accepting golib API found no second instance (@string/sstringtranscode at construction; remainingstringparams are format/message/parse-ASCII only). Guards:NilSliceConversion+StringLenUtf8Bytes, both discriminating (reverted-fix runs fail Output: exit-2 crash / UTF-16 counts). R2: whole-file hand-ownedinternal/godebug/godebug.cs([module: GoManualConversion], old output preserved asgodebug.cs.auto,godebug_impl.cssubsumed/removed) — parses $GODEBUG once on first use, “” unset default, unlisted-name panic kept; the scout’s “atomic pointer” nil deref is actually the generated promoted-field box of the embedded*settingtreating its held nil pointer as a nil DEREFERENCE even for the populating assignment (Setting.g.cssetting => ref Ꮡʗsetting.Value→ PanicException) — a latent TypeGenerator/ж defect for ALL post-construction embedded-pointer assignment, worth its own gen-gated chip. R3:rand_impl.cscompanions for math/rand AND math/rand/v2 (the failing site was v1’s rand.cs:375; both carry the same linkname) —runtime.randonRandom.Shared, correctly non-deterministic. Probe evidence (small driver against the built assemblies):godebug.Value()= “” unset / “0” underGODEBUG=randautoseed=0, and under that setting v1Int63()returns 5577006791947779410 — Go’s canonical first Seed(1) draw, proving the GODEBUG-routed deterministic path end to end; unset, v1/v2 draw random values through the new hook. R5–R13 remain.
🏁 SORT VALIDATED (2026-07-18) — package #2
With R1-R4 merged (32638f729), sort’s full differential went green:
Validated 63 tests against go test (1 skipped identically on both sides, 46
disclosed-unsupported declarations excluded). — every included test agrees, skip-parity holds
(TestSearchWrappersDontAlloc), and the converted test sources are committed beside the production
code per the validated-package policy. The path consumed, in order: B1, B2b, B2c, B3, B4/B5, B6
(+Skip), B7a (+fold-widening +float-context), B7b (+const-expr arms), B8, B9, B10, AllocsPerRun,
CoverMode census, /vN imports, –json-implies-Verbose (R14), array-copy cloning, IEEE float
equality, the reflectlite mini-bridge, the gen nil-embed fix, and R1-R4. Bytes/strings same-day
attempts ran deep and reported their R5-R13 tails honestly (bytes: DeepEqual/MakeNoZero/nil-empty
classes; strings: Builder-allocs/Map/Finder classes) — next wave’s work order.
- R12 + R13: FIXED (2026-07-18, worktree branch
claude/dazzling-golick-918c1a— coordinator gates the merge). R12 (converter): the RECEIVER of a direct-ж method now joinsnilSafePtrParamNamesunder a precise predicate — the body==/!=-compares the bare receiver ident (OBJECT identity viaidentResolvesToReceiver; a shadowing local never qualifies), gated on the direct-ж form (implied by the comparison itself viabodyUsesReceiverAsPointerValue’s promotion arm, checked explicitly) — so the entry preamble emitsref var b = ref Ꮡb.DerefOrNil();and the body’sif (Ꮡb == nil)guard runs where Go’s does; every non-comparing method keeps.Valuebyte-for-byte. CNR drift = exactly TWO files, both the mapped shape (the extendedPointerReceiverNilCompareguard +PointerReinterpretIdentity, whosecopyCheckis strings.Builder’sb.addr != b); no third shape. R13 (golib):slice<T> == nilis now REPRESENTATION nilness (m_array is null), the slice==slice operator (thes == default!emission ofs == nil) is Go header identity,Reslicepreserves a nil backing (nil[0:0]stays nil),Appendwith zero elements returns the source header unchanged (nil stays nil; bytes.Clone’sappend([]byte{}, empty...)stays non-nil), andbuiltin.widenprojects only a nil source to nil. Identity enumeration + known adjacent gaps (zero-arg variadic, named-slice wrapper== nil, NilType’s ISlice arm) recorded in ConversionStrategies-Reference.md. Guards:PointerReceiverNilCompareextension (nil-pointer calls print Go’s results; reverted-converter run fails Target + crashes exit-2) and NEWSliceNilVsEmpty(14 identity probes incl. theresliceTailCapZerooperator discriminator,nilResliceReslice discriminator,appendNilNothingAppend discriminator; reverted-golib run fails Output).NilSliceConversion(R1) stays green. Full behavioral suite: 408/408/408 + 379 output-compared, 0 failed. Bytes differential: 14 → 10 test mismatches — TestNil (R12), TestClone, TestTrim, TestTrimFunc (R13) all cleared; honest remainder = R5 ×2 (TestSplit/TestSplitAfter DeepEqual), R6 ×1 (TestRepeatCatchesOverflow), and 7 allocation-count divergences (TestEqual, TestGrow, TestIndex, TestIndexRune, TestLastIndex, TestNewBufferShallow, TestWriteAppend — C# allocates where Go’s optimized paths don’t; a distinct class, not nil semantics). Strings differential: 15 → 15 test mismatches, none R12/R13-related (unchanged classes R5–R11 + Builder-allocs) — and a measurement finding: strings’ host needs-test-timeout> 2m headroom because TestCompareStrings alone runs ~109 s in the C# runtime (theunsafeString→@stringcopy cost, a pre-existing performance gap worth its own row). - R5: FIXED (2026-07-18, worktree branch
claude/adoring-kirch-cd66c2— coordinator gates the merge; base masterf999c8f78). The reflect bridge gains DeepEqual: the converter skips ONLYdeepValueEqual(manualConversionFuncs["reflect"]—DeepEqualstays auto, its body only touches the bridged ValueOf/Type/AreEqual), andreflect/deepequal_impl.csre-implements the recursion arm-for-arm over the bridge’s BOXED values: elementwise arrays/slices (+[]bytespan fast path), nil-vs-empty slice from the REAL backing (m_arrayvia cached reflection — publicSourceis a detached copy), struct fields via goStructFields, maps key-by-key through the backing Dictionary (same-map identity short-circuit), pointer identity = ж-box reference equality, NaN ≠ NaN, funcs never equal unless both nil; cycle detection mirrors Go’s hard()/visited on managed-identity pairs (self-referential structures terminate). The committeddeepequal.cswas regenerated from a FULL -stdlib reconvert (filtered reconverts derive wrong cross-package names) — the only reflect drift was the placeholder swap. Guard:DeepEqualbehavioral test (30 cases output-compared vsgo run; discriminating — the pre-fix reflect crashes it at the first slice compare, exit 2 NRE). ItsDirectory.Build.targetsredirects the emittedcore\reflectref togo-src-converted\reflect(baseline has no reflect; the targets file survives csproj regeneration — the Performance-suite pattern). golib print/println now render bools gc-style (true/false) — surfaced by the guard’s output compare. Differentials: bytes 14 → 12 mismatches (TestSplit + TestSplitAfter flip to agreement), strings 15 → 13 (same two flip) — the four R5 tests exactly; remainder untouched. - Shared infrastructure-error diagnosis (the six-test class, from the real stacks): strings
TestSplit/TestSplitAfter + bytes TestSplit/TestSplitAfter are R5 (identical stacks:
deepequal.cs:74 → unsafe.cs:261
Pointer.op_Impliciton the never-populatedv.ptr). Strings TestReplacer + TestWriteStringError are R8, not R5: TestReplacer NREs at replace.cs:83 (r.replacements[o]— the[256][]bytefield left zero-value by the composite literal, so the defaultarray<T>’s null backing faults at array.cs:134 — the same class as TestFinderCreation/Next), and TestWriteStringError is R8-DOWNSTREAM: build’s panic runs undersync.Oncewhose deferred done-store marks the shared package-level Replacer built, so the nextDono-ops andr.ris still nil at replace.cs:109. Fixing R8’s zero-value array backing clears both (plus the Finder pair). - Differential-run note: strings’ C# host needs
-test-timeoutheadroom — TestCompareStrings legitimately takes minutes through the golib @string paths (the 2m default killed the host mid-run, reporting everything after TestCompareIdenticalString asC#=""); 8m completes. Slow ≠ hung.
Status updates (2026-07-18 — R10/R11 + the Builder-allocs/IndexRune analysis)
- Baseline moved before this chip ran: with R1–R4 merged, the strings host runs end-to-end at
57 pass / 5 fail / 10 infrastructure-error (72 run; full host ≈1m43s — the pipeline’s default
2m
-test-timeoutis borderline and killed the compare mid-run; pass-test-timeout 5mwhen driving strings through-test-action compare/all). The scout-era “23 pass / 42 infra” picture is obsolete. - R10: FIXED (worktree branch
claude/sleepy-germain-074709— coordinator gates the merge).%Tof a generated adapter now renders the Go dynamic type at BOTH format layers:GoReflect.GoTypeName(the reflection bridge’srtype.String(), which the converted fmt’s%Treads — the differential’s actual path) gainsTryAdapterWrappedType—IжAdapter→*T, ᴠ-infix value adapter → wrapped struct, both recovered structurally from the adapter’s single one-parameter constructor (never name-parsed; glyphs viaSymbols) — and golib’sbuiltin.GetGoTypeName(stub fmt%T,TestFormat, panic texts) unwrapsIжAdapter.Box/IInterfaceAdapter.Valueat the value level and routes ж-boxes/adapters/named types throughGoReflect.GoTypeName(sogo.main_package+softrendersmain.soft, rawж<loud>renders*main.loud). Guard:FormatTypeAdapters(+typelibsub-package for the foreign ᴠ arm), output-compared vsgo run; discrimination: reverted-fix run printsmain_package+loudжgreeter/ж`1[[go.main_package+loud, …]]/main_package+typelib_Markᴠstamper. Honest scope note: TestPickAlgorithm still reports infrastructure-error — its%Tmismatch (case 0) is gone, but later cases crash inbuild()at replace.cs:83 on the R8 zero-value[256]bytenull-backing NRE (the same class as TestFinderCreation/Next, owned by the Finder chip). The test flips only when R8 lands.reflect.Kind()/Elem()of adapter TYPES still report the adapter class — a reflection-bridge follow-up that belongs with R5’s DeepEqual work. -
R11: FIXED — no disclosure needed (same branch). Decision path: the map entry anticipated an acceptable-difference disclosure, but root-causing showed identity was ALREADY preserved — converted
Map’s fast path returnss, sharing the backingbyte[]through the@stringstruct copy — and only the COMPARISON was blind:unsafe.StringDatamaterializes a freshPinnedBufferview per call, andж.Equals’s array-index arm compared view instances by reference. Fix: the array-index arm (andGetHashCode) canonicalizes aPinnedBufferto its pinned storage object (PinnedTarget) before the identity comparison — strictly widens equality to same-storage-same-index pairs; Go pointer semantics otherwise unchanged (distinct-but-equal arrays still unequal, value comparison never introduced). TestMap flips to PASS. Guard:StringDataIdentity(header-copy true / repeated-call true / runtime-copy false / content-equal true); discrimination: reverted-fix run printsfalse false false true. - After-differential (same branch, full run to completion under
-test-timeout 5m): 59 pass / 4 fail / 9 infrastructure-error (72 run; baseline same-day: 57 / 5 / 10 — the earlier “6 fail” read double-counted the host’s bare trailingFAILline). Delta: TestMap fail→pass (R11), TestClone infrastructure-error→pass — its baseline crash was R9’s PrintPointer defect TRIGGERED BY R11’s wrong comparison: the empty-string legunsafe.StringData(clone) != unsafe.StringData(emptyString)compared wrongly-unequal (fresh-view reference equality), enteringt.Errorf("Clone(%#v)…", unsafe.StringData(input)), whose pointer print walksж.ToString → PrintPointer → PinnedBuffer[0]over the EMPTY backing → IndexOutOfRange. Post-R11 the comparison is correctly equal, the Errorf never fires, and the non-empty leg’s must-NOT-share assert still holds (distinct arrays stay unequal through the canonicalization). R9 stays open as a latent golib defect (PrintPointer on an empty-backing ж) — the strings suite merely no longer triggers it. (SUPERSEDED — R9 was fixed the same day in theblissful-poitrasbatch, which merged after this one; see § R6–R9 FIXED below.) Honest remainder: 4 fail = the AllocsPerRun divergence classes below (proposal pending); 9 infra = R5 (TestSplit/TestSplitAfter), R6 (TestRepeatCatchesOverflow), R7 (TestCaseConsistency), R8 (TestFinderCreation/TestFinderNext/ TestPickAlgorithm/TestReplacer/TestWriteStringError) — all owned rows.
AllocsPerRun divergence — IMPLEMENTED (2026-07-18, disclosed-divergence manifest)
Ruling: build the manifest. The proposal below is now the shipped mechanism. A hand-owned,
repo-committed go2cs_test_disclosures.json beside each affected converted package pins
{name, class, signature, reason} per divergent test; the -test-action compare oracle
(matchTerminalStatuses, testConversion.go) reclassifies a Go=pass/C#=fail row as
disclosed-divergent only when the exact name is listed AND the captured C# failure output
contains the pinned signature substring — any other failure shape (different signature, other
status pair, C#=infrastructure-error) stays a strict mismatch, and a package with no manifest is
unaffected (sort, utf8 compare strictly). The validation line gained the count:
- bytes (2026-07-18):
Validated 81 tests against go test (0 skipped identically on both sides, 7 disclosed-divergent (alloc-profile), 123 disclosed-unsupported declarations excluded).— TestEqual, TestGrow, TestIndex, TestIndexRune, TestLastIndex, TestNewBufferShallow, TestWriteAppend (allalloc-profile, want-zero). - strings (2026-07-18):
Validated 68 tests against go test (0 skipped identically on both sides, 4 disclosed-divergent (alloc-count-semantics, alloc-profile), 118 disclosed-unsupported declarations excluded).— TestBuilderAllocs / TestBuilderGrow / TestBuilderGrowSizeclasses (alloc-count-semantics), TestIndexRune (alloc-profile).
Both packages policy-committed (converted *_test.cs + host + tests-csproj + IP-4 production
exclusion + the disclosure manifest). Guards: converter TestDisclosedDivergenceOracle +
TestDisclosureManifestLoading. Reference: the AllocsPerRun entry in
docs/ConversionStrategies-Reference.md. The original analysis follows.
AllocsPerRun divergence analysis (Builder trio + TestIndexRune) — original proposal
The four remaining alloc-asserting failures are diagnosed to two distinct divergence classes, and NONE is legitimately fixable in golib without faking measurements:
| Test | Asserts | Measured (bytes/run) | Class |
|---|---|---|---|
| TestBuilderAllocs | exactly 1 malloc | 648 | count-shape (+ profile) |
| TestBuilderGrow, growLen>0 legs | exactly 1 malloc | (with 0-leg: 520+) | count-shape (+ profile) |
| TestBuilderGrow, growLen=0 leg | exactly 0 mallocs | 520 | allocation profile |
| TestBuilderGrowSizeclasses | allocs ≤ 1 | 712 | count-shape (+ profile) |
| TestIndexRune (alloc leg) | exactly 0 mallocs | 32 | allocation profile |
- Count-shape (
want 1,want ≤ 1): the shim is deliberately byte-derived (see the AllocsPerRun entry indocs/ConversionStrategies-Reference.md) — a nonzero count-assert can never agree and diverges loudly BY DESIGN. - Allocation profile (
want 0): the zero-shape maps exactly through the shim, but the managed MODEL genuinely allocates where Go’s compiler doesn’t — the emitted test lambda heap-boxes the addressedvar b Builderper run (ref var b = ref heap(...); Go’s escape analysis stack-allocates, which is the very thing issue-23382’s test verifies), convertedBuilder.String()copies throughunsafe.Stringwhere Go’s is zero-copy, andIndexRune’sIndex(s, string(r))materializes abyte[]where Go uses a 4-byte stack buffer (runtime.intstring). A malloc-COUNTING shim would fail these identically — the divergence is the CLR allocation model, not the measurement unit. TestIndexRune’s index-semantics legs all pass; only the alloc leg fails. - TestIndexRune post-CoverMode note: the shim’s
CoverMode() == ""is Go’s exact coverage-off value; the test’s gateallocs != 0 && CoverMode() == ""behaves exactly as an uncoveredgo testrun. The failure is the 32-byte profile divergence above, not a CoverMode artifact.
Proposed mechanism — test-level disclosed-divergence manifest (extends the existing
“disclosed-unsupported” vocabulary from declaration level to test level): a hand-owned,
repo-committed per-package manifest beside the converted package (reviewed like any source, NOT
regenerated) that the -test-action compare oracle consumes. Each entry pins {test name,
divergence class, expected C# failure signature} — e.g. TestBuilderGrow /
alloc-count-semantics / "got %d allocs during Write" — and the oracle reports a matching
divergence as disclosed-divergent (alloc profile) in the validation line (alongside the existing
“N disclosed-unsupported declarations excluded”) instead of a failure. The signature pin is the
integrity guard: a disclosed test that fails with any OTHER message (e.g. TestIndexRune’s index
legs regressing via Fatalf) still fails the differential — the disclosure covers exactly the
documented divergence, never the whole test. Alternatives considered and rejected: demoting
AllocsPerRun from supportedTestCapabilities (loses sort’s TestSearchWrappersDontAlloc and every
want-zero guard that legitimately passes); static assertion-shape analysis in the converter
(fragile, and cannot see the profile class at all). No implementation in this chip — the
mechanism, its manifest shape, and whether strings validates with disclosed rows are a
coordinator/user ruling.
R6–R9 FIXED (2026-07-18, worktree branch claude/blissful-poitras-5b23d5 — coordinator gates)
Four mechanical golib/hand-owned runtime fixes, base master f999c8f78. No converter (*.go)
change — CNR is byte-identical apart from the four new guard projects. The go2cs-gen analyzer DID
change (R8), so the change carried the full behavioral suite (411/411 four phases) + the corpus
gate (302-package go-src-converted clean compile). Sort re-validates 63/63 and utf8 14/14
through the two-arg -tests command, both dirs left git-clean.
- R6 FIXED —
internal/bytealg.MakeNoZero(bytealg_impl.cs) and golib’sslice<T>make-path ctor (slice(nint length, nint capacity, nint low)) validate first and throw recoverableRuntimeErrorPanic.MakeSliceLenOutOfRange()/MakeSliceCapOutOfRange()(Go’s exactruntime error: makeslice: len/cap out of rangetext, probed vsgo run; recovered value was aruntime.errorString), usingArray.MaxLengthas .NET’smaxAlloc. The .NETArgumentOutOfRange/OverflowExceptionthese replaced could not be caught byrecover().TestRepeatCatchesOverflowflips infra-error → pass in BOTH strings and bytes. Guard:MakeSlicePanicRangebehavioral test (discriminating — revertingslice.cscrashes it with the .NET exception). - R7 FIXED — golib
ToUTF8Bytes(the single seam all rune-span → UTF-8 encodings route through) substitutesRune.ReplacementChar(U+FFFD) viaRune.TryCreatefor a surrogate or out-of-range rune instead of throwing on theint→System.Text.Runeconversion. stringsTestCaseConsistencyflips infra-error → pass. Guard:InvalidRuneString(discriminating — revertedbuiltin.csthrowsArgumentOutOfRangeException). - R8 FIXED — root cause was one level deeper than the sketch. The generated PARAMETERIZED
struct constructor (
StructTypeTemplate.GenerateConstructor) assigned every member from its argument, and an OMITTED fixed-array field arrives asdefault!(null backing), overwriting the= new(N)field initializer — sostringFinder{pattern:…, goodSuffixSkip:…}leftbadCharSkip [256]intunusable. Fix: the generated ctor guards ago.array<…>member withif (m.Source is not null) this.m = m;(IsFixedArrayMember— prefix match, reference members likeж<array<T>>excluded, which was necessary for runtime’sinlineUnwinderto compile). Plus golibarray<T>reads are now null-safe (Backing => m_array ?? []) so a baredefault(array<T>)enumerates/indexes/prints as empty and panics Go-style rather than NRE-ing. stringsTestFinderNextflips infra-error → pass.TestFinderCreationADVANCES: R8 cleared itsbad-table[256]intwalk (search_test.go:76-84 — the exactsearch.cs:56NRE cited), so the error moved from R8’s site (array.cs/search.cs:56) to R5’s site (deepequal.cs:74→unsafe.cs:261) at itsreflect.DeepEqual(good, tc.suf)(search_test.go:86) — an R8-was-masking-R5 layer; TestFinderCreation is now an R5 test, owned by the DeepEqual chip. Guards:ZeroValueArrayFieldbehavioral (literal-omission ranged/indexed vs Go, explicit-arg control; discriminating — reverted gen panicsindex out of range [3] with length 0). - R9 FIXED —
PrintPointerno longer dereferences an out-of-range array/slice-element reference (checksIndexIsValidand prints the backing store’s identity instead), and hand-ownedunsafe.StringData("")returns nil (Go-faithful, probed — distinct empty strings’ data pointers compare equal, which TestClone asserts; bothunsafe.cscopies edited identically). stringsTestCloneflips infra-error → pass. Guards:UnsafePointerPrintbehavioral (StringData nil-identity + in-range print shape) +GolibTests.PointerPrintTests(new golib UNIT project under/tests/library/, for the out-of-range print shape that has no Go-parity spelling; discriminating — reverted, 2/3 fail).
Differential AFTER (master f999c8f78 + R6–R9, this branch):
- strings: 63 agree / 6 disagree / 3 infra / 0 skip (72 included). My targets:
TestRepeatCatchesOverflowpass,TestCaseConsistencypass,TestFinderNextpass,TestClonepass;TestFinderCreationinfra (R8-cleared, now R5-blocked). Honest remainder (other chips):TestBuilderAllocs/Grow/GrowSizeclasses(alloc-class),TestIndexRune,TestMap(R11),TestPickAlgorithm(R10), infraTestSplit/TestSplitAfter(R5). - bytes: 75 agree / 10 disagree / 3 infra (88 included). My target
TestRepeatCatchesOverflowpass. Honest remainder (other chips):TestClone/Trim/TrimFunc(R13),TestEqual/Grow/Index/IndexRune/LastIndex/NewBufferShallow/WriteAppend(R5/alloc), infraTestNil/Split/SplitAfter(R5/R13).
Both bytes/strings dirs restored to git-clean after measurement (non-validated packages — no policy commit). R5, R10, R11, R13, and the Builder alloc-class rows remain, owned by their chips.
Cross-cutting lessons
- Capability-excluded tests still compile — exclusion gates the run registry, not emission; a broken emission inside an AllocsPerRun-excluded test blocked all of bytes.
- The sweep’s environment-dependent error sets were both shadows of B1/B2b, not an abi problem.
- Minor: the tests-csproj template’s later
<OutDir>override defeats its ownBaseOutputPath=bin\tests\(exe lands in bin/Debug/net9.0/) — cosmetic; align when touching the template.