DESIGN — the .NET build-warning census, and what to suppress in the generated .csproj
Measured 2026-08-07 (r46a-warnaudit) against
src/go2cs-stdlib.slnxat273f126340, 304 projects, isolated builds (MSBUILDDISABLENODEREUSE=1,-p:UseSharedCompilation=false). The deliverable is the table in §4 plus the judgment in §5: which codes are structural to the emission model and will never go away, which are a converter defect wearing a warning’s clothes, and which are a build property problem thatNoWarnis the wrong instrument for.STATUS: the configuration half is IMPLEMENTED (r46b-warnsuppress, 2026-08-08) — §10 records what landed and what it measured. Both converter roots are now IMPLEMENTED too (r48c-warnroots, 2026-08-08) — §11 records what landed, what it measured, and what is retained by design. §5.1/§5.2 are kept as written: they are the diagnosis the fixes were built from, and §11 corrects the one place the diagnosis was incomplete. The golib/
go2cs-genitems (§7) remain open board rows, and the do-not-suppress rulings in §4 stay binding — the point of fixing these two rather than suppressing them was to keep both codes alive as signals.
1. The measured baseline
Four full solution builds, each capturing every warning to a file logger
(-flp:LogFile=…;Verbosity=normal;WarningsOnly) rather than a console filter:
| Run | Configuration | Warnings |
|---|---|---|
| A | -c Debug, as committed |
4,147 |
| B | -c Release, as committed |
4,147 (code-for-code identical to A) |
| B2 | dotnet pack -c Release --no-build |
0 additional — no NU####, no NETSDK#### |
| C | -c Debug -p:NoWarn= (existing suppression cleared) |
15,445 |
| D | -c Debug + the §5 proposal |
6,181 measured / 1,903 with CS8618 retained (§6) |
Debug and Release are identical, so there is one number to manage, not two. The “2,315 on a Debug
build” and “4,148 on the Release pack” figures that opened this arc reconcile as: 4,148 ≈ the true
full-build number (4,147 here, ±1 for a transient restore line), and 2,315 was an incremental
build — MSBuild reports warnings only for projects it actually recompiles, so a partially up-to-date
tree always under-reports. Any warning count quoted for this corpus must come from a clean or
-t:Rebuild build.
199 of the 304 projects emit at least one warning; 105 are already silent. The distribution is
heavily skewed — math/rand (614), runtime (478), go/types (352), net/http (229), net (178)
carry 45 % of the total between them.
Nothing is TreatWarningsAsErrors, and no code-style analyzer runs at the command line
(EnforceCodeStyleInBuild is unset), so IDE#### diagnostics are a Visual Studio-only concern —
they are not in any of these counts.
2. Where the suppression lives today
One line, in one file, copied into every generated project:
src/go2cs/csproj-template.xml:17 <NoWarn>CS0282;CS0660;CS0661;CS8618;CS8981;IDE0060;IDE1006;CA2255</NoWarn>
src/go2cs/test-csproj-template.xml:34 <NoWarn>CS0282;CS0660;CS0661;CS8618;CS8981;IDE0060;IDE1006;CA2255</NoWarn>
(Line numbers as of the audit; both lines moved when §10 landed — csproj-template.xml:25,
test-csproj-template.xml:33.)
It grew by accretion: CS0660;CS0661;CS8981;IDE0060;IDE1006 (3805a0551, 2025-01-16), +CS0282
(6315d6658, 2025-01-27), +CA2255 (1d2344db4, 2026-07-11), +CS8618 (6411ed09f, 2026-07-13).
golib keeps its own, older list — 660;661;1701;1702;IDE1006;CA2255;CS8500;CS8981 — with bare
numeric codes and two entries (1701/1702, assembly-binding redirects) that cannot fire on
net9.0.
Hand-owned .csproj files carry a copy of the template list and need the same edit by hand:
src/core/unsafe/unsafe.csproj, src/core/internal/godebug/internal.godebug.csproj (hand-owned by
consequence — its only Go file is fully hand-owned, so unmarkedFileCount == 0 makes the driver
continue before writeProjectFile), and src/core/testing/testing.csproj (which carries a shorter
list, CS8981;IDE1006).
Correction (r46b). That list of three is incomplete, and the audit had no way to see it: the hand-owned set is not a property of the file, it is whichever production
.csproja-stdlibreconvert does not re-emit — so the only reliable census is to reconvert into a seeded temp root and diff. Doing that found five:internal/weakandinternal/concurrentare hand-owned by the sameunmarkedFileCount == 0consequence asinternal/godebug(their single Go file is fully hand-owned), and both were silently carrying the old list. Addgolib(which keeps a deliberately different list, §7) and the by-hand set is six. Do not carry this number forward either — it moves whenever a package’s last unmarked file acquires a marker; re-measure it the same way.
3. Re-justification of the existing eight entries
Measured by clearing NoWarn corpus-wide (run C). The existing list suppresses 11,298 warnings —
73 % of everything the compiler has to say about this corpus.
| Entry | Fires | Projects | What it is | Verdict |
|---|---|---|---|---|
CS8981 |
6,166 | 303 | “type name X only contains lower-cased ascii characters” — every Go type name, plus the any/rune/uint8 global-using aliases |
KEEP. Structural and permanent; Go’s naming convention is the whole point. |
CS8618 |
4,421 | 182 | non-nullable field uninitialized — Go zero values. 4,278 of them are in go2cs-gen output, not converter output |
KEEP — and see §6: it cannot be replaced by the Nullable property because generated files carry their own #nullable enable. |
CS0660 |
227 | 81 | type defines ==/!= without overriding Equals — the converter emits Go comparison operators on value types |
KEEP. Structural. |
CS0661 |
226 | 81 | …without overriding GetHashCode |
KEEP. Same emission. |
CS0282 |
206 | 64 | “no defined ordering between fields in multiple declarations of partial struct” — go2cs splits a struct’s fields between its type file and package_info.cs |
KEEP. Structural to the partial-class model. |
CA2255 |
53 | 39 | [ModuleInitializer] “only intended for application code” — go2cs aliases it to GoInitAttribute to model Go init() |
KEEP. The analyzer’s premise (libraries shouldn’t self-initialize) is exactly what Go semantics require. |
IDE0060 |
0 | 0 | unused parameter | KEEP, inert at the CLI. Only fires in Visual Studio’s live analysis; that is where it would be loud (every _-shaped Go parameter). |
IDE1006 |
0 | 0 | naming rule violation | KEEP, inert at the CLI. Same — and the VS noise would be enormous (all Go identifiers). |
Nothing in the list is stale. Two entries in golib’s list are: 1701 and 1702 are .NET Framework
assembly-binding warnings that cannot occur on net9.0, and the bare 660;661 should be written
CS0660;CS0661 for consistency with everything else. CS8500 in golib’s list is justified (see §7).
4. The census — every code, sorted by count
Classification key: S = structural to the emission model (suppress) · F = fixable in the
converter (keep visible, root it) · G = golib/go2cs-gen-local · P = a build property
problem, not a NoWarn problem.
| Code | n | Projects | What the emission actually looks like | Class | Recommended action | Risk of suppressing |
|---|---|---|---|---|---|---|
CS0219 |
1,219 | 136 | rune r = default!; — the named-return prologue. 1,218 of 1,219 are exactly that shape; the one exception (bufio/scan.cs:169) is a folded local const. The function returns explicit tuples, so the declaration is dead |
F | Fix in converter: emit the named-return local only when the body references it. Do not suppress | A dropped assignment to a named return would leave default flowing out of a bare return — CS0219 is the only static signal for that. Suppressing hides the one case that matters |
CS8778 |
620 | 7 | -(nint)4181792142133755926L inside new int64[]{…} (math/rand/rng.cs, 607 of the 620). Untyped Go constants take the default int→nint type instead of the composite literal’s int64 element type |
F | Fix in converter (two roots, §5.2). Do not suppress | This is a live 32-bit truncation bug the compiler is pointing straight at. Suppressing it deletes the only warning in the corpus that is unambiguously right |
CS0162 |
607 | 70 | Two shapes: 386 are the body of an if (constFalse) — raceenabled, msanenabled, boring.Enabled, debugFloat; 221 are a break; the converter appends after a case body that already ends in throw panic(…)/return |
S | Suppress in template — and stop emitting the redundant break; (source cleanliness, not warning count) |
Unreachable code is inert by construction; the residual risk is an unintended early return, which the Phase-4 verdict comparison catches |
CS8604 |
495 | 66 | Possible null reference argument — (~pe) after err._<ж<fs.PathError>>(ᐧ); the type-assertion helper’s failure value is null and the flow analysis does not follow the ok && guard |
P | <Nullable>annotations</Nullable> (§6) |
See §6 — Go has no non-nullable pointer, so this analysis is answering a question the language cannot pose |
CS8602 |
243 | 53 | Dereference of a possibly null reference — same family |
P | <Nullable>annotations</Nullable> |
” |
CS8619 |
226 | 65 | (nint, error? err) doesn’t match (nint n, error err) — the named-return local is error err = default! (non-null) while the tuple literal infers error? |
P | <Nullable>annotations</Nullable> |
” |
CS0164 |
181 | 35 | Unreferenced labels: 91 are the converter’s synthetic continue_<label>: / break_<label>: pair, emitted for every labeled Go statement whether targeted or not; 90 are the Go label itself (Loop:, bucketloop:) |
S | Suppress in template; optionally emit labels only when referenced | An orphaned label could mean a dropped goto, but a dropped goto changes behavior and is caught downstream |
CS8974 |
115 | 5 | ["and"u8] = and in a map<@string, any> — Go stores function values in map[string]any; also abi.FuncPCABIInternal(mapaccess2_fast64) |
S | Suppress in template | “Did you mean to invoke it?” — the converter decides call-vs-value from the AST, so a false positive here is not a class go2cs can produce accidentally |
CS1717 |
77 | 24 | (x, err) = (x, default!); — the named-return store on the goto ᒐdone path through a defer frame |
S | Suppress in template | Self-assignment is a no-op; the emission is generated, never hand-written |
CS8603 |
69 | 36 | Possible null reference return |
P | <Nullable>annotations</Nullable> |
See §6 |
CS8601 |
40 | 20 | Possible null reference assignment |
P | <Nullable>annotations</Nullable> |
” |
IL2091 |
37 | 9 | trim analyzer: generic argument lacks DynamicallyAccessedMembers |
P | Condition the publish properties (§6.2) | The analysis re-runs at app publish where it is actionable; nothing is permanently lost |
CS8600 |
35 | 18 | Converting null literal or possible null value to non-nullable type |
P | <Nullable>annotations</Nullable> |
See §6 |
IL2026 |
31 | 9 | RequiresUnreferencedCode — StackFrame.GetMethod() in runtime/managed_impl.cs, encoding/json reflection |
P | Condition the publish properties | See IL2091 |
IL2111 |
27 | 6 | Delegate.CreateDelegate reached via reflection (time/sleep.cs) |
P | Condition the publish properties | ” |
CS8714 |
19 | 2 | K doesn’t satisfy the notnull constraint on golib’s IMap<TKey,TValue> (maps/iter.cs, 15) |
P/G | <Nullable>annotations</Nullable> clears it; the durable fix is golib relaxing where TKey : notnull (src/core/golib/map.cs:50) or the converter emitting notnull on Go type parameters |
Low — it is a constraint-annotation mismatch, not a nullability defect |
CS8500 |
15 | 4 | takes the address of … a managed type — fixed (void* ptr = &value.Value) in TypeGenerator output (10) plus converter unsafe reinterprets in runtime/iface.cs, os/user, internal/abi |
G/F | Do NOT suppress corpus-wide. golib’s local suppression stays; the 10 generated ones are a go2cs-gen board row |
High. This is the static signal for the exact managed-referent hazard the S1 fork ruling is about. Silencing it corpus-wide removes the only compile-time tell |
IL2070 |
14 | 2 | trim analyzer, Type.GetFields etc. (10 in golib) |
P/G | Condition publish properties for the corpus; golib should annotate, not silence (§7) | See IL2091 |
CS8625 |
14 | 6 | Cannot convert null literal to non-nullable reference type |
P | <Nullable>annotations</Nullable> |
See §6 |
CS0675 |
11 | 4 | bits \|= (uint64)((byte)(c & ~0x20) - 'A' + 10) — sign-extended int widened to uint64 |
F | Keep visible. Low volume, and signed/unsigned width choice is a defect class that has bitten this converter before | Real: a genuine width mistake looks exactly like this |
CS1718 |
10 | 3 | return f != f; — Go’s NaN idiom, verbatim (runtime/float.cs:17) |
S | Suppress in template | The idiom is deliberate and the warning can never be right in converted float code |
IL2067 |
9 | 1 | trim analyzer, golib only | G | golib-local (§7) | — |
CS8826 |
7 | 1 | partial-method signature differences — the hand-owned runtime/debug/stubs_impl.cs names parameters _ where the converted declaration names them fd/in/Ꮡp |
F | Keep visible. Fix by renaming the parameters in the hand-owned file | Real: a genuine signature drift between a hand-own and its converted declaration is exactly what this catches |
IL2075 |
5 | 2 | trim analyzer (2 golib, 3 reflect) |
P/G | as IL2091 |
— |
CS8618 |
5 | 1 | golib fields (slice.m_array, ж.m_val) |
G | golib-local — add CS8618 to golib’s own NoWarn, or = null! the fields |
— |
CS0252 |
4 | 1 | if (key == ᏑcancelCtxKey) in context/context.cs — any == ж<nint> binds reference comparison, bypassing ж<T>’s == |
F | Keep visible — board row (§5.3) | Real, and pointed at a correctness assumption (that Ꮡ<global> yields a stable box) that is currently unstated |
CS0649 |
3 | 3 | traceviewer.staticContent is a //go:embed embed.FS the converter cannot populate; exithook.running is dead in Go too; windows._ᴛ1ʗ |
F | Keep visible — one of the three marks the unimplemented //go:embed |
Real: “field never assigned” is how a dropped initializer would present |
CS8860 |
3 | 1 | net/http/fcgi has a Go type named record |
S | Suppress in template (same family as CS8981) |
None — Go type names are not negotiable |
IL2060 IL2090 IL2072 IL2059 |
1 each | 1 | trim analyzer, all in golib | G | golib-local (§7) | — |
CS8620 |
1 | 1 | slice<error> vs slice<error?> in fmt/errors.cs |
P | <Nullable>annotations</Nullable> |
See §6 |
CS1522 |
1 | 1 | empty switch block (net/http/httptest/server.cs:144) |
F | Keep visible — an empty switch is a converter artifact worth a look | Real, and it is a single site |
5. The three converter roots this census found
These are the product of the audit, separate from the suppression question. Each is a board row.
5.1 Dead named-return locals — CS0219, 1,219 sites, 136 packages
The converter emits every named result as a local at function entry:
public static (rune r, nint size) DecodeRune(slice<byte> p) {
rune r = default!; // never read — the body returns explicit tuples
nint size = default!;
1,218 of 1,219 CS0219 sites are that declaration. The names already survive in the C# return
type ((rune r, nint size)), so nothing is lost by omitting a declaration the body never touches —
and the emission gets closer to the “readable Go-like C#” goal, not further. The check is a body
walk: emit the local only if the named result is read, assigned, address-taken (Ꮡ(err)), or
captured by a defer closure. Doing this preserves CS0219 as a live signal for a genuinely
dropped assignment, which suppression would destroy.
5.2 nint-typed constants that should be int64 — CS8778, 620 sites
Two distinct roots wearing one warning code:
- Composite-literal element type ignored (607 sites, all
math/rand/rng.cs). Go’srngCooked [607]int64 = [...]int64{-4181792142133755926, …}becomesarray<int64> rngCooked = new int64[]{-(nint)4181792142133755926L, …}— the untyped constants tookint→nintinstead of the literal’sint64element type. On a 32-bit target these truncate andmath/randsilently produces different numbers. The same defect appears sporadically elsewhere:math/rand/v2/regress_test.cs:41has(nint)1000000000000000000Lsitting in anew int64[]{…}beside twelve correctly-typed siblings. - Folded constants not wrapped in
unchecked(13 sites).bufio/scan.csshows both halves of this in three lines: the const declaration is emitted correctly asunchecked((nint)9223372036854775807), but the folded use ofmaxInt/2two lines later is emitted bare as(nint)(4611686018427387903L). (The declaration is then dead — it is the single non-named-returnCS0219in the whole corpus.)
5.3 Interface-vs-pointer comparison binds reference equality — CS0252, 4 sites in context
internal static any Value(this ж<cancelCtx> Ꮡc, any key) {
if (key == ᏑcancelCtxKey) { // any == ж<nint> -> object reference comparison
Go’s key == &cancelCtxKey compares an interface to a pointer by dynamic type + pointer value. The
emitted form compares two object references, bypassing ж<T>’s own ==. It works today only
because Ꮡ<global> yields a stable singleton box — an invariant nothing states or guards.
context validates at 36/38, so this is not a live failure; it is an unstated dependency that a
future boxing change would break silently.
6. The two property changes — why NoWarn is the wrong instrument twice
6.1 <Nullable>enable</Nullable> → <Nullable>annotations</Nullable>
The nullable family is 1,142 warnings across nine codes (CS8604 495, CS8602 243, CS8619 226,
CS8603 69, CS8601 40, CS8600 35, CS8625 14, CS8714 19, CS8620 1) — plus the 4,421 CS8618
already suppressed. Listing nine codes in NoWarn is the wrong shape for one decision.
The decision is: Go’s type system has no non-nullable pointer, interface, map, slice, channel or
func. Every one of them is nil-able by construction, so C#’s flow analysis can only be satisfied by
annotating the entire emitted corpus ? — which would bury the Go shape the project exists to
preserve. And the analysis is not protecting a semantic go2cs wants: a converted program that
dereferences a nil Go value should fault, because that is Go’s nil-pointer panic.
annotations keeps ? meaningful (golib’s ж<T>?, PanicException?) and keeps default! legal,
while turning the warnings off. disable would be wrong — it makes every ? in the emitted code a
fresh CS8632.
Measured, and the reason CS8618 must stay in NoWarn: run D applied
Nullable=annotations and removed CS8618 from the list. CS8618 came back at 4,278 — and
all 4,278 are inside go2cs-gen output, because the generator emits #nullable enable in each
.g.cs (Templates/TemplateBase.cs:83) and a file-level directive beats the project property.
NoWarn does not lose that fight, which is why the entry earns its place.
That same mechanism is a feature for everything else: under annotations, the residual nullable
warnings are 30, every one of them in generated .g.cs (CS8619 19, CS8604 8, CS8714 2,
CS8603 1). The corpus goes quiet and the generator stays checked — a real, small, gen-local to-do
list instead of 1,142 lines of noise.
6.2 <PublishTrimmed>True</PublishTrimmed> on library projects
Every IL2### warning in the corpus — 127 across ten codes — is caused by this one line in the
template. The SDK turns PublishTrimmed into EnableTrimAnalyzer=true at build time, so the trim
analyzer runs on ordinary dotnet build. Verified directly: archive.zip rebuilds with 24 IL
warnings as committed and 0 with -p:PublishTrimmed=false; the full run D emitted zero IL####
corpus-wide.
On a Library project PublishTrimmed does nothing else — trimming is an application publish
operation. The same is true of PublishReadyToRun, IncludeNativeLibrariesForSelfExtract and
EnableCompressionInSingleFile, which sit in the same PropertyGroup. The honest fix is to condition
that group on '$(OutputType)' != 'Library', so a converted main package (which is published, and
where the Performance suite really does Native-AOT it) keeps the analysis, and 302 library packages
stop paying for it.
Nothing is permanently lost: the trimmer re-runs over the whole closure at app publish, where the warning is actionable.
7. golib and go2cs-gen — separate owners, separate answers
- golib’s 26
IL####warnings should not be silenced. golib is the reflection core (GoReflect,AdapterBinder,PointerExtensions,builtin.ZeroFacts<T>) and it is exactly the assembly a trimmed or AOT-published converted app will break on. The right work isDynamicallyAccessedMembersannotations and targetedUnconditionalSuppressMessagewith a justification — not aNoWarn. - golib’s
CS8500suppression is justified and should stay: golib is where the deliberate unsafe machinery lives. The corpus must not inherit it. - golib’s
NoWarnneeds a small clean-up: drop1701;1702(net9.0 cannot emit them), spell660;661asCS0660;CS0661, and addCS8618(5 sites) or initialize the two fields= null!. go2cs-genhas a 30-warning nullable to-do list of its own (visible only once the corpus goes quiet, §6.1) plus 10CS8500fromTypeGenerator’sfixed (void* ptr = &value.Value)over a managed type. If the generator stopped emitting a blanket#nullable enable,CS8618could eventually leave the corpusNoWarnlist entirely.
8. Projected totals
| Stage | Warnings | Δ |
|---|---|---|
| Today (Debug or Release, clean build) | 4,147 | — |
+ NoWarn additions: CS0162, CS0164, CS1717, CS8974, CS1718, CS8860 |
3,154 | −993 |
+ <Nullable>annotations</Nullable> |
2,042 | −1,112 |
+ publish properties conditioned to non-Library (golib keeps trim analysis) |
≈1,941 | −101 |
| + converter fix §5.1 (dead named-return locals) | ≈722 | −1,219 |
+ converter fix §5.2 (nint constants) |
≈102 | −620 |
Measured outcome (§11): 1,945 → 158. The §5.2 projection was exact (−620, to zero). The §5.1 projection was optimistic by 52 — see §11.3 for the three shapes that legitimately remain, none of them the named-return prologue this arc was about.
The residual ~100 is the honest signal: golib’s 26 trim warnings and its own 6 nullable ones, the 30
gen-local nullable items, 15 CS8500, 11 CS0675, 7 CS8826, 4 CS0252, 3 CS0649, 1 CS1522 —
every one of them a real item with an owner, and none of them noise.
For reference, the exact configuration of run D (Nullable=annotations, PublishTrimmed=false,
proposed NoWarn including CS8618) measured 1,903; the ≈1,941 above adds back CS0675
(kept visible by recommendation) and golib’s own warnings, which the global -p:NoWarn in run D
overrode.
9. Footprint
The template edit regenerates <NoWarn> and the Nullable/publish properties in all 304
.csproj at the next -stdlib reconvert — a one-family corpus diff, mechanically verifiable
(every changed line is inside those two PropertyGroups). Hand-owned project files need the same
edit by hand (see the §2 correction: five under core/, plus golib with its own list). Both
templates change together — csproj-template.xml and test-csproj-template.xml carry the same
NoWarn line and the same Nullable property, and csprojTemplate_test.go gates that both still
render well-formed XML.
The audit under-counted the footprint in one more way:
csproj-template.xmlis rendered for every conversion, not just-stdlib, so the same delta lands on the 574 behavioral-test and 13 performance-benchmark.csprojthe moment any transpile gate runs. That is not optional churn to be restored — those project files are converter output, and leaving them behind would make every futurecheck-no-regression.ps1run dirty the tree. They land with the template.
10. What landed — r46b-warnsuppress, 2026-08-08
The configuration half only. Everything §4 marks F stays visible, and both converter roots (§5.1, §5.2) remain open.
The edits. csproj-template.xml and test-csproj-template.xml: <Nullable>enable</Nullable> →
<Nullable>annotations</Nullable>; the six new NoWarn entries merged into one numerically-sorted
list, CS8618 retained for the reason in §6.1; and the publish PropertyGroup conditioned
'$(OutputType)'!='Library'.
⚠ AllowUnsafeBlocks had to come out of that group first. §6.2 named the group by its four
publish properties, but the fifth element in it is <AllowUnsafeBlocks>%s</AllowUnsafeBlocks> — a
compile setting the converted stdlib’s library packages cannot build without. Conditioning the group
as written would have taken AllowUnsafeBlocks off every library project in the corpus. It now lives
in its own unconditional PropertyGroup immediately below, and
TestPublishPropertiesAreScopedOffLibrariesButAllowUnsafeBlocksIsNot pins both halves.
Measured, src/go2cs-stdlib.slnx, 304 projects, -t:Rebuild, isolated
(MSBUILDDISABLENODEREUSE=1, -p:UseSharedCompilation=false):
| Run | Configuration | Warnings | Errors |
|---|---|---|---|
| before | -c Debug |
4,147 | 0 |
| before | -c Release |
4,147 | 0 |
| after | -c Debug |
1,945 | 0 |
| after | -c Release |
1,945 | 0 |
The before runs reproduce the audit’s baseline code for code, so the two measurements are
comparable line by line. Debug and Release stay identical on both sides. −2,202, or −53.1 %.
Against §8’s projection of ≈1,941 the measured 1,945 is +4, and the four are named: run D’s global
-p:NoWarn also overrode go2cs-gen’s own generated files, so the gen-local nullable list is 34
here rather than 30 (CS8619 19, CS8604 9, CS8603 2, CS8714 2, CS8625 1, CS8600 1).
The residual, in full: CS0219 1,219 and CS8778 620 (the two converter roots — 94.5 % of what is
left, and both are supposed to be there until §5.1 and §5.2 land), 34 gen-local nullable, CS8500
15, CS0675 11, CS8826 7, CS8618 5 (golib), CS0252 4, CS0649 3, CS1522 1, and 26 IL####.
Every IL#### warning left in the corpus is golib’s — 127 → 26, and all 26 are attributed to
golib.csproj, which keeps its trim analysis on purpose (§7). That is the §6.2 hypothesis confirmed
end to end: nothing but PublishTrimmed on a library was producing the other 101.
The nullable family went to zero in converter output. 1,142 across nine codes → 34, every one of
them inside a go2cs-gen-emitted .g.cs (the #nullable enable those files carry beats the project
property). §7’s “small, real, gen-local to-do list” is now visible instead of buried.
golib’s own list was cleaned as §7 asked: 660;661;1701;1702;IDE1006;CA2255;CS8500;CS8981 →
CS0660;CS0661;CS8500;CS8981;IDE1006;CA2255. CS8618 was not added — the five sites
(slice.m_array, ж.m_val, one in GoReflect.ValueMarshalling) stay visible as the to-do §7 wants;
the durable fix is = null! on the fields, not a suppression.
Corpus footprint: 303 .csproj under src/core (297 regenerated by a seeded reconvert + 6
hand-edited), 574 behavioral and 13 performance .csproj regenerated by their transpile gates, and
110 .tests.csproj regenerated by the validated sweep. One family: every changed line is inside the
two PropertyGroups, at a uniform +18/−4 per generated project (+5/−2 for a .tests.csproj, which
has no publish group to condition; unsafe +15/−4, testing +5/−2, golib +6/−1, reflecting their
divergent shapes). The seeded reconvert’s 12,520 emitted .cs/.csproj/README.md were
byte-compared against the committed tree first: zero .cs or README.md content differences (the
50 that differ raw are pure CRLF phantoms — CR-stripped equality is exact), so the .csproj delta is
provably the whole change.
Gates. go test ./... green with the two new guards, each proved by negative control. Marker gate
before the overlay: 41 marked files, 15 with a .cs.auto sibling, 0 clobbered.
check-no-regression.ps1 574/574 byte-identical .cs (438 s) — a template change cannot move .cs,
and it did not. Behavioral suite 549/549 transpile + compile + golden with 523/523 stdout comparisons
against go run, 0 fail (688 s). Validated sweep 110 pass / 0 fail over 13,628 expected verdicts
(3,025 s), every package at its exact banked count. The sweep’s remaining aftermath was classified and
restored, never banked, per the standing families: 20 -tests-closure production .cs, 7 production
.cs and 16 *_test.cs that are pure CRLF phantoms with no numstat at all, 85 *_test.cs carrying
accumulated drift that belongs to a milestone test-source rebank rather than to this arc, 8 .cs.auto
review siblings, and 10 -text testdata paths verified CR-only by CR-stripped equality against
HEAD (their numstat is non-empty by construction, so --numstat is the wrong instrument there).
Zero unclassified.
11. What landed — r48c-warnroots, 2026-08-08
Both converter roots. The NoWarn list is unchanged: the point of fixing these two rather than
suppressing them was to keep both codes alive as signals, and both are still fully enabled.
11.1 CS8778 — the constant took the untyped-int DEFAULT type (§5.2)
§5.2 named two roots. There were three emitters, and the accounting closes exactly:
| Emitter | Sites | Shape |
|---|---|---|
convBasicLit — the beyond-int32 literal branch |
614 | -(nint)4181792142133755926L in a new int64[]{…} |
convBinaryExpr — the constant FOLD |
3 | (nint)(4611686018427387903L), (nuint)(140737488355327UL) |
convCallExpr.csNintLiteral — an array LENGTH |
3 | (nint)140737488355327 for (*[maxAlloc/2 - 1]byte) |
| 620 | = the measured CS8778 total |
§5.2’s first root said the composite literal’s element type was “ignored”. The mechanism is sharper
than that, and worth recording because it will recur anywhere the converter reasons about a constant
operand’s type: go/types deliberately leaves the operands of a constant expression untyped.
updateExprType0 short-circuits with “if x is a constant, the operands were constants” and does
not descend — in Go they never materialize at runtime, so there is nothing to type. The observable
consequence is that in
rngCooked [rngLen]int64 = [...]int64{-4181792142133755926, 1395769623340756751, …}
the negated element records untyped int while its positive sibling records int64, purely
because one is wrapped in a unary minus. Every element of rngCooked is negative — which is exactly
why 607 of the 620 sites are that one table, and why positive-only tables elsewhere were never
affected. The emission branch then never consulted a type at all, so it applied the untyped-int
default (int → nint) universally.
The fix resolves the literal’s integer type from the two routes convBasicLit already uses for the
float F/D suffix — the type go/types recorded directly, else the contextual type
markUntypedConstContexts propagated (which already pushes an integer context through unary
+/-/^). An int64 resolution emits the bare …L; everything else keeps nint, because an
any slot must box a Go int as nint for a later x.(int), but wraps it unchecked(…) so the
constant conversion is legal without the warning. nint is 64-bit on every platform go2cs targets,
so the value is exact at runtime; unchecked only states it.
wholeExprIsCastOfType — the whole-expression-cast redundancy guard 17 call sites share — now
peels an unchecked( wrapper before matching. Without that, the wrapped fold stops being recognized
and the enclosing guards re-wrap it into (nint)(unchecked((nint)(…))). The balance walk was
factored into balancedCloseIndex and is otherwise unchanged.
The emission is now byte-for-byte the Go source’s own digits:
internal static array<int64> rngCooked = new int64[]{
-4181792142133755926L, -4576982950128230565L, 1395769623340756751L, 5333664234075297259L,
CS8778: 620 → 0.
11.2 CS0219 — the named-return prologue (§5.1)
§5.1’s diagnosis and its proposed check were both correct, and the implementation follows it: emit the named-result local only when the body can read it. What §5.1 did not say is that the check must be switched off — not merely satisfied — for the two lowerings where generated code reads the locals and the Go body need never mention them:
namedReturnDeferMode, where the declarations sit outside thefunc()wrapper precisely so deferred closures can mutate them, and the read happens after the wrapper returns.- A GoFrame, whose named exit emits a trailing
return <names>;after thetry, reached bygoto ᒐdonefrom every return inside.
Inferring liveness from the body in either case would drop a declaration the emitted code
references — CS0103, a hard error rather than a warning. So the caller passes a nil body and the
predicate returns true wholesale.
Otherwise the declaration is kept when the body references the result (read, assigned,
address-taken, or captured — the walk descends into function literals, because a capture is a use),
when the body has a naked return (which reads every named result; that walk stops at a nested
*ast.FuncLit, whose bare returns belong to the literal), and when the result is heap-box
backed. The same rule and the same opt-out apply to function literals via namedReturnDeclLines.
Everything unclear — no body, unresolved object — keeps the declaration: one dead line is cheap,
a dropped live one is not.
CS0219: 1,219 → 52, and none of the 52 is a named-return prologue.
11.3 The 52 retained CS0219, by design
| Shape | n | Why it stays |
|---|---|---|
Hand-owned files the converter never re-emits — sync/atomic/type.cs (27), syscall/exec_windows.cs (6) |
33 | These carry [module: GoManualConversion], so a reconvert leaves the .cs alone. Their .cs.auto review siblings do carry the fixed form (both appear in this arc’s A/B), which is the tell: this is hand-own staleness (CleanupBacklog item 18), not a converter gap. Fixing it means hand-editing hand-owned files — a separate, deliberate act. |
Go’s own var witnesses for a constant-folded unsafe.Sizeof/Offsetof — runtime/runtime1.cs (11), debug/elf/file.cs (6), sync/runtime.cs (1) |
18 | var a int8 exists in the Go source only so unsafe.Sizeof(a) can name a typed operand. go2cs folds the Sizeof/Offsetof to a constant and keeps the original in a comment (/* unsafe.Sizeof(a) */), which leaves the declaration unread. It is Go-visible source, and the emitted comment still names it — deleting it would delete code the reader is meant to see. |
A folded local const — bufio/scan.cs:169 maxInt |
1 | The const maxInt = int(^uint(0) >> 1) declaration is Go-visible; its only use, maxInt/2, is constant-folded by convBinaryExpr. Already identified in §4 as the single non-named-return site. |
Each is Go-visible code or frozen hand-owned text, so none is fixable at the converter layer without
distorting the output. CS0219 stays enabled: with the prologue gone, a genuine dropped
assignment to a named result would now stand out among these 52 instead of being lost in 1,219.
11.4 Measured
src/go2cs-stdlib.slnx, 304 projects, -c Debug -t:Rebuild, isolated (MSBUILDDISABLENODEREUSE=1,
-p:UseSharedCompilation=false), warnings captured to a file logger:
| Run | Warnings | Errors |
|---|---|---|
| before (reproduces §10’s post-suppression baseline exactly) | 1,945 | 0 |
| after | 158 | 0 |
−1,787, or −91.9 %. Attributable per family, measured not estimated:
| Code | before | after | Δ |
|---|---|---|---|
CS0219 |
1,219 | 52 | −1,167 |
CS8778 |
620 | 0 | −620 |
| every other code (21 of them) | 106 | 106 | 0 |
The zero on that last row is the load-bearing one: no code moved in either direction except the two targeted, so nothing was traded away. Cumulatively with §10 the corpus is 4,147 → 158, −96.2 %.
11.5 A/B footprint
Two seeded temp roots, converted from the same seed by the pre-fix and post-fix binaries, so the
diff is this change alone and is independent of any pre-existing corpus drift. Marker gate on both
roots: 41 marked / 15 with a .cs.auto sibling / 0 clobbered. A control comparison of the
pre-fix root against the committed tree found zero production .cs/.csproj/README.md
content drift (52 CRLF phantoms, CR-stripped-equal; the only real differences were 12 stale
.cs.auto, the known standing family) — so the committed tree sat exactly on the -stdlib emission
point and the overlay banks precisely what is classified below.
287 files differ; zero unclassified:
| Family | Lines | |
|---|---|---|
CS0219 — removed dead declaration |
1,213 | |
CS0219 — the orphaned blank separator that followed the declaration block |
437 | every one in a file that also lost a declaration |
CS8778 — old (nint)/(nuint) cast removed |
165 | |
CS8778 — new bare …L (the int64 resolution) |
157 | |
CS8778 — new unchecked((n[u]int)…) |
8 |
By artifact type the diff is .cs only — no .csproj, no README.md, no .slnx — which is
what an emission-only change must look like. 283 .cs were overlaid; the 4 .cs.auto were excluded
per the standard overlay rule (they protect the hand-owned .cs beside them and go stale on their
own schedule).
11.6 Gates
| Gate | Verdict |
|---|---|
go test ./... (converter) |
green, 58 s. Both new guards run, and each is proved by negative control — nativeIntConstWidth_test.go fails when the int64 arm is neutered; namedResultLiveness_test.go fails in BOTH directions (always-true leaves the three dead declarations, always-false drops all five live ones). projitemsIntegrity green, including its BOM and line-ending assertions. |
| Marker gate, both A/B roots | 41 marked / 15 with a .cs.auto sibling / 0 clobbered, line-anchored and path-precise. Matches the r44a census; re-measured, not carried forward. |
Corpus build — go2cs-stdlib.slnx, 304 projects, -t:Rebuild, isolated |
0 errors, 1,945 → 158 warnings. |
| Behavioral suite — 549 projects, 1,239 s | Transpile 549/549, Compile 549/549, Output 523 pass / 0 fail / 26 skip (no package main), Target 534 pass / 15 re-baselined. The emission change compiles everywhere and stdout still matches go run everywhere; only the goldens were stale, and the 15 match the 15 git-modified .cs exactly. |
| Golden re-baseline | UpdateTestTargets --createTargetFiles after the suite’s own transpile — one golden per changed .cs, and no *Tests.cs churn, confirming no project was added or removed. |
check-no-regression.ps1 — 574 packages, 411 s |
NO REGRESSION: byte-identical across all behavioral projects, so the banked tree is a fixed point of the converter. Solution integrity 576/576 registered; path casing 4,142/4,142. |
go2cs.slnx build — 573 projects |
0 errors (the only gate that compiles the non-generated members). |
Not run, and deliberately: the validated sweep. This change re-emits the banked *_test.cs
sources too, but those are refreshed at a milestone test-source rebank rather than per arc (the
standing practice §10 followed), so they are left as they are and will pick the new emission up on
their next regen.