go2cs Roadmap

Companion to /CLAUDE.md. The path from “loop stalled” to a converted Go standard library that compiles, passes its upstream tests, and has working C# implementations for its assembly-backed declarations. Sequenced green the loop first, then compile, validate, and complete the full conversion.

Status (2026-06-27): Phases 0–2 done — baseline is green. Phase 3 is driving the full conversion to compile. Phase 4 will convert and run Go’s own package tests; Phase 5 will use those tests to prove the C# implementations that replace assembly/cgo stubs.

Phase 0 — Documentation ✅ done

Orientation docs so any task starts informed: /CLAUDE.md, Architecture.md, Baseline-vs-FullConversion.md, this file; plus a refresh of README.md.

Phase 1 — Restore the separation (repo surgery) ✅ done

Goal: src/core = compiling baseline; src/go-src-converted/ = full WIP; golib shared.

Phase 2 — Green the loop ✅ done (via stub restore)

Outcome: rather than green the full fmt closure bottom-up (57 packages incl runtime/syscall/os), we restored the old hand-finished stub (3426298eb) into src/core — it compiles cleanly against today’s golib, so the test loop went green immediately. Plus several converter fixes (below). The 57-package-closure analysis below is retained as context for Phase 3 (the full conversion).

How the baseline-restore path worked

The behavioral tests reference golib + the go2cs-gen analyzer + a few stdlib projects (fmt mostly, plus time/unsafe/strings/sort/math/rand/io). The stub fmt is a minimal proxy with a tiny closure (errors, io, strings, sync, math, …) — it avoids the runtime substrate, which is why restoring it is the fast path to green. Restored 14 packages; excluded the stub testing (drifted, unused by tests).

The full-fmt-closure path (NOT taken for the baseline; relevant to Phase 3)

The behavioral tests reference golib (all 59) + the go2cs-gen analyzer (all) + a few stdlib projects: fmt (55 tests), time (5), unsafe (3), strings (1), sort (1), math/rand (1), io (1).

But these pull a transitive dependency closure. fmt alone references: errors, internal/fmtsort, io, math, os, reflect, slices, strconv, sync, unicode/utf8 (and those pull more). So “green the loop” means green fmt’s closure, bottom-up.

  1. Compute closure + order. Reuse stdLibConverter.go’s topological sortedQueue, restricted to the baseline roots, to get a leaf-first build order (unsafe, internal/abi, unicode/utf8, math/bits, … up to fmt).
  2. Green packages bottom-up. For each package in order: dotnet build against current golib; fix the converter (preferred) — or, for genuinely out-of-band pieces like builtin, the runtime — until it compiles. Crib from the go2cs-stub-ref worktree when a hand-finished prior approach helps.
  3. Lock it in with a regression test. Each greened package gets a behavioral/compile test so it stays green (mirrors the “raw code vs target file” comparison mode noted in src/go2cs/ToDo.md).
  4. Exit criterion. Baseline src/core builds clean and all 59 src/Tests/Behavioral projects pass. The converter-improvement loop is restored.

Phase 2 findings (measured 2026-06-25)

Converter fixes landed (2026-06-25)

Each verified by rebuild + reconvert + compile:

Phase 3 — Drive the full conversion to compile

  1. Build-error roadmap. Convert + dotnet build src/go-src-converted/, capture build.log; bucket compile errors by frequency and by Go feature. This is the prioritized work queue (the README already frames this log as the road map).
  2. Fix by error class, bottom-up the full DAG. Highest-frequency converter defects first (each fix clears many packages). Re-convert, re-bucket, repeat. Measure by packages-compiling and error-count, never by “conversion succeeded.”
  3. Promote, don’t fork. When a full package compiles cleanly and matches behavior, promote it toward the baseline; golib’s hand-written core stays shared and never auto-overwritten. Track promotions here. SUPERSEDED (2026-07-01): promotion go-src-converted → core is deferred to post-Go-test (Phase 4) and may not be needed at all. Compiling ≠ operating; a clean compile is NOT a promotion trigger. core stays the bootstrap stub. The hand-owned [module: GoManualConversion] / *_impl.cs files live in core and are copied back into go-src-converted (the real final state). See Baseline-vs-FullConversion.md The corrected end-state and its contract rules 4–5. golib’s hand-written core stays shared and never auto-overwritten.

Phase 3 iteration 1 — converter fixes landed (2026-06-25)

Measurement workflow used (no wholesale commit of go-src-converted; it stays regenerable): reconvert the stdlib to a temp dir (go2cs -stdlib -comments -go2cspath <tmp> — always -comments so the Go authors’ BSD license header survives; output lands in <tmp>/core/<pkg>), overlay the fresh .cs onto src/go-src-converted/<pkg> (keeping the relocated csprojs, or regenerating them and rewriting $(go2csPath)core\$(go2csPath)go-src-converted\ except core\golib), then dotnet build src/go-src-converted.sln and bucket. Single packages build with -p:go2csPath=<repo>\src\ so the $(go2csPath) golib ref resolves outside the solution.

Four converter defects fixed (each verified by reconvert + compile of an affected package; behavioral suite stays green: 216/216):

Defect Symptom (top error class) Fix
Variadic of a type parameter (func Or[T any](v ...T)) emitted a namespace-level using ꓸꓸꓸT = Span<T> alias — T out of scope CS0246 'T' not found visitFuncDecl.go: when the variadic element is a *types.TypeParam, emit params Span<T> inline (C# 13 params-collections) instead of the alias.
Go built-in comparable constraint emitted as bare comparable (golib’s type is generic comparable<T>) CS0305 requires 1 type argument main.go getGenericDefinition: special-case comparablecomparable<T>.
Bodyless (assembly/cgo) funcs emitted as accessibility-modified partial methods with no implementing half CS8795 (49) — biggest cluster visitFuncDecl.go: emit a non-partial throwing stub (=> throw new NotImplementedException(...)) until an asm/cgo backend exists.
Filename build-constraint over-exclusion: isFileNameCompatible treated any unknown _word suffix (e.g. bits_tables.go, bits_errors.go) as a failing platform tag, silently dropping the file → missing symbols CS0103 (pop8tab/ntz8tab/divideError…) directiveOperations.go: only a trailing recognized _GOOS, _GOARCH, or _GOOS_GOARCH constrains the build (full GOOS/GOARCH name tables added); descriptive suffixes impose no constraint.

The filename fix is the highest-impact: a full reconvert went from 1472 → 1660 emitted .cs files (~188 previously-dropped stdlib source files now converted). That raises the raw error count (144 → 224) because newly-included files surface their own latent defects — so track packages-compiling, not error count, this phase.

Phase 3 iteration 2 — internal/cpu address-of-field (2026-06-25)

internal/cpu/cpu_x86.cs owned ~140 of the syntax errors, all from &cpu.X86.HasADX (address of a field of the anonymous-struct package global X86). Two stacked bugs, both fixed; behavioral suite stays green 216/216:

Bug Symptom Fix
The anonymous struct type of X86 is lifted to X86ᴛ1 while visiting cpu.go, but liftedTypeMap is per-file (each file gets its own concurrent Visitor), so cpu_x86.go couldn’t resolve it — getExprTypeName fell back to the raw struct text, mangled to cpu.CacheLinePad} syntax errors ()/; expected …) New package-level shared registry (packageDynamicTypeNames, signature→C# name) populated by visitStructType for package-level lifts; convUnaryExpr emits a marker for unresolved anonymous structs, resolved after the file-visit barrier (dynamicTypeOperations.go, main.go). Race-free: resolution runs post-Wait().
With the name fixed, &X86.HasADX emitted ᏑX86.of(…) (identifier form), assuming a heap-boxed pointer companion — but a package-global value var has none CS0103 ᏑX86 does not exist convUnaryExpr: choose the address-of form by escape state — isHeapBoxedExpr (mirrors the existing escape check) → Ꮡvar.of(…) for escaping locals, else the constructor form Ꮡ(value).of(…) (consistent with the existing whole-value &global path).

Result: internal/cpu went ~140 → 8 errors. Caveat: Ꮡ(value) heap-allocates a copy (golib Ꮡ<T>(in T)), so &global.field currently points into a copy — a pre-existing whole-category limitation (&global already did this on line 124); the proper fix is boxed companions for package-global vars whose address is taken (future work).

Phase 3 iteration 3 — internal/cpu compiles clean (2026-06-26)

The remaining 8 internal/cpu errors are fixed — internal/cpu is the first full-conversion stdlib package to compile clean (was the ~140-error blocker). Three general fixes, all behavioral-green (228/228); re-transpiling all 61 behavioral projects left every golden byte-identical (no converter-output regression), and the whole solution rebuilds clean against the changed golib:

Defect Symptom Fix
Large untyped constant typed by value-range as (nint)…L even in an unsigned context (cpuid(0x80000000, 0)) CS1503 nint → uint (×6) convBasicLit: in the > int32 branch, if the literal’s contextual type is unsigned (isUnsignedType via info.Types), emit an unsigned C# literal (2147483648U / …UL).
Slicing an @string returned slice<byte>, so field[:4] != "cpu." was slice<byte> != string CS0019 golib: @string this[Range] now returns @string (Go string slicing yields a string). Runtime-only — no .cs change.
Empty-string literal in a tuple assignment emitted ""u8 (a ReadOnlySpan<byte> ref struct) — illegal as a ValueTuple element CS9244 visitAssignStmt: suppress the u8 form for string literals in a multi-value (tuple) RHS (field, env = env, "").

Guarded by the StringSliceAndUnsignedConst behavioral test.

Phase 3 iteration 4 — address-of-global correctness (2026-06-26)

Ꮡ(value) heap-allocates a copy, so &global / &global.field pointed into a copy — mutations never reached the global (e.g. internal/cpu.doinit set feature flags on a throwaway copy of X86). Fixed by backing address-taken package-global vars with a heap box, so the pointer references the original:

internal/cpu still compiles clean and now mutates the real X86. Behavioral green; GlobalStructFieldPointers strengthened to assert the global itself is mutated (would print false/0 before the fix).

Known limitation: cross-package &otherPkg.ExportedGlobal isn’t boxed (only globals addressed within their own package are detected).

Phase 3 iteration 5 — anonymous-struct global declarations (2026-06-26)

A package-global var whose type is inferred from an anonymous-struct composite literal (var S = struct{…}{…}) emitted the raw struct{…} text as its C# declaration type (public static struct{A int; B int} S = new Δtype(…);) — invalid C#. The value was lifted to a named type but the declaration wasn’t (the lifting happened inside the composite literal, after the declaration type name was resolved). Fix in visitValueSpec: for a package global with an inferred anonymous-struct type, lift the struct with the var name before resolving the declaration type (mirroring the explicit-type path), so both the declaration and the value share one lifted name (Sᴛ1). This also unblocks boxing such globals, so addressed anonymous-struct globals (&S.field) now work too. Behavioral green; zero existing goldens changed (no behavioral test had an anonymous-struct global). Guarded by an extension to the AnonymousStructs test (a package-global anonymous-struct var, read and mutated through a field pointer).

Phase 3 iteration 6 — TypeGenerator CS0051 (unexported embedded marker) (2026-06-26)

A public struct embedding an unexported marker type as a blank field (_ noCopy, the sync/atomic.Bool pattern) made the TypeGenerator (Roslyn) emit public Bool(noCopy _) — a public constructor whose parameter type noCopy is internal → CS0051. Root cause: GetScope("_") returns "public" (the firstChar == '_' rule), so the blank embedded field was classified as a public member and drove the public ctor. Fix in StructTypeTemplate.PublicStructMembers: exclude blank/underscore-prefixed fields (never exported in Go) from the public constructor. All CS0051 in sync/atomic cleared; behavioral green (232/232 + the new test). Guarded by UnexportedEmbeddedMarker.

Phase 3 iteration 7 — asm-function companion source generator (2026-06-26)

Resolves the iteration-6 follow-up. Bodyless (asm/cgo) Go functions are once again emitted by the converter as partial declarations (reverting iteration-1’s non-partial throwing stub). A new PartialStubGenerator (go2cs-gen) emits a throwing partial implementation for every bodyless partial method that has no other implementing part in the compilation (IMethodSymbol.PartialImplementationPart is null). So:

sync/atomic now compiles clean — the second full-conversion stdlib package to go green (after internal/cpu). sig compiles too. Behavioral suite stays green (the generator is a no-op for the tests — none contain asm functions; zero behavioral .cs changed). Not behaviorally testable (Go rejects a bodyless function without an .s file), so verified via the full-conversion packages compiling.

Phase 3 — promotion gate finding: sync/atomic typed API is broken (2026-06-26)

A behavioral validation test (atomic ops, Go-vs-C# output, referencing go-src-converted/sync/atomic) was written as the gate before promoting sync/atomic to the baseline. It failed, and that is the point — “compiles” ≠ “correct”. The package-level functions (atomic.AddInt32(&n, 3)) work, but the typed atomic types are broken: var i atomic.Int32; i.Store(10); i.Add(5); i.Load() yields 0 in C# instead of 15 (Go).

Root cause: a method like func (x *Int32) Store(v) { StoreInt32(&x.v, v) } converts &x.v (address of a field of the pointer receiver) to Ꮡ(x.v), which boxes a copy (the ж(in T) ctor copies), so the atomic op never touches the real field. Attempting the fix uncovered a deep stack of issues in the receiver-capture mechanism, which is the only way to get a non-copying pointer to a receiver field:

  1. &recv.field must use the captured receiver box (Ꮡx.of(Type.ᏑField)), not Ꮡ(x.field) — and the detection has to run before the struct-field gate (a pointer receiver’s selector type is a pointer).
  2. The capture field name (<Method>ꓸᏑx) collides across overloaded same-named methods on different receiver types (Int32.Add, Int64.Add, …) — needs the receiver type in the name (converter + generator).
  3. The capture field is a static ThreadLocal on the (non-generic) package class, so it cannot hold a generic receiver’s T (atomic.Pointer[T]).
  4. Even fixed, the ThreadLocal is only initialized when the method is called via the ж (pointer) overload; a value-receiver-style call (i.Store(10)) routes through the ref overload and the capture is never initialized (runtime “Receiver target … is not initialized”).

Conclusion: sync/atomic is not promotable — its primary API doesn’t work, and the receiver-field address / capture machinery needs a substantial rework (likely replacing the static-ThreadLocal capture with something that works for value calls and generic receivers). internal/cpu likewise isn’t promotable (asm cpuid is a stub). Promotion stays pull/validation-driven; this gate correctly blocked it.

Receiver-field address / capture rework — staged (2026-06-26)

Making &recv.field reference the real receiver field (the sync/atomic typed-type unlock). Split into two stages:

Phase 3 iteration 8 — re-bucket + 4 converter/generator fixes (2026-06-26)

Fresh full reconvert (305 pkgs, 1659 .cs) + full go-src-converted.sln build, re-bucketed by CS#### frequency. Measurement gotcha caught: the reconvert-overlay must NOT blanket-delete .cs first — ~15 hand-written companion/pseudo-package files (unsafe/unsafe.cs, *_impl.cs, generator companions) are not regenerated by the converter, and deleting them produces a phantom ~120-error unsafe_package CS0246 bucket. Overlay by copying generated .cs over; git checkout -- any deletions.

After clearing the phantom bucket, the real own-error defects bucketed as: reflectlite (24, CS1537 dup global using), container/list (6→25 post-fix) + container/ring (12) sharing one ж<T> defect, unicode (4), internal/types/errors (4), plus runtime/unsafeheader-missing-ref cascades. Four fixes landed (behavioral suite green throughout; zero existing goldens changed by any):

Defect Symptom Fix
Generator dup global usingGetFullyQualifiedUsingStatements (all 5 generators) copied global using alias directives from the source file as file-local using, colliding with the in-scope global one CS1537 (×24 in reflectlite, via PartialStubGenerator asm stubs) go2cs-gen/Common.cs: skip global using directives (already in scope everywhere, generated files included). reflectlite 24→0. Not behaviorally testable (asm-stub-only path); verified via the full conversion. Commit 9c9431b3f.
Mixed-type for-initfor i, e := s.Len(), s.Front() (int + pointer) emitted two ;-separated decls inside the C# for-init clause (invalid: the ; ends the clause); the combined var (a,b) form is blocked by the int special-casing CS1002/CS1003 (container/list + ~20 files) visitAssignStmt.go (+ forInit flag in FormattingContext/visitForStmt): emit a tuple-deconstruction declaration with per-element types — (nint i, var e) = (...). Gated on all-new, non-heap-boxed LHS. Guarded by ForInitMixedTypes. Commit 6d339a3d0.
Variadic-of-pointer paramfunc In(r rune, ...*RangeTable) emitted an invalid using alias ꓸꓸꓸж<RangeTable> = Span<…> (alias identifier can’t contain </>) CS1002/CS1022 (unicode) visitFuncDecl.go: emit params Span<T> inline when the element type is generic/pointer (Contains "<"), extending iteration-1’s type-parameter special case.
Empty/spread variadic-of-pointer callIn(r) (no trailing args) panicked the converter (Args[i] indexed past end); f(slice...) emitted Ꮡslice (element address-of applied to the spread slice) converter panic → dropped file; CS0103 convCallExpr.go: guard the element-pointer arg treatment with paramHasArg (empty call) and !(hasSpreadOperator && last param) (spread).

The last two ship together with the VariadicPointerParam behavioral test (args/empty/single/spread calls).

Phase 3 iteration 9 — blank-identifier collision (CS0102) (2026-06-26)

A package declaring blank _ constants (skipping iota values) and a blank func _() (the stringer compile-time-assertion idiom — e.g. internal/types/errors) emitted multiple internal static readonly … Δ_ fields that collided: CS0102 “already contains a definition for ‘Δ_’”. Root cause: performNameCollisionAnalysis recorded _ in both the named-element set (the blank consts) and the method-name set (func _()), flagged it as a const↔method collision, and getSanitizedIdentifier Δ-prefixed every _ to the same Δ_ — defeating the value-spec visitor’s per-blank unique naming (_ᴛNʗ). Fix (nameCollisionAnalysisOperations.go): exclude the blank identifier from collision analysis (it is a discard, never referenced, and already gets unique names). internal/types/errors CS0102 4→0 (remaining: a strconv project-ref, separate). Guarded by BlankIdentifierCollision; behavioral suite green, zero existing goldens changed. Found-but-deferred: a bare discard _ = expr inside a func _() emits _ = … which binds to the method group → CS1656 (“cannot assign to ‘_’”) — a separate edge case (real stringer asserts use _ = x[C-C]); not hit by internal/types/errors’ actual body.

Phase 3 iteration 10 — accurate re-bucket + pointer-copy fix (2026-06-26)

Measurement-methodology finding (important): the committed go-src-converted csprojs are stale and lack inter-package ProjectReferences that the current converter emits correctly. Overlaying fresh .cs onto those stale csprojs inflated the bucket with phantom CS0246 “package not found”. Regenerating the csprojs from the fresh conversion (with the documented core\go-src-converted\ rewrite, golib excepted) dropped the total 95→79 and CS0246 23→5. So the measurement loop must regenerate csprojs, not keep the committed ones — there is no converter csproj-emission defect. Reusable overlay script: scratchpad/overlay.sh.

True own-defect leaders after the rewrite: container/list (25) + ring (12) = the ж<T> model; internal/chacha8rand (7, mostly the same ж<T> pattern — State.Init64/Refill on a value needing the box); math/bits (4, unsigned- arithmetic/shift-count coercions); a handful of 1–2-error leaves.

The ж<T> model split into two sub-problems (converter derefs a pointer param/receiver to a value alias ref var x = ref Ꮡx.Value, losing pointer identity when Go uses it as a pointer):

Other queued leaf defects (from the 79-error bucket): plugin CS0553 (ImplicitConvGenerator emits illegal object↔T conversions), runtime/internal/math CS0133/0266 (const MaxUintptr ulong→nuint not const), unicode CS0051/52 (exported field of an unexported type), internal/runtime/atomic CS1526 (new without ()), and the deferred CS1656 (bare _ = expr discard inside a func _()).

Phase 3 iteration 11 — ж<T> sub-problem A: receiver as a pointer value (2026-06-26)

A method that uses its pointer receiver as a bare valuefunc (r *ring) initSelf() { r.next = r } (assign the receiver to a pointer field) — emitted n.next = n: a value-ref receiver (this ref ring r) has no box, so a ring value was assigned to a ж<ring> field (didn’t compile; would point into a copy). Fix, two parts:

Validated by ReceiverPointerValue (self-link, cross-link, pointer-walk; mutating through the self-link proves the field points at the real receiver — 42/42/3/2). Suite green 264/264, zero existing goldens changed. Payoff: container/ring 12→8 errors, container/list 25→~10 (and the remainder are now different, narrower defects).

Remaining ж<T> sub-problems (the still-failing ring/list errors, each a distinct mechanism — follow-up work):

Phase 3 iteration 12 — container/ring + list COMPILE AND RUN; ж<T> identity equality (2026-06-26)

Finished the ж<T> self-referential-pointer model — container/ring and container/list both compile clean, and (crucially) run correctly. Four converter pieces + one runtime fix; zero existing behavioral goldens changed:

Guarded by the RingPointerMethods behavioral test (a full mini-ring exercising A/A-variant/B/C/D/E — build, walk, Len, Move±, Prev — output matches Go). The golib change touches all pointer equality; validated by the full suite. Both container packages now compile and run — closing out the general pointer-as-value problems.

Phase 3 iteration 13 — leaf-defect sweep: 5 fixes, 3 leaves greened (2026-06-26)

Fresh full reconvert + regenerated-csproj overlay + go-src-converted.sln build re-bucketed the leaves: the ж<T> work had cleared the big clusters, leaving 39 errors across 17 of 304 projects (down from last session’s 79). Five fixes landed (each its own commit; all behavioral-green, zero existing-golden churn except the one intended re-baseline noted):

Fix Defect Greens Commit
min/max built-ins (golib builtin.cs) Go 1.21 min/max emitted verbatim but golib had no such methods → CS0103. Added generic min/max constrained to IComparable<T> (numeric primitives + @string). No converter change. crypto/subtle (+ unblocks ~dozens that use the built-ins) daddd953d
unsafe.Pointer keyword sanitization (convIdent.go) The name.Value deref form for an unsafe.Pointer ident used the raw Go name, so a param named new (C# keyword) emitted new.Value → CS1526. Now sanitized to @new.Value. (internal/runtime/atomic syntax; pkg has deeper latent ж issues) 175e0dfd3
unsigned unary-minus + shift precedence + shift-assign count (convUnaryExpr/convBinaryExpr/visitAssignStmt) x & -x → CS0023 (now (T)0 - x); Go x>>4 + x / 1<<15 - 1 re-associated in C# (shift binds looser) → now shift exprs parenthesized; y <<= s cast count to RHS type → CS0019 (now (int)). math/bits 4→1; corrected a latent 1<<15-11<<14 miscompile in the StdLibInternalAbi golden (re-baselined) 9089b9c4b
builtin-shadowing local rename (variableAnalysisOperations.go) len := len(buf) — a local named like a called built-in shadows the using static method → CS0149/CS0841. Renames the local (lenΔ1) when that built-in is actually called in the function; built-in call stays len. hash/maphash e87705650
comma-ok map access (convIndexExpr/convExpr/visitAssignStmt) v, ok := m[k] (+ blank, if-init, reassign forms) wasn’t detected as a tuple result → indexed twice, value assigned to ok (CS0029). Now routed through golib’s two-value indexer m[key, ꟷ] via a new IndexExprContext.isTupleResult. High-value correctness fix (every comma-ok map read was wrong; no behavioral test had covered it). internal/coverage/rtcov 92d832204

Guarded by new behavioral tests: MinMaxBuiltin, UnsafePointerKeywordParam, ShiftPrecedenceUnsigned, BuiltinShadowLocal, MapCommaOk. Leaf packages greened this iteration: crypto/subtle, hash/maphash, internal/coverage/rtcov. Found-but-deferred: reading a nil map NREs instead of yielding zero/false (golib nil-map representation gap — a chip was filed). math/bits’ last error is a local untyped const (1<<32) typed UntypedInt/BigInteger in uint64 arithmetic — context-typing untyped local consts is a larger follow-up.

Phase 3 — earlier note: the ж<T> self-referential-pointer-struct confusion in container/ring & container/list —

r.next = r (assign receiver to a pointer field → needs the box Ꮡr) and r = r.prev (reassign the Go pointer variable, but the C# ref var r = ref Ꮡr.Value deref aliases the value). The deeper one (CS0019/CS1061/ CS0029/CS1929 cluster), blocking both container packages.

Phase 4 — Convert and run Go package tests

Goal: validate each compiling converted package against the same _test.go suite used by Go before assembly-backed implementations are attempted at scale. The detailed and authoritative design is TestingInfrastructureRequirements.md; this section tracks the delivery sequence and exit gates.

The behavioral suite proves individual converter/runtime constructs using hand-written fixtures. Phase 4 adds the complementary whole-package gate: load Go’s internal and external test-package variants, convert the eligible tests, compile them with the converted package sources, and compare their results with a clean go test -json -count=1 run for the same source, build tags, GOOS, and GOARCH.

Phase 4A — colocated test projects and serial test core

Phase 4A exit: all harness fixtures pass and at least one already-compiling leaf standard-library package is validated end to end.

Phase 4B — subtests, parallelism, and differential results

Phase 4B exit: representative leaf packages using external tests, subtests/parallelism, and file fixtures validate reproducibly on the matched target platform.

Phase 4C — package coverage and CI scaling

Phase 4 exit: one documented command converts, builds, and runs a selected package’s tests; the harness semantics are guarded end to end; every compiling package attempted is honestly classified; and at least three representative leaf packages are validated (including external-package and subtest/parallel cases). Only validated satisfies the default Phase 5 behavior gate.

Phase 5 — Implement assembly-backed declarations in C#

Goal: replace the PartialStubGenerator’s throwing implementations for Go declarations backed by assembler, cgo, runtime/compiler intrinsics, or platform services with maintainable C# implementations, proved package by package by Phase 4.

Phase 5A — inventory and classify the external surface

Phase 5B — declaration/implementation companion pattern

Phase 5C — validate, promote, and close the stub inventory

For each implemented package:

  1. reconvert production and tests from clean inputs;
  2. confirm the production and test projects compile without a generated stub for the implemented members;
  3. run the matched Go test baseline and converted C# suite;
  4. require all eligible relevant tests to pass, with no silent exclusions;
  5. run focused stress/edge tests for concurrency, atomics, unsafe pointers, cryptography, or platform calls where ordinary unit tests are insufficient; and
  6. promote/update the baseline only after the evidence is recorded in the package ledger.

Compilation alone is not completion. If a package’s tests cannot yet run, it stays infrastructure-blocked or conversion-blocked; any platform-specific waiver must be explicit and reviewed.

Phase 5 exit: for every supported target, the external-declaration inventory has no unexplained throwing stubs; each implemented member has a real companion or a documented target exclusion; all applicable packages are Phase 4 validated; and the full converted standard-library test run passes for the supported package/target matrix.

Progress tracking

Metric Source Status
Baseline + tests build clean dotnet build src/go2cs.sln ✅ 79 / 79
Behavioral suite passing BehavioralTests (MSTest) ✅ 216 tests
Full packages compiling src/go-src-converted.sln ◻ Phase 3 — iters 1–2: 5 converter fixes; internal/cpu ~140→8 errors
Full-conversion error count build-error buckets ◻ Phase 3 — next: address-of-global correctness; re-bucket after reconvert
Converted package tests Per-package Phase 4 manifests/results ◻ Phase 4 planned — requirements complete
Assembly-backed implementations Phase 5 external-declaration ledger ◻ Phase 5 planned — gated by Phase 4 validation

Reference: open converter items (src/go2cs/ToDo.md)

visitMapType completion; remaining dynamic-struct implicit-cast checks across AssignStmt/CompositeLit/ IndexExpr/BinaryExpr/UnaryExpr/SelectorExpr/TypeSwitchStmt/ValueSpec; optional recursive dependent-package conversion; map/channel GoType generator support (IMap/IChannel); comment conversion; cgo + Go-assembler (.s) targets.