Blocker map: strings & bytes test suites (Phase-4 packages #3–4 work order)

Read-only scout, 2026-07-17, against master 1fb3eae1c (post /vN fix; testing.AllocsPerRun was 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 @string performance cliff — strings’ TestCompareStrings runs ~109 s in the C# runtime on the unsafeString@string copy 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’s ISlice arm) — recorded in docs/ConversionStrategies-Reference.md; (4) cosmetic: the tests-csproj template’s <OutDir> override defeats its own BaseOutputPath=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 has null backing; enumerator NREs (array.cs:201 via search.cs:56) golib array lazy/nil-safe backing 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

Sequencing recommendation

  1. 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.
  2. B4/B5 is the structural decision (converter-side two-anchor split — avoids the go2cs-gen full-suite+corpus gate).
  3. B7a/B7b/B8 are self-contained converter emission fixes; each deserves a behavioral guard test.
  4. 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)

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).

🏁 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.

Status updates (2026-07-18 — R10/R11 + the Builder-allocs/IndexRune analysis)

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:

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

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.

Differential AFTER (master f999c8f78 + R6–R9, this branch):

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