Pointers
Reference index · Summary of this topic
Pointer conversions use the golib heap box ж<T> (read “zhe”). Taking the address of a value uses the address-of operator Ꮡ (e.g. Ꮡx); an escaping local is allocated via heap(...), and addresses of a struct field or array element are taken through .of(Type.ᏑField) / .at<T>(index).
The box’s value accessors follow one naming scheme : Value is the strict dereference (ref-returning; panics on a nil pointer, as Go does), ValueSlot is its no-check twin (the identical real slot — for reads/writes of a held value that may legally be nil), and DerefOrNull() is the null-box-tolerant extension every pointer ENTRY alias binds through (an extension method is the only ref-returning form C# permits on a possibly-null receiver; it returns a NULL ref when nil, so the alias binds and the panic lands at the body’s own deref). The same Value name is used by the generated named-type wrappers for their underlying-value accessor and by the golib uintptr struct for its raw word — converted code has exactly one spelling for “the value behind this thing”. A Go struct field named val still emits as .val (it is the user’s identifier, not the accessor):
ref var a = ref heap(new array<@string>(2), out var Ꮡa); // escaping local
var p = Ꮡa.at<@string>(0); // &a[0]
var pField = Ꮡsettings.of(settingsᴛ1.ᏑRetries); // &settings.Retries
A heap-boxed range variable needs the box allocated per iteration. When a for i := range s (or for _, f := range s) variable has its address taken, it escapes — but the foreach already declares that name, so a single ref var i = ref heap(…) before the loop would clash (CS0136). The converter iterates a temp and, inside the body, allocates a fresh box each pass and copies the temp into it:
for i := range s {
p := &i // i escapes
use(p)
}
foreach (var iᴛ1 in range(s)) {
ref var i = ref heap(new nint(), out var Ꮡi); // a FRESH box each iteration
i = iᴛ1;
var p = Ꮡi;
use(p);
}
The per-iteration box is required for Go 1.22 loop-variable semantics: each iteration’s variable is distinct, so a stored &i must point to a different box each pass (for i := range s { ptrs = append(ptrs, &i) } yields 0 1 2, not 2 2 2). A non-escaping companion variable still declares directly in the foreach. (Guarded by the RangeVarHeapBox behavioral test — both a within-iteration &i and the stored-pointer distinctness case; runtime exercises it in for i := range stackpool and for _, f := range s.Fields.) A heap-boxed for i := …; cond; post clause variable takes the same per-iteration box through its carrier rewrite — see For-clause variables are per-iteration.
The at<T>(index) element-address accessor has int, nint and ulong overloads, and each checks the index at its FULL value against the length before anything narrows it, as Go does (goPanicIndex / goPanicIndexU): an out-of-range index panics with Go’s index out of range [i] with length n, recoverable. A wide unsigned index (uint/uint32/uint64/uintptr) takes (ulong) and binds the ulong overload, so an index at or above 2^63 keeps its value. (A (nint) cast read it as negative and reported [-N].) It is a cast rather than bare because the C# expression can be wider than its Go type: g % 2 for a uint g is a C# long. Any other non-int integer index (int64, or a small kind) takes (nint), which loses nothing. An int index, or an untyped int constant (which renders as a plain int literal), is emitted as-is:
var pi = Ꮡa.at<nint>((ulong)(i)); // &a[i] where i is a uintptr
var pe = Ꮡa.at<nint>((ulong)(g % 2)); // &a[g%2] where g is a uint (g%2 widens to long in C#)
The field forms Ꮡp.at(S.Ꮡa, i) (an array field reached through a pointer) carry the same three overloads. This is the element-address analogue of the indexed-literal key cast (SparseArray<T>, in Slices and Arrays) and the IBinaryInteger<T> width-agnostic length params on unsafe.Add/Slice/String. (Guarded by the ArrayWideIndexAddress and PointerElementAtUnsignedIndex behavioral tests.)
A string indexed by a wide/unsigned integer keeps its full value too. An @string variable’s indexers (int, nint, ulong) each bounds-check before any narrowing, so an unsigned index is emitted bare onto this[ulong] and an int64 takes (nint). A string LITERAL renders as a ReadOnlySpan<byte> ("…"u8) whose indexer takes int and throws a CLR exception recover() cannot see, so a NON-constant index on a literal renders as golib’s LiteralByteAt(<literal>, <index>), which applies the same check and allocates nothing: runtime heapdump.go’s "0123456789abcdef"[pc&15]. A constant index is in range by Go’s own compile-time check and keeps "…"u8[i]. (Guarded by the ArrayWideIndexAddress extension, literal and variable string bases with uintptr/uint64 indexes, and by UnsignedIndexPastIntRange and SliceBoundsEscapeRoutes.)
The address of a slice element uses the call form Ꮡ(slice, index) (golib overloads Ꮡ<T>(IArray<T>|slice<T>|array<T>, int|nint|ulong)) rather than at<T>. Every overload checks the index at its full value against the slice’s LENGTH, so &s[5] on a slice of length 3 and capacity 10 panics as Go does. An unsigned index (uint/uint32/uint64/uintptr) is emitted bare onto the ulong overload and an int64 takes (nint). Go int (→ nint) and the small integer types that implicitly widen to int bind directly. (Mirrors the runtime’s &datap.pclntable[funcoff] / &filetab[fileoff], indexed by uint32 offsets. Guarded by the ElementAddressUnsignedIndex and SliceBoundsEscapeRoutes behavioral tests.)
The Ꮡ(slice, index) form applies to any slice-typed base expression, not just a named slice variable — a method-call result (&b.stk()[0], runtime mprof.go; &StringByteSlice(s)[0], syscall), a builtin/make result, an unsafe.Slice(…) result (reflect), or a slice-expression base (&x[0:cap(x)][cap(x)-1], math/big). Such bases have no bare identifier, so an identifier-gated slice arm would drop them into the array branch — a slice’s type name also starts with [ — whose naive fallback textually prefixes Ꮡ onto the postfix chain: Ꮡb.stk().at<uintptr>(0) binds as (Ꮡb).stk()…, referencing a box that does not exist (CS0103), or copy-boxes the slice header (a lost write). The element address of the returned slice view reaches the shared backing array per Go aliasing, so a write through the pointer is visible via the original storage. (Guarded by the NestedFieldElementAddr extension — &st.stk()[0] through a pointer local, write-through vs Go.)
The bounds of a 3-index (full) slice s[low:high:max] lower to the golib .slice(nint low, nint high, nint max) method, and a uintptr/uint/uint32/uint64/int64 bound takes (nint) (the castWideIntegerToNint helper): arr[:n:n] (n a uintptr) → arr.slice(0, (nint)(n), (nint)(n)), the omitted low written as the 0 it means. A plain int/small-int bound is left uncast. (nint) rather than (int): an (int) cast truncated s[0:w:w] at w = 1«32+5 to len 5 cap 5. The three bounds can mix kinds, so they take one nint overload rather than unsigned ones, and an unsigned bound at or above 2^63 panics with a signed bound in its text, a stated residual. (A 2-index s[lo:hi] keeps the C# [..] range only when each bound is a constant that fits in an int; any other bound takes the same nint method, .slice(lo, hi), through sliceMethodCall.) (Guarded by the Slice3IndexWideBound behavioral test — uintptr/uint/uint64 full-slice bounds on an array and a slice + an int control, values verified vs Go.)
Address of an element of an array field reached through a pointer or boxed struct. When the array being indexed is a field of a heap-boxed value — &mp.future[i] where mp is a *memRecord, or &g.future[i] where g is an address-taken global — the array field’s address goes through the box-field accessor first, then the element index: Ꮡmp.of(memRecord.Ꮡfuture).at<cycle>(i) (pointer parameter), mp.of(...) (pointer local), Ꮡg.of(rec.Ꮡfuture).at<cycle>(i) (boxed global). A naive Ꮡ prefix on the field read (Ꮡ(~mp).future) instead binds .future to the box value Ꮡ(~mp) (a ж<memRecord>, which has no future member) → CS1061. This requires a matching golib detail: ж<T>.at<TElem>(index) resolves the array through the Value property, which every box kind answers from its real storage — a field-reference pointer produced by of(...) is a FieldRefBox holding no value of its own, and the real array lives behind Value (the same resolution of(...) itself uses). Reading a box’s raw m_val field instead would, for a field reference, read an empty default — a null-deref at runtime even though the C# compiles. (array<T> is a readonly struct over a shared backing T[], so the value Value yields still aliases the real elements; writes through the returned element pointer land.) (Guarded by the PointerFieldArrayElementAddress behavioral test — pointer parameter and pointer local both taking &p.future[i] and mutating through it.)
The RECEIVER’s own array field is the one case that does not take this route: a Go pointer receiver renders as this ref T recv, which has no box companion, so of(...) would name a box that does not exist (Ꮡr.of(RegArgs.ᏑInts), CS0103). It uses the element-aliasing two-arg Ꮡ(recv.field, (int)(i)) instead — correct because copying an array<T> wrapper shares its backing T[]. See Element address of an ARRAY FIELD of the receiver under Combined field-element address for the write-dropping copy this avoids (compress/flate losing all LZ77 matching).
A colliding pointer-adapter name qualifies its FOREIGN interface side
A pointer-interface adapter class is named [<pkg>_]<structSimple>ж<ifaceSimple>. The STRUCT side is
package-qualified when foreign (bytes_ReaderжReader), which keeps two same-named foreign structs
adapting to one interface apart. Composed from its bare last-dot segment alone, the INTERFACE side
would collide in the mirror-image case: ONE struct cast to TWO interfaces whose simple
names match composes one class name twice (CS0102, CS0111 per member, CS8646).
compress/flate is the case that surfaced it. It declares its own Reader (io.Reader +
io.ByteReader), and its tests hand a *bufio.Reader and a *bytes.Reader to NewReader, which
casts to both that and io.Reader — so without a qualifier bufio_ReaderжReader and
bytes_ReaderжReader would each be emitted twice and the test build would fail. The rule is
collision-conditional: only within a group of records composing the same name does the interface
side take a package qualifier (bufio_Readerжio_Reader), and the LOCAL member of a group keeps the
bare name (at most one member can be local, so that stays unambiguous and preserves the Go-like short
form). Qualifying unconditionally was measured and rejected: it would rename hundreds of adapter names
across thousands of construction sites. The production corpus contains no collisions, so the rule is
byte-neutral there by construction; it takes a test closure’s extra casts to make one.
Grouping keys on the whole composed name, struct side included. compress/gzip records both
<Reader, io.Reader> and <bufio.Reader, flate.Reader> — two records whose interfaces share a simple
name but whose struct sides differ, composing ReaderжReader and bufio_ReaderжReader. Keying on the
interface name alone would call that a collision and rename a validated package’s adapters for nothing.
The converter and the generator must agree on every name, and neither may guess, so both read the same
authority: the final [assembly: GoImplement<…>(Pointer = true)] lines. That set is not known while
cast sites are being rendered — it is settled only after the whole package is visited and
writePackageInfoFile has applied its alias-covered skip and its interface-inheritance prune — so a
cast emits a deferred marker (mirroring the DYNTYPE marker of the anonymous-struct barrier) that
resolveAdapterNameMarkers rewrites once the records are final. The marker’s payload is hex-encoded
for the same reason DYNTYPE’s is: a rendered type name passes through string transformation passes
before reaching the file. Only the INTERFACE side is ever rewritten — the struct side is emitted
verbatim, because it is the reference’s path, not just a name fragment: rewriting it turned
new os.FileжWriter(f) (namespace os, adapter class FileжWriter, generated in os’s own assembly)
into a bare FileжWriter that resolves nowhere. (Guarded by AdapterNameInterfaceCollision — a local
Reader and io.Reader reached from one *src, verified by reverting the fix: CS0102 + CS8646
on srcжReader.)
The Value-not-m_val rule of ж<T>.at (see the top of this page) applies to the dereference operator ~ too. A value read through a pointer — (~c).field, the form the converter emits for c.field where c is a *T — must resolve through Value. For a field-reference pointer (c := &b.w → Ꮡb.of(box.Ꮡw)) or an array-element pointer, the real storage lives behind Value and m_val is an empty default, so operator ~ returning m_val would read a zero-valued copy ((~c).a → 0) — it compiles but is silently wrong. ж<T>.operator ~ therefore reads the box’s real slot — ValueSlot, after a structural nil check (see A pointer’s nilness and identity are STRUCTURAL below) — which resolves struct-field / array-element references and, for a standard pointer, is the box’s own value; the IPointer<T>.operator ~ reads the same way. This surfaced when a defined-type-over-struct’s forwarded fields were read back through a *wrapper, but it is general to any *x.field value read. (Guarded by the NamedTypeOverStruct behavioral test’s read-back path.)
The at<E>(i) element type E is rendered fully-qualified — at<sync.atomic_package.Int32>, not the file-local alias at<atomic.Int32>. A namespace-rooted type resolves inside namespace go; without any using <pkg> alias, whereas the alias form needs the file to import that package. A file can index a cross-package-typed array field of a struct without ever naming the element type (so Go requires no import, and the converter emits no using atomic), which would leave the alias unresolved (CS0246, e.g. runtime’s tracecpu.go indexing trace.cpuLogWrite). A current-package or basic element renders identically either way, so this is churn-free. (Guarded by the ArrayOfCrossPackageType behavioral test’s &x.c[i] element-address case.)
Using ж<T> rather than the C# ref keyword avoids the escape-analysis complications of passing a ref into code that expects a heap-allocated pointer. This is a simplification that can cost an unnecessary heap allocation when an address is taken; a future escape-analysis pass could keep such values on the stack when it is provably safe, similar to how Go does this at compile time. The ref-lowering below is the first such pass: an unexported function’s dereference-only pointer parameter takes a C# ref, and an address-taken local that feeds only such parameters stays a plain local.
Note: a package-level global whose address is taken is backed by a real heap box so that writes through
&global(and&global.field) are observed, rather than mutating a copy. So is every exported global of a value type, since another package may take its address:&otherpkg.V,&otherpkg.S.Fieldand&otherpkg.Arr[i]bind that package’s box. A pointer- or interface-typed exported global has no box, so another package’s&otherpkg.Pboxes a copy (Limitations). A global built on its first read keeps that box inside its[GoLazy]holder, so&globalnames the same box before and after the first read — see A pure-data var is built on its first read.That box is golib’s
StaticBox<T>(internal static readonly ж<T> ᏑX = new StaticBox<T>(…);, read asref ((StaticBox<T>)ᏑX).ValueSlot), aStandardBox<T>that also says it is package-level storage, as Go keeps a global outside every heap span. One Go answer turns on that: synctest leaves a package-levelsync.WaitGroupUnbubbled, so a global WaitGroup used in a bubble records no association and itsWaitis never durable. A field of a global answers the same through its view; an element of a global array does not, since its view holds the array’s backing rather than the box.
Pointer equality canonicalizes the STORAGE, not the referent — slice/array element identity
Go compares pointers by address: unsafe.StringData(s) == unsafe.StringData(t) is true whenever both strings share the same backing data (a header copy t := s, or strings.Map’s identity fast path returning s unchanged — strings’ TestMap asserts exactly that). ж<T>.Equals already models address identity per referent shape — struct-field refs compare (source, field-identity), array-index refs compare (backing, index), heap boxes compare wrapped-object identity. @string.buffer materializes a fresh PinnedBuffer view per access, so comparing the IArray instance would never find two StringData results over the very same bytes equal (“unexpected copy during identity map”); the array-index arm (and the matching GetHashCode) therefore canonicalizes a PinnedBuffer to the object its GCHandle pins (PinnedTarget, normally the string’s backing byte[]) before the reference comparison, so equal addresses compare equal while everything previously-equal stays equal — the canonicalization only ever adds true results for same-storage-same-index pairs, and distinct-but-equal arrays still compare unequal (Go pointer semantics, never value comparison). strings.Map’s fast path needed no change at all: it already returned s, sharing the backing array through the @string struct copy — only the identity comparison was blind. (Guarded by the StringDataIdentity behavioral output test — header-copy identity true, repeated-call identity true, a runtime copy false, content equality unaffected; before the fix the two identity cases printed false.)
The same class covers ORDINARY slice and array element pointers. Ꮡ(target, index) has slice<T> and array<T> overloads beside IArray<T>, and a slice<T> is a HEADER (backing array + low + len + cap) over storage it merely shares, so element identity must come from that storage, never from the header: comparing per-call header boxes would make &s[0] == &s[0] false, Go’s most basic pointer identity. The blast radius would be wider than comparison alone, because GetHashCode canonicalizes the same way: two aliasing element pointers would land in different map[*T] buckets, so m[&s[2]] = "two" then m[&s[2]] = "TWO" would add a second entry instead of overwriting. array<T> (and the generated named-array wrappers) is reached the same way through ж<T>.at (&a[i] on a boxed array), which hands the element box a copy of the wrapper struct.
The element-reference box ElemRefBox<T> classifies its referent when it is constructed, reducing it to the actual storage object plus the ABSOLUTE element index within it — the referent as stored is never the storage itself:
| Referent | Canonical storage | Index |
|---|---|---|
PinnedBuffer (per-access view) |
the object its GCHandle pins (PinnedTarget) |
unchanged |
slice<T> |
its backing T[] (m_array) |
Low + index |
| a named slice type | the backing of the slice<T> its full-window interface sub-slice hands back |
Low + index |
array<T>, named-array wrapper |
the raw backing via non-generic IArray.Source
|
unchanged |
any foreign IArray
|
the referent itself (prior behavior) | unchanged |
Folding the window offset into the index is what makes every Go alias of one element compare equal, not just the same-header case: a re-slice (&s[1:][0] == &s[1]), a re-slice of a re-slice, an in-capacity append result (&append(s[:0:1], 9)[0] == &s[0]), and a slice over an array (&a[:][i] == &a[i]) all reduce to the same (T[], absolute index) pair. A named slice type wraps a slice<T> it does not expose, so it is unwrapped via view.Slice(0, view.Length) — the same trick slice<T>’s ISlice<T> constructor uses; Source cannot serve, because a slice header’s Source deliberately materializes a detached copy (only array<T>’s is raw).
Canonicalizing array<T> to its backing is sound precisely because Go’s by-value array COPY is emitted as golib’s .Clone() (see Slices and Arrays), giving the copy its own backing — two distinct Go arrays can never canonicalize to the same storage. Like the PinnedBuffer case, the canonicalization only ever adds true results for same-storage-same-index pairs: distinct backings still compare unequal (&z[0] != &s[0]), distinct indices still compare unequal (&s[0] != &s[1]), and the struct-field and heap-box arms are untouched. This is golib behavior; the emitted code does not depend on it.
One box per element, for an element type that has had a field view. &a[i] mints a fresh
ElemRefBox<T> per expression for most element types; equality is the (backing, absolute index) pair,
so two boxes for one element already compare equal. Once an element type is Slotted — it has had a field
view (BoxShape<T>.Slotted, set by a [GoBoxViews] attribute on T or by the first of(…) asked of any
ж<T>; see
the field-view cache)
— Ꮡ(array<T>, i) answers ONE persistent box per (backing, absolute index) instead, so the field views
cached on that box (&a[i].f) are found again by the next expression rather than re-minted. The boxes live
in a ConditionalWeakTable keyed on the backing and grown in 64-slot chunks on the first address taken
inside each chunk, so a dropped array is collected with its boxes and a big array addressed once pays one
chunk, not its length. Only the object count changes, never Go’s semantics. (Guarded by GolibTests’
ElemBoxPersistTests: identity across expressions and all three index overloads, the view cache hitting
with zero bytes allocated, Go’s aliasing and equality, the chunk cost, a type with no field view keeping
fresh boxes, and a dropped backing being collected.)
(Guarded by the SlicePointerIdentity behavioral output test — self identity, distinctness, re-slice and re-slice-of-re-slice aliasing, the in-capacity append result, array-vs-slice-over-that-array, struct elements, a write through an element pointer observed through both views, and map[*int] store/overwrite/lookup/miss including a lookup keyed through a different window — vs go run. Counter-proven: pre-fix every identity assertion printed false, both map lookups returned empty, and len(m) grew from 2 to 3 on the overwrite.)
The same “a box is a temporary, the storage is the object” reasoning answers lifetime questions —
when the referent dies, and whether two boxes name the same allocation — which runtime.SetFinalizer
and sync.Cond’s copy detector both depend on, and which is also why Ꮡ(IArray<T>, index) must take
its target by value: see
A pointer’s REFERENT, not its box, answers every lifetime and identity question.
A value of a NAMED pointer type (type itemPtr *item) is already a pointer value, so it compares as
written: ip == itemPtr(pi) emits ip == new itemPtr(pi). Deciding “already a pointer” by testing
the type itself for *types.Pointer would miss it, since a named pointer is not one — its UNDERLYING
is — so the operand would render in pointer context as Ꮡip, the box of a value local that does not
exist (CS0103). The test reads the underlying. A parameter or a heap-escaping local renders its box. (Guarded by
the NamedPointerEquality behavioral output test — same-pointer equality, two
distinct allocations, nil, != and self-comparison, against Go.)
A pointer’s nilness and identity are STRUCTURAL — the IsNull / IsNilPointer split
ж<T> answers two different questions that a single predicate used to conflate, and the conflation was a defect class: IsNull was m_isNull || m_val is null, so it reported true for three unrelated things —
-
THE nil pointer (
m_isNull: nil-constructed, or the canonicalNilBox) — the only one that is actually nil; -
a real address whose reference-typed pointee is legitimately nil —
&iwithi == nil,new(any), a closure-capturedp *Tlocal (ж<ж<T>>). In Go these are ordinary non-nil addresses, and*pyields the nil value rather than panicking; -
a struct-field or array-element reference box over a reference-typed
T—&s.next,&elems[i]— whose storage lives in the referenced struct/array, leavingm_valan unuseddefaultthat reads as null for any reference type. A perfectly valid address, misread as nil purely because of where its storage is.
IsNilPointer (m_isNull) is the STRUCTURAL predicate, and everything about pointer identity keys off it — equality, GetHashCode, PointerOrderToken, the reflection bridge’s IsNil/Elem, and the dereference guard on operator ~. IsNull keeps only case 2 (case 3 is excluded structurally, as is a native address, for the same reason), and is consulted only where peeking at the value is the actual question: the strict Value getter, PinnedBuffer, and the uintptr/void* address conversions — where a reference-typed pointee has no reportable address at all, so 0/null is the only answer the managed model can give, and for the value-typed pointees that actually cross into native code the two predicates coincide (unsafe.Pointer’s pointee is uintptr, so every IsNull in that class is the structural question).
Consumers of the structural predicate: operator ~ (both the ж<T> and the IPointer<T> interface form) guard on the structural predicate and read ValueSlot, so *p on a real address whose pointee is nil yields nil — and a field-reference deref reads the field instead of throwing; DerefOrNil/IsNilStandardPointer likewise, so the pointer-walk re-alias hands back the real slot (a write through it persists) rather than the throwaway; and the reflection bridge’s interface-routed slot read (GoReflect.ReadPointerSlot → IGoReflectPointer.ReadReflectSlot) and deepValueEqual’s cycle-detection identityRoot ask INilPointer.IsNilPointer instead of the value-peeking property (which would drop &i-shaped values out of cycle detection).
Two identity rules follow from it. (a) A standard heap box’s identity is never derived from the value it holds: for a reference-typed T that would make two distinct boxes wrapping one referent compare equal — &c == &d true for two distinct *int variables holding the same pointer, map[**int]V{&c: …, &d: …} collapsed into a single entry, and a pointer’s hash mutating when its pointee is assigned, so a key inserted while its pointee was nil could never be found again. A pointer’s identity is its storage: a standard box is the storage, so it hashes and compares by its own identity, and &x == &x holds because an addressed Go variable is heap-boxed once. (b) Conversely, two boxes aliasing the same native address (m_nativeAddr) are the same pointer — a uintptr round-trip mints a fresh box each time, and Go requires (*T)(unsafe.Pointer(p)) == (*T)(unsafe.Pointer(p)).
This is golib behavior; the emitted code does not depend on it. (Guarded two ways, because the converter routes every reference-typed-pointee deref through .ValueSlot — verified with a 6-shape probe including generics — so operator ~ is unreachable from converted Go and only golib-internal, hand-owned and reflection-bridge code takes it. The Go-expressible half is in the PointerToNilPointerIdentity behavioral output test: distinct-variable identity, map[**int] two-key distinctness, hash stability across a pointee assignment, and reference-typed field-reference deref + map keying — pre-fix &c == &d printed true, the two-key map held 1 entry, and the stable-key lookup read back empty. The unreachable half is in GolibTests.PointerNilPredicateTests, which drives operator ~ (both forms), DerefOrNil, ReadPointerSlot through a hand-written stand-in for a generated named-pointer wrapper, and native-alias identity — 7 of its 10 assertions fail pre-fix.)
Reading a pointer and taking a field pointer allocate NOTHING — the two costs hidden inside ж<T>
Go’s *p and &x.f are free, and in go2cs neither allocates either. Each has a hidden cost inside ж<T> that the box must avoid, and the bill shows only where something counts it: os.TestWriteStringAlloc bounds f.WriteString(s) at zero allocations, and a byte-exact decomposition of that call (markers around every frame of WriteString → File.Write → poll.FD.Write → syscall.Write, arithmetic closing to the byte) found most of its cost in these two places, not in the defer machinery.
1. IsNull must not box the whole pointee on every dereference — the larger share. Value’s standard-box branch guards on IsNull, whose last term is the value-peeking m_val is null (case 2 of the split above — a real address whose reference-typed pointee is legitimately nil). On an unconstrained type parameter is null compiles to box !T; ldnull; ceq, so a term that is constant-false for every struct T would still allocate and memcpy a full copy of the pointee, on every read. A pointer to a large record would pay its own size per dereference: os.file is several hundred bytes, and the write path walks eight of() links, each bottoming out in one of these. The term is guarded by a per-T s_valueCanBeNull (!typeof(T).IsValueType || Nullable.GetUnderlyingType(typeof(T)) is not null), computed from the type rather than by boxing default(T), so type initialization allocates nothing either. The guard also makes the peek read the RIGHT storage: a T containing no references keeps its value in the pinnable m_slot and leaves m_val the unused default, so an unguarded m_val is null would answer for the wrong slot (every ж<Nullable<T>> would report nil — unreachable from converted code, since Go has no Nullable).
2. of(…) must not mint the untyped accessor wrapper per CALL — the smaller share. of<TElem> stores an object-taking wrapper around the typed field accessor. The wrapper closes over nothing but that accessor, so it is a pure function OF it — and the accessor is a static method group, which the compiler already caches to a singleton. Minting the wrapper per call would buy a fresh display class plus a fresh delegate for a value identical every time, on every &x.field. It is memoized per accessor in a ConditionalWeakTable; the keys are weak, so an accessor that is genuinely per-call leaves no permanent entry. Pointer equality is unaffected — it compares the field IDENTITY token (the original accessor), not the wrapper.
The same two costs would otherwise be paid by every pointer read and every field address in every converted package. The rest of os.File.WriteString’s cost lies elsewhere: the ж<T> boxes themselves, the syscall seam’s unsafe.Pointer/heap boxes, the defer machinery (the display class and delegate of each capturing defer) and the unsafe.StringData pin.
Neither changes the emitted code. (Guarded by GolibTests.PointerDereferenceAllocationTests: four measured-byte assertions plus a semantics pair — three dereference shapes that must allocate nothing, and of(…) against a bare box of the same type, stated as a COMPARISON rather than a byte count so it survives any future change to ж<T>’s layout.)
A pointer parameter whose every use is a dereference is a ref parameter — the ж-box ref-lowering
The emitted-form rule: an unexported package-level function’s pointer parameter whose every body use is a dereference (*p, p.f, p[i], range p), a derived address feeding another lowered position (&p.f → a lowered argument), or a forward into another lowered position, emits as a C# ref T parameter instead of the boxed ж<T> — and every call site passes a ref expression instead of minting or carrying a box. A ref T argument is an alias into the caller’s storage the GC tracks and updates, so no pinning, no box, no allocation, and writes through it land in the caller’s storage by construction. The signature reads as Go’s *T, ref reads as Go’s &, and the entry deref preamble disappears because the parameter is the alias:
func p224Sub(out1, arg1, arg2 *p224MontgomeryDomainFieldElement) { ... }
p224Sub(&e.x, &t1.x, &t2.x)
internal static void p224Sub(ref p224MontgomeryDomainFieldElement out1, ref p224MontgomeryDomainFieldElement arg1, ref p224MontgomeryDomainFieldElement arg2) { ... }
p224Sub(ref nonnil(ref e).x, ref nonnil(ref t1).x, ref nonnil(ref t2).x);
What disqualifies (the whitelist argument — any use the classifier does not positively recognize keeps the box): pointer identity or nilness (p == nil, map keys), escapes (returned, stored, captured by a closure or a defer/go argument frame), representation observations (unsafe.Pointer(p), uintptr(p), interface conversions, a method call ON p), re-pointing (p = q), and function-identity escapes (exported functions, func-value uses, //go:linkname registry membership, named pointer types, bodiless assembly stubs, declaration in — or a curated call from — a [module: GoManualConversion] hand-owned file). Blank/unnamed pointer parameters are never candidates (no uses, nothing to gain, and the boxed path owns the synthesized-name conventions). The fixed point is two-sided: a call site whose argument shape has no ref emission row (including the tuple-splat f(g()) form) strips the position rather than dead-ending emission.
The call-site emission rows (each self-checks and falls back to the boxed emission wrapped .DerefOrNull() — total over classifier-admitted shapes without coverage ever being a soundness premise):
| Go argument | lowered emission |
|---|---|
&e.x / &p[i] (base is a pointer’s deref alias or a lowered ref param) |
ref nonnil(ref e).x / ref nonnil(ref p)[i]
|
&x.f / &s[i] (value-rooted base: local, value param, global, slice) |
ref x.f / ref s[i] — nonnil elided, the base cannot be null |
&x (address-taken local/param/result — reverted or kept-box) |
ref x (the plain local, or the entry ref alias into the surviving box — same storage either way) |
| a pointer variable/field/deref/assert (carries a box) |
ref (q).DerefOrNull() — reads the box at CALL time, so a re-pointed pointer is never stale |
a lowered ref parameter forwarded |
ref p — it already is the ref |
(*T2)(&v.x) (the named-array-wrapper reinterpret — §10.3’s hoisted-temp rule) |
var ᴛ1 = nonnil(ref v).x.Value; … ref ᴛ1 — the wrapper’s Value yields its array<T> header, a copy whose T[] backing is SHARED, so element writes flow through and whole-header writes are lost in both emissions equally (byte-parity with the old Ꮡ((Ꮡv.of(…)).Value.Value) form). Go requires identical underlying types for pointer conversions, so the wrapper family closes under .Value reads and single user-defined conversions; anything else (e.g. a named-SLICE reinterpret) keeps the boxed fallback |
&T{…} composite literal |
var ᴛ1 = new T(…); … ref ᴛ1 — observationally identical to a distinct heap box, since a lowered callee can never compare, store, escape or convert the address |
the literal nil
|
ref ((ж<T>)default!).DerefOrNull() — binds the null ref; the callee’s first use faults with Go’s panic |
Address-taken locals revert for free. A local (or value parameter, or named result) whose EVERY address-connected use feeds a lowered position — directly, outside defer/go, outside any closure — loses its heap() box entirely: the declaration reverts to a plain local, removing two counted objects per unmanaged local (the box and its eager pinnable slot). Any surviving box use (a stored address, a closure crossing, a pointer-receiver method) keeps the box, and the lowered sites alias the same storage through the entry ref alias. The reversion also collapses the per-iteration loop-variable boxing scaffold where the loop var’s address only feeds lowered positions (ForVariants).
The nil rule (design §10.4). Go panics eagerly at &e.x when e is nil — before the callee is entered. A naive null byref would instead let the callee run side effects Go never runs and let a callee recover catch a panic it can never catch. Lowered field/element address formation over a nullable base (a pointer’s deref alias — null exactly when the pointer is nil) is therefore eagerly checked by golib’s nonnil(ref e) — one branch, zero allocation, throwing the exact panic ж<T>.Value raises — and elided where the base provably cannot be null (a value local/parameter/result, an addressed global’s ref property). A plain nil pointer ARGUMENT (f(q) with nil q) still enters the callee and faults at first use, exactly as Go. Measured gc subtlety recorded with the guard: Go evaluates sibling function CALLS among the arguments in lexical order before non-call operands like &e.x, so “later arguments unevaluated” holds only for non-call operands.
defer f(&x) / go f(&x) are boxed sites, categorically. The defer/go machinery stores eagerly-evaluated argument values in a frame, and a managed ref cannot be stored there (the compiling alternative — a copy-box — silently loses writes) . The eager arguments keep the boxed emission, the statement always takes the temp-param lambda form (a ref-parameter method group cannot convert to Action<…>), and the thunk derives each ref at invoke time: defer(ᴛ1 => setErr(ref ᴛ1.DerefOrNull()), Ꮡerr, ref ᒐ); — preserving Go’s defer-time argument evaluation. An address flowing to a lowered position under defer/go keeps its box (the locals carve-out), and an address-carrying use of a candidate’s OWN parameter inside a defer/go argument frame vetoes that parameter.
Determinism across emissions: classification reads only the production package’s own files — never _test.go — so the -stdlib and -tests emissions of production sources agree by construction (a white-box export_test.go func-value alias cannot un-lower what -stdlib lowered; unit-guarded).
Effect on crypto/internal/fips140/nistec/fiat: it transpiles with no .of( field-ref argument feeds, and the only heap( sites left are the fixed-size byte buffers of Bytes/SetBytes. (Guarded by the RefLoweredParams behavioral test — write-through, forwarding chains, the mixed kept/reverted local, the defer/go carve-out in all three observable directions, the func-value exclusion — and RefLoweredNilTiming — the eager-panic differential in three nil spellings plus the deferred-fault half, all output-compared against go run. The classifier and its fixed point are unit-guarded in refLoweringAnalysis_test.go; the corpus-wide census instrument is -ref-census.)
The lowered emission, row by row — the seven argument shapes in emitted code
The seven argument shapes, each with its emitted form and the golden that pins it. Every snippet is verbatim from a committed .cs.target; only the current form is stated here.
| # | Go argument | boxed emission | lowered emission | golden |
|---|---|---|---|---|
| 1 |
&e.x — field of a deref’d parameter or receiver (a nullable base) |
Ꮡe.of(T.Ꮡx) — 1 box |
ref nonnil(ref e).x |
RefLoweredParams, GenericReceiverFieldAddress
|
| 2 |
&x — an address-taken local, value parameter or named result |
Ꮡx, the heap() box minted at the declaration |
ref x — the plain local; the box and its eager T[1] slot are gone |
ForVariants |
| 3 | a pointer variable/field/deref/assert q — it carries a box |
q |
ref (q).DerefOrNull() — read at CALL time, so a re-pointed pointer is never stale |
PointerParamNilWalk, PointerFieldArrayElementAddress
|
| 4 |
&s[i] / &x.f over a value-rooted base (local, value param, global, slice) |
Ꮡ(s, i) — 1 box + 1 interface temp; Ꮡx.of(T.Ꮡf) for the field form |
ref s[i] / ref x.f — nonnil elided, the base provably cannot be null |
AddressOfParamWrite, PointerFieldArrayElementAddress
|
| 5 |
(*T2)(&v.x) — a pointer conversion over a generated named-array wrapper |
Ꮡ((Ꮡv.of(…)).Value.Value) — 2 boxes |
hoisted temp: var ᴛ1 = v.x.Value; … ref ᴛ1
|
NamedArrayWrapper |
| 6 | a non-variable pointer expression — &T{…}, new(T), any call result |
Ꮡ(new T(…)) / carries the returned box |
hoisted temp, same shape as row 5: var ᴛ1 = new T(…); … ref ᴛ1
|
RefLoweredParams |
| 7 | the literal nil
|
default! |
ref ((ж<T>)default!).DerefOrNull() — binds the null ref; the callee faults at first use |
GuardedNilPointerParamDeref |
A lowered parameter forwarded into another lowered position is ref p — it already is the ref. Rows 5–7 share one justification: a lowered callee can never compare, store, escape or convert the address, so a caller-side temporary is observationally identical to a distinct heap box.
Row 1 — the parameter is the alias, and it survives generic instantiation (GenericReceiverFieldAddress; the callee’s ж<T> Ꮡp box and its DerefOrNull() preamble are gone, and the caller’s 128-byte-per-evaluation field box becomes free):
internal static void setT<T>(ref T p, T val) {
p = val;
}
public static void Set<T>(this ж<Box<T>> Ꮡb, T val) {
ref var b = ref Ꮡb.DerefOrNull();
setT(ref nonnil(ref b).v, val);
}
Row 2 — an address-taken local comes home from the heap (ForVariants; two counted objects per unmanaged local removed, and the per-iteration boxing scaffold of a labeled loop collapses to one plain loop variable):
nint i = 0;
while (i < 10) {
f(ref i);
i++;
}
internal static void f(ref nint y) {
fmt.Print(y);
}
Row 3 — the callee lowers, the call site unwraps (PointerFieldArrayElementAddress; c comes from .at(…) indexing and so still carries a box — each function makes its own deal and the convention change composes across the boundary):
internal static void bump(ref cycle c) {
c.n++;
}
internal static void viaParam(ж<rec> Ꮡp, nint i) {
var c = Ꮡp.at(rec.Ꮡfuture, i);
bump(ref (c).DerefOrNull());
}
The same row, dereferenced per call rather than bound once, is what keeps a reassigned pointer honest (PointerParamNilWalk, whose walk loop emits advance(ref (Ꮡp).DerefOrNull())); note also what does not lower there — a pointer escaping through a return keeps its box identity, so advance’s (ж<node>, nint) result is unchanged.
Row 5 — two boxes become one temp (NamedArrayWrapper; the wrapper’s Value yields an array<T> header whose T[] backing is SHARED, so element writes flow through and whole-header writes are lost in both emissions equally — byte-parity, not a new behavior. Type-gated by refConvPairingSupported to the identical-underlying-array family; a string or numeric wrapper’s value is a plain copy and keeps its identity box end to end):
scal sm = new();
var ᴛ1 = sm.s.Value;
fromBytes(ref ᴛ1, 7);
var ᴛ2 = (nonMont)((sm.s).Value);
@double(ref sm.s, ref ᴛ2);
Row 7 — a lowered parameter still accepts Go’s nil (GuardedNilPointerParamDeref; the synthesized argument binds a null box and defers the fault to the first actual use inside the callee, which is Go’s “a nil pointer only panics when dereferenced” timing. RefLoweredNilTiming pins it against go run):
internal static nint digits(nint @base, ref nint invalid) {
...
}
nint c2 = digits(10, ref ((ж<nint>)default!).DerefOrNull());
The counter-examples are guarded beside the lowered ones (RefLoweredParams), so the boundary is itself under test: a parameter compared to nil keeps its box (its identity is observed); one used as a func value keeps it (a method group cannot close over a ref); a defer/go site keeps it and derives the ref at invoke time (defer(ᴛ1 => bump(ref ᴛ1.DerefOrNull()), Ꮡresult, ref ᒐ);); in-lambda call sites are uniformly boxed-fallback wrapped .DerefOrNull(); string/numeric wrapper reinterprets sit outside row 5’s family; and a blank or unnamed pointer parameter is never a candidate.
Pointer-typed globals and double-pointer walks (&head, *pp, ValueSlot)
A package-level global of pointer type whose address is taken — var head *node with pp := &head — is heap-boxed like any addressed global, yielding a double box: ж<ж<node>> Ꮡhead. Three rules make the classic linked-list walk (for pp := &head; *pp != nil; pp = &(*pp).next { … *pp = n }) faithful:
-
One star is ONE deref.
*ppon a**Tyields a*T— a single.Value/.ValueSlothop, never two. A genuine**ppis two nestedStarExprs, each contributing its own hop. Likewise a field read through an explicit single star on a**T—(*outer.ptr).Value— keeps the base pointer-typed after one star, so normal pointer-base field handling supplies the remaining auto-deref:(~(outer.ptr.Value)).Value. -
A deref whose result is still reference-like reads
ValueSlot, notValue. Go’s*ppmay legally yield nil (*pp != nilis the loop condition); only dereferencing that nil panics. golib’s strictValueaccessor nil-checks the slot, so a deref (or boxed-global property) producing a pointer/slice/map/chan/func/interface value routes throughж<T>.ValueSlot— the identical real slot with no nil check — and reads and writes both persist:pp.ValueSlot = nlands in the original global storage. A deref producing a plain value keeps the strictValue(a nil*nodederef must panic, as in Go). The boxed global’s ref-property follows the same split:internal static ref ж<node> head => ref Ꮡhead.ValueSlot;for the pointer-typed global,=> ref Ꮡg.Value;for a value-typed one. -
&globalon an addressed global is the identity box, never a copy.&allm(wherevar allm *mis boxed) emitsᏑallm— the existing box — notᏑ(allm), which would heap-allocate a copy and silently disconnect writes. And&(*pprev).alllink(address of a field behind one explicit star) peels the star and goes through the field-box accessor:(pprev.ValueSlot).of(m.Ꮡalllink)(rule 2 —*pprevyields a pointer).
The full emitted walk:
internal static ж<ж<node>> Ꮡhead = new StaticBox<ж<node>>(default(ж<node>));
internal static ref ж<node> head => ref Ꮡhead.ValueSlot;
for (var pp = Ꮡhead; pp.ValueSlot != nil; pp = (pp.ValueSlot).of(node.Ꮡnext)) {
if ((~(pp.ValueSlot)).val == v) {
pp.ValueSlot = (pp.ValueSlot).Value.next; // *pp = (*pp).next — write lands in real storage
...
This is exactly the runtime’s allm/itabTable shape (for pprev := &allm; *pprev != nil; pprev = &(*pprev).alllink). (Guarded by the GlobalPointerWalk behavioral test — ordered insertion, head/middle removal, and a method call through the pointer global, all via **node writes, output-compared against Go.)
Capturing the address of a heap-boxed local in a closure
A local whose address is taken (&m) is heap-boxed: the converter emits ref var m = ref heap(new T(), out var Ꮡm), where Ꮡm is the box and m is a ref-local alias of Ꮡm.Value. When a function literal captures such a local and takes its address inside the closure, the variable must be referenced through the box, not snapshot-copied. A C# ref-local cannot be captured by a lambda (CS8175), and a snapshot capture (var mʗ1 = m;) would be wrong twice over: it copies the value out of the box (so writes through the captured &m are lost), and the copy declaration is a statement that has nowhere valid to land when the literal sits in an expression position — e.g. a func literal passed as a call argument (run(func(){ use(&m) })) or a local initializer (f := func(){ use(&m) }).
So a heap-boxed local whose address is taken inside a lambda is marked box-ref and the snapshot is suppressed. The box Ꮡm is a plain local (a capturable reference), so the C# closure captures it by reference — matching Go’s capture-by-reference semantics. Inside the closure the converter then renders every form through the box:
ref var n = ref heap(new box(), out var Ꮡn);
run(() => {
set(ref (Ꮡn).DerefOrNull()); // &n → the box Ꮡn (set's pointer parameter is ref-lowered, so the call unwraps it)
Ꮡn.Value.y = Ꮡn.Value.x + 1; // value use of n → Ꮡn.Value
});
// &c.x (value struct field) → Ꮡc.of(box.Ꮡx)
This also covers &m.field (a value-struct field address inside the closure: Ꮡm.of(box.Ꮡfield)). The detection is scoped to the bare &m and value-struct &m.field forms (the ones with a box-ref emission form); an element address &m[i] keeps the existing snapshot path. The behavioral test FuncLitArgCapture guards the call-argument, value-use, field-address, and initializer cases.
A capture that is WRITTEN after the capture point routes to shared storage, not a snapshot
Go closures share the ONE variable with the enclosing function. The value snapshot the converter uses for captured structs/arrays/slices/maps/chans (var tʗ1 = t; hoisted before the lambda, in-lambda references renamed to tʗ1) is therefore only observationally correct while neither side writes the variable after the snapshot point. Once anything does, the snapshot silently diverges — the program compiles and runs, with wrong values:
- a closure’s writes land in a divorced copy:
bump := func() { t.total += 100 }never affectst(probe-proven: Go 106, snapshot 6); - body writes after the lambda’s creation are invisible to a read-only closure:
get := func() int { return t.total }; t.total += 10reads the stale copy (Go 15, snapshot 5); - a deferred literal observes the variable’s registration-time value instead of Go’s final value (
defer func(){ fmt.Println(t.total) }(); t.total = 42— Go 42, snapshot 5); - two closures over one variable each get their own copy, so a writer and a reader stop communicating entirely.
The converter detects written-after-capture per variable during analysis (varShareFacts, one cached scan of the enclosing declaration) and routes such captures to shared storage. Writes counted, conservatively by syntax: an assignment or ++/-- whose target roots at the variable’s own storage (t = …, t.f.g = …, array a[i] = … — but not through a deref, an implicit pointer deref p.f = …, or a slice/map element, which a snapshot copy shares anyway); a pointer-receiver method call on the variable held as a value (Go’s implicit &t); a for t = range clause; an explicit &t anywhere or an uncalled pointer-receiver method value (an alias — later writes through it are syntactically invisible, so it counts at any position, e.g. p := &t; get := func(){…}; p.total = 50); and any of these inside any func literal (the literal may run at any time). A plain body write counts only if positioned after a referencing literal or sharing a for/range loop with one (a later iteration’s write follows an earlier iteration’s creation).
The routing, by variable shape:
// Heap-boxed variable (escaping struct local, aliased int, …) → by-box (boxRefVars):
ref var t = ref heap<Tally>(out var Ꮡt);
t = new Tally(5, "s");
void bump() {
Ꮡt.Value.total += 100; // value use → Ꮡt.Value: writes the ONE box the body reads
}
// Unboxed variable (value parameter; slice/map/chan local, whose copy diverges on
// reassignment) → NATIVE C# capture — no snapshot, no rename; the display class
// shares the local exactly as Go shares the variable:
internal static void probeB1(Tally t) {
void bump() {
t.total += 100; // captures the parameter itself
}
bump();
t.total++; // 106, matching Go
This applies only to genuine closure-body references (a func literal’s body, directly or as a go/defer statement’s literal callee). A go/defer statement’s non-literal callee/receiver expression and its call arguments keep their statement-time evaluation — defer fmt.Println(t.total) still prints the registration-time value, which IS Go’s argument semantics. Read-only-after-capture variables keep the snapshot (observationally identical, zero churn — the vast majority of stdlib captures). A loop-statement-defined variable (for-init or range clause) also keeps it: its per-lambda snapshot approximates Go 1.22’s per-iteration variable, which shared routing would break. A literal’s own parameters/results are not captures and are excluded. (A for-init variable captured by closures gets Go 1.22’s per-iteration copy through a carrier rewrite — see For-clause variables are per-iteration.)
The native route makes the emitted C# read exactly like the Go for the parameter case; the by-box route reuses the box-ref machinery above (including ValueSlot for inherently-heap locals: a captured slice that the closure reassigns emits Ꮡs.ValueSlot = append(Ꮡs.ValueSlot, …) against a materialized heap<slice<T>> box). (bump is only ever called, so it emits as a C# local function — see A func literal that is only ever CALLED emits as a C# LOCAL FUNCTION below; the routing is the same for a lambda.) (Guarded by the ClosureWriteVisibility behavioral test — probes covering boxed/plain × local/param × closure-writes/body-writes-after × plain/defer/go/IIFE/loop-created contexts, plus slice/map reassignment, alias writes, two-closure sharing, and the read-only/defer-argument/range controls that must KEEP snapshot semantics.)
A NAMED RESULT routed to shared storage declares its box too. The defer func(){ hook(written, err) }() idiom is exactly the written-after-capture shape above with the captured variable being a named result — Go’s deferred closure must observe the FINAL named-result values. When the escape analysis marks such a result (an interface-typed result is blanket-marked the first time it is reused on a mixed v, err := … define; a value-type one when &x is taken), the render sites go through the box (Ꮡerr.ValueSlot inside the deferred literal), so the named-result declaration must declare that box too — a plain error err = default!; would leave Ꮡerr undeclared (CS0103; internal/poll SendFile’s deferred TestHookDidSendFile is the corpus case ). A box-backed named result (identHasHeapBox, the same gate plain locals use) declares the box, in three shapes:
-
No defer frame (plain function, or a closure writing the result): the full escaping-local form at the declaration site —
ref var err = ref heap<error>(out var Ꮡerr);— body and bare returns keep reading the plain alias, nested closures read/writeᏑerr.ValueSlot. A value-type result with&xgetsref var x = ref heap(new nint(), out var Ꮡx);, making the write through&xvisible to the bare return. -
namedReturnDeferMode (a function whose results are declared before the frame’s
try): the decls sit OUTSIDE thetry, and the deferred closures that read them are lambdas, which cannot capture areflocal (CS8175) — so the outside line creates only the box (heap<error>(out var Ꮡerr);), thetryre-derives the value alias inside (ref var err = ref Ꮡerr.ValueSlot;, exactly like a deref’d pointer parameter’sref var fd = ref Ꮡfd.DerefOrNull();), and the post-finallyreturn reads through the box:ᒐdone: return (written, Ꮡerr.ValueSlot, handled);(internal/pollsendfile_windows.cs). -
Func-literal sibling (a literal with named results + defer + post-capture writes): same split, except the literal’s body is itself a lambda conversion, so every in-
tryuse already renders through the box — including the explicit-return rewrite’s assignment targets ((var v, Ꮡe.ValueSlot) = pair(n);) — and the literal’s trailingreturn (w, Ꮡe.ValueSlot);reads the box.
The box-read accessor follows the box-ref rule above: .ValueSlot for an inherently-heap result (reading the held reference is not a dereference), .Value for a value-type box. Results NOT escape-marked are untouched — written in the same defer stays a plain local captured natively by the C# closure, which already observes the final value. (Guarded by the NamedResultDeferCapture behavioral test — value + error named results logged by a deferred closure with post-capture writes and bare returns, the &x value-result, the func-literal sibling, and a non-defer closure write; output-compared vs Go, proving the deferred observation of FINAL values.)
A PARAMETER routed to shared storage declares its box too — the third position of the same family (plain locals, named results, parameters). A parameter can be escape-marked without any capture-mode method call: a body-top-level mixed := REDECLARES the parameter object (the spec’s redeclaration rule includes the parameter lists when the block is the function body), so the define walker escape-analyzes it — and an interface-typed one is blanket-marked. When such a parameter is also captured by a closure and written after the capture point, the routing above sends it by-box (Ꮡctx.ValueSlot inside the lambda) — so the parameter prologue must declare that box, or the lambda names an undeclared one (CS0103): database/sql beginDC’s ctx (redeclared by ctx, cancel := context.WithCancel(ctx) after withLock’s closure captured it) and go/types nify’s x, y (swapped by x, y = y, x and redeclared by xorig, x := x, Unalias(x) after the trace defer captured them). paramNeedsHeapBox (and its func-literal analogue funcLitHeapBoxParamIdents) also fires for a box-ref-routed parameter, emitting the exact capture-mode form — the signature takes the incoming value as ctxʗp and the preamble declares ref var ctx = ref heap(ctxʗp, out var Ꮡctx); (inside the frame’s try when the function has a frame, where the box is an ordinary capturable local). Body statements keep reading/writing the plain ref alias — the redeclare emits (ctx, var cancel) = … against it — so both sides hit the ONE box, and a deferred observer sees Go’s FINAL values. The check rides the declaring-ident lookups, so a box-ref’d value RECEIVER (never ʗp-renamed by the signature paths) can never take the param form. (Guarded by the WrittenCaptureParam behavioral test — the beginDC redeclare shape, the nify deferred-observer shape (named result + defer frame), a closure-write read back by the body, the func-literal sibling, and an inherently-heap slice param; all output-compared vs Go.)
A write that ENCLOSES the literal counts as written-after-capture — the self-recursive closure
The write scan above compares positions: a body write counts when it sits after a referencing literal, or shares a loop with one. Go’s standard recursive-closure idiom defeats a pure position test, because the write starts before the literal it contains:
var check func(uint32, []bool) bool
check = func(pc uint32, m []bool) (ok bool) {
…
ok = check(inst.Out, m) && check(inst.Arg, m) // recurses through the variable
…
}
The assignment statement’s position is that of check on its left, which precedes the literal on its right — yet the RHS is evaluated first, so the store to check unambiguously happens after the literal exists. Scored as read-only-after-capture, the capture would take the snapshot path (var checkʗ1 = check; hoisted above the assignment) and every recursive call would invoke the still-null delegate: a NullReferenceException on the first recursion. In regexp’s makeOnePass that is the entire ambiguity check, so ^.$ — and most of the package — would fail to compile. So the scan also counts a write whose syntactic extent contains a referencing literal (w.pos < lit.pos < w.end), which routes the capture to shared storage; check escapes, so it takes the by-box form and the recursion resolves against the box the assignment fills:
ref var check = ref heap<Func<uint32, slice<bool>, bool>>(out var Ꮡcheck);
check = (uint32 pc, slice<bool> mΔ1) => {
…
ok = Ꮡcheck.ValueSlot((~inst).Out, mΔ1) && Ꮡcheck.ValueSlot((~inst).Arg, mΔ1);
…
};
The same edge covers mutually recursive closures (even/odd, each literal enclosed by the write to its own name while reading the other), and it generalizes beyond closures: any write that evaluates a referencing literal as part of itself — t.mutate(func(){ use(t) }) — counts. (Guarded by the ClosureWriteVisibility probes Q1/Q2 — a self-recursive sum and a mutually recursive parity pair; the pre-fix converter compiles both and nil-derefs at the first recursive call.)
A nested closure’s capture snapshot reads the enclosing closure’s snapshot
When a heap-boxed ref-local is used by VALUE (its address is not taken) and captured by NESTED closures, it is not box-ref’d — it is snapshot-copied: the converter declares var mʗ1 = m; before the closure and the closure uses mʗ1, so the uncapturable ref-local m is never referenced inside the lambda. The snapshot chain must be threaded through each level. A capture generated for an inner closure that lands inside an outer closure’s body must read the outer closure’s snapshot, not the enclosing method’s ref-local — the shape testing/fuzz.go’s run closure has, capturing fn := reflect.ValueOf(ff) (a heap-boxed reflect.Value) and spawning go tRunner(t, func(t){ … fn.Call(args) }) from inside itself, where the method-level fn is a ref-local uncapturable inside a closure (CS8175). The guard’s own shape emits it:
ref var p = ref heap<payload>(out var Ꮡp);
p = new payload(vals: new nint[]{1, 2, 3, 4}.slice());
var @out = new channel<nint>(1);
var outʗ1 = @out;
var pʗ1 = p; // outer's snapshot (before the outer closure)
[MethodImpl(MethodImplOptions.NoInlining)] void outer() {
var outʗ2 = outʗ1;
var pʗ2 = pʗ1; // the goroutine's snapshot reads outer's pʗ1, NOT p
goǃ(() => {
outʗ2.ᐸꟷ(pʗ2.sum());
});
}
generateCaptureDeclarations finds the RHS by walking the conversion stack outward past pass-through levels (a go/defer statement’s own enterLambdaConversion, which carries an empty rename map) to the first enclosing lambda that renamed the variable. It skips the capture’s OWN owner state — pendingCaptures is shared across a function’s lambdas, so an outer lambda’s snapshot can be generated while converting an inner func-literal argument (go dnsWaitGroupDone(ch, func(){}), net/lookup.go), leaving the owner’s state on the stack with a rename equal to the name being declared; adopting it would emit a self-reference var fʗ1 = fʗ1; (CS0841). Guarded by FuncLitArgCapture (a heap-boxed struct re-captured in an inner goroutine — CS8175 without the fix — and the go f(x, func(){}) self-reference shape) and by DeferValueFieldPtrReceiver (a defer inside a lambda).
A pointer (or other inherently-heap) local captured by a closure that takes its address needs the box too, but reaches it by a different route. A local of an inherently heap-allocated type — a pointer, slice, map, channel, interface, or func — is already a reference, so it normally gets no heap box (the convertToHeapTypeDecl path returns nothing for such types). But when one is captured by a closure that takes its address (mToFlush := &node{…}; run(func(){ prev := &mToFlush; … *prev = mToFlush.next })), the closure needs a shared box so writes through &mToFlush inside it reach the outer function’s storage. The converter detects this as the same box-ref mark used above (an inherently-heap local whose address is taken inside a lambda), and for a box-ref local it emits the heap box even though the type is inherently heap — ref var mToFlush = ref heap<ж<node>>(out var ᏑmToFlush) — so the box ᏑmToFlush (a ж<ж<node>>, i.e. a **node) exists for the closure to reference. Without it the closure would emit ᏑmToFlush for &mToFlush against a never-declared box (CS0103); a same-function &ptr with no closure takes the Ꮡ(ptr) copy form (a copy is fine there — no shared storage is needed).
Reading such a box needs care, because for a box-of-pointer the held value can legitimately be nil while the box itself is a real allocation. Ꮡm here is a ж<ж<node>> (a **node), so Ꮡm.Value reads the held pointer value — not a dereference of Ꮡm — and in Go reading *(&p) when p is a nil *T/slice/map yields the nil value, with no dereference and no panic. The strict ж<T>.Value getter (which panics on a null stored value by design, so a genuine *p on a nil pointer still throws) would wrongly panic on that read. So the converter emits the golib ж<T>.ValueSlot accessor for these box-of-pointer reads — identical to .Value but without the nil-pointer-dereference check, returning the real slot so reads and writes both persist (and unlike the retired DerefOrNil, which yielded a throwaway slot for a genuinely-nil box). ValueSlot is selected here for a box-ref local of inherently-heap type; a deref’d pointer parameter reaches the same slot through DerefOrNull(), whose non-nil path IS ValueSlot — see The THREE deref accessors of ж<T>. The heap(out …) / heap(target, out …) helpers likewise return ref pointer.ValueSlot: a freshly allocated box is structurally non-nil, so the getter’s nil check there is always spurious (identical to .Value for a value-type box; it just avoids a spurious panic when establishing the ref var mToFlush = ref heap<ж<node>>(out var ᏑmToFlush) alias). A genuine dereference of the held pointer (the second .Value in ᏑmToFlush.ValueSlot.Value.v) stays strict and still panics on nil — preserving Go’s “panic ⇒ panic” semantics, and complementing the deliberate strict-.Value design at every genuine USE site. (Guarded by the ClosureCapturedPointerAddress behavioral test — a closure that takes the address of a captured pointer local, walks a linked list by reassigning through that address and mutating each node, with the outer function observing both the reassignment-to-nil and the persisted mutations, proving the box is shared rather than copied. Mirrors runtime’s trace.go mToFlush := allm; systemstack(func(){ prev := &mToFlush; … mToFlush = mToFlush.next }), which failed CS0103.)
A pointer-receiver method called through a FIELD of such a boxed pointer local, inside the closure, field-refs through the held pointer, not the box. The receiver of c.flushGen.Store(…) (runtime mcache.go’s allocmcache, inside systemstack) is taken via the &-machinery, and inside a lambda the box-ref address form substitutes the capturable box for the uncapturable ref-local alias. For a value-struct local (box ж<T>) and a deref’d pointer parameter (box ж<T> — the Go pointer itself) the bare box is the correct .of() receiver — but a boxed pointer LOCAL’s box is ж<ж<T>>, one level above the ж<T> the field accessor projects from, and feeding it to .of fails inference (CS0411 — the one error that skip-cascaded every package behind runtime ). Such a base declines the bare-box form and falls through to the pointer-variable field arm, whose ident render reads the box the same way every other in-lambda value use does: Ꮡc.ValueSlot.of(mcache.ᏑflushGen).Store(…) — .ValueSlot because reading the held pointer out of the box must not nil-check (the dereference happens in .of, preserving panic semantics), and because that slot IS what the enclosing ref var c = ref heap<ж<mcache>>(out var Ꮡc) alias reads. When such a local is named after its own type (gauge := newGauge()), the accessor’s owning-type name additionally qualifies with the package class (Ꮡgauge.ValueSlot.of(main_package.gauge.Ꮡv)): the enclosing ж<gauge>-declared local stays visible inside the lambda, so the bare type name binds the uncapturable ref-local (CS8175) with no identical-simple-name fallback — the declared type differs from the type name. (Guarded by the ClosurePtrLocalFieldMethod behavioral test — the allocmcache shape: a pointer local written inside a closure and immediately method-called through a value field, read back after the closure, plus the named-after-type variant; output-compared vs Go, proving the write-through and the field-method call both bind the one shared box.)
A deref’d pointer parameter or pointer receiver captured by a closure is box-ref’d the same way, even when only its value is used inside the closure (not its address). Such a parameter is emitted as the box ж<T> Ꮡp with ref var p = ref Ꮡp.DerefOrNull(), and the ref-local alias cannot be captured (CS8175). Inside the closure a value use becomes Ꮡp.Value.field and an address use Ꮡp, so the closure captures the box by reference — matching Go capturing the pointer. (Guarded by the behavioral test PointerParamCapturedInClosure; the runtime captures *maptype / *m parameters this way pervasively.)
A pointer receiver captured by a closure needs an extra step the parameter case does not: the box Ꮡp only exists if the method is emitted direct-ж (the box passed as the receiver, this ж<T> Ꮡp). A normal pointer-receiver method is this ref T p (a value-ref receiver, with the ж<T> companion generated separately), which has no box for the closure to reference. So “the receiver is referenced inside a function literal” is a direct-ж trigger — a fourth one alongside taking a field’s address (&p.field), returning the receiver (return p), and using the receiver as a bare pointer value (p.next = p, p != q). Mirrors runtime’s func (p *_panic) nextFrame() { systemstack(func(){ … p.lr … }) }. A closure parameter that shadows the receiver name resolves to a distinct object, so it does not falsely trigger the promotion. (Guarded by the ReceiverCapturedInClosure behavioral test — receiver captured by an immediately-invoked closure that reads/writes through it, by one that takes a field’s address, and by one that is returned so the box must outlive the call.)
Once a method is direct-ж, its receiver is the box Ꮡc, but the deref’d value alias ref var c = ref Ꮡc.DerefOrNull() is what most uses see. When such a receiver is passed whole as a pointer argument — stackcache_clear(c) in func (c *mcache) prepareForSweep() — the argument must be the box Ꮡc, not the value alias c (a value cannot bind a ж<mcache> parameter → CS1503). A deref-aliased pointer parameter is already handled (it is an identIsParameter), but a direct-ж receiver is not a parameter, so the call-argument conversion recognizes it explicitly and emits the box. (Guarded by the DirectBoxReceiverPassedWhole behavioral test.)
The receiver placed whole into a composite-literal element whose field is a pointer — func (f *_func) funcInfo() funcInfo { …; return funcInfo{f, mod} } (runtime symtab.go; funcInfo’s first field is the embedded *_func) — needs the same box, and is itself a direct-ж promotion trigger (bodyUsesReceiverAsPointerValue’s composite arm): a boxless ref receiver has no Ꮡf to place in the field (CS1503). Once promoted, the composite renders the box through the existing pointer-field element machinery: new ΔfuncInfo(Ꮡf, mod). Both positional and keyed elements trigger, gated on the field’s declared type being a Go pointer (resolved positionally or by key from the composite’s struct type — the element expression’s own type is always *T for a pointer receiver): a receiver placed into an interface-typed field also typechecks in Go, but that emission compiles, and promoting for it would re-route every such method stdlib-wide (the field gate trims the audit only slightly — the shape is genuinely pervasive: go/types’ Checker methods, net/textproto’s dotReader{r: r}, zstd readers — every audited site the same signature+box re-routing) — its pointer-identity semantics are logged as a separate question. (Guarded by the DirectBoxReceiverPassedWhole extension — positional + keyed composites, identity verified by writing through the wrapped pointer and reading the original.)
The same composite arm also fires when the receiver is stored as an element of a SLICE or ARRAY literal whose element type is a pointer — func (s *UserTaskSummary) Descendents() []*UserTaskSummary { descendents := []*UserTaskSummary{s}; … } (internal/trace summary.go). Without promotion the boxless ref receiver renders the value alias s into a ж<T>[] slot (CS0029); once promoted direct-ж, the element renders the box: new ж<UserTaskSummary>[]{Ꮡs}.slice() (and [2]*T{s, other} → new ж<T>[]{Ꮡs, Ꮡother}.array()). Gated on the slice/array element type being a pointer (the *types.Slice/*types.Array arms of bodyUsesReceiverAsPointerValue), mirroring the struct-field pointer gate. (Guarded by the ReceiverPointerValue extension — the receiver stored into a []*ring and a [2]*ring literal, chain[0] identity verified by mutating through the stored pointer and reading back through the receiver.)
The same pointer-element boxing must also fire for an ELIDED (type-inferred) nested composite — the inner {c} of [][]*Certificate{{c}} (crypto/x509 Verify). The inner literal has no Type node; its inferred element type is *Certificate, and its sole element c is the deref-aliased *Certificate receiver. The typed composite path boxes a bare pointer-typed ident element (argTypeIsPtr), but the untyped-elided slice/array path rendered its elements with a nil context, so that treatment never ran and c emitted the value alias into a ж<Certificate>[] array (CS0029). The elided path supplies a context that boxes a bare pointer-typed ident when the element type is a pointer — new ж<Certificate>[]{Ꮡc}.slice() — returning nil (unchanged nil-context rendering) when the element type is not a pointer or no element is a bare pointer ident, so non-pointer elided literals stay byte-identical. (Guarded by the ElidedNestedPtrComposite behavioral test — [][]*Node{{n}} where n is a pointer receiver.)
A MAP composite literal whose value or key type is a pointer boxes its element the same way — but through convKeyValueExpr (the [key] = value form), not the slice/array element loop above. map[K]*T{k: c} where c is a deref’d pointer parameter renders the value alias c into a ж<T> map slot (CS0029); the map-source branch of convKeyValueExpr sets the isPointer ident context for the VALUE when the map’s declared element type is a pointer, so a bare-ident pointer value emits its box Ꮡc — new map<@string, ж<node>>{["a"u8] = Ꮡa}. A pointer-KEY map (map[*T]V{c: 1}) boxes the key the same way (new map<ж<node>, nint>{[Ꮡa] = 1} — the ж<T> dictionary key matches by box identity). Gated on the map’s declared element/key type being a pointer (not an interface — an interface-valued map still routes through the interface conversion) and the element expr’s own type being a pointer, so a value already rendered as a box (&x, a pointer local) is unaffected. (Guarded by the MapPointerElementLiteral behavioral test — a pointer-value map and a pointer-key map built from *node parameters, aliasing verified by mutating through a stored value and looking up by pointer-key identity.)
Reassigning a pointer parameter to a new pointer. A *T parameter that walks memory by reassignment — bits = addb(bits, n) (a *byte step in the runtime’s bitmap scanners) or p = p.next (a list walk) — cannot write through its value alias: ref var bits = ref Ꮡbits.DerefOrNull() makes bits the pointed-to value, and a pointer RHS (ж<byte>) does not fit it (CS0266/CS0029). The reassignment instead repoints the box and re-aliases the value var — Ꮡbits = addb(Ꮡbits, n); bits = ref Ꮡbits.DerefOrNull(); — reusing the same box-reassignment path that handles a direct-ж receiver’s r = r.prev (the RHS already emits the box form). (Guarded by the PointerParamWalk behavioral test, a circular-list walk that reassigns the parameter and reads the pointed-to value each step.) Reassigning a pointer local (not a parameter) is unaffected — a local already holds the box.
Reassigning the RECEIVER is itself a direct-ж trigger. Go’s pointer receiver is an ordinary local, so a method may repoint it to walk a structure — func (s *fakeStmt) QueryContext(…) { for { …; if s.next == nil { break }; s = s.next } } (database/sql’s fakedb_test.go) — and the rebind is local to the callee. The emission is the receiver twin of the parameter case directly above (Ꮡs = s.next; s = ref Ꮡs.DerefOrNull();), and visitAssignStmt’s arm for it has existed as long as the parameter’s — but it is reachable only through the box Ꮡs, which exists only when the method is emitted direct-ж. So the repoint is itself a trigger: a method whose receiver is repointed but which returns nothing, compares nothing to nil and takes no field address matches none of the other predicates, and without this one would keep the boxless this ref T s and assign a ж<T> into a ref T (CS0029). bodyReassignsReceiver is that trigger, matching by object identity so an inner := that shadows the receiver’s name — a different variable — does not promote the method. The trigger is invisible on the production corpus by construction: every stdlib method that repoints its receiver is also carried by a neighbouring predicate (container/ring’s Move and go/types’ LookupParent return it; math/big’s fmtX, net/http’s addBytes and time’s two (*Location) lookups likewise already emit this ж<T> Ꮡx), which is why the gap first surfaced in a test file — and why adding the trigger moves no production emission. (Guarded by the PointerReceiverRepoint behavioral test — three methods that repoint the receiver to walk a list, one summing, one advancing a bounded number of steps and reading a field afterward, one writing through the receiver at every step so a stale alias would scale the head repeatedly; all deliberately free of every other trigger, with the caller’s head pointer proven unmoved by the callee’s rebind, output-compared vs Go.)
A box accessor’s qualifier names the class that DECLARES the type, which under -tests can be the bridge. The accessor Type.Ꮡfield in receiver.of(Type.Ꮡfield) package-qualifies whenever a bare name could be shadowed (see Field address of a collision-renamed heap-boxed local). For a type an internal _test.go declares, the production <pkg>_package class is the wrong qualifier — the white-box emission unit is the bridge (<pkg>_internal_test_package), so the reference resolves to nothing (CS0117). database/sql’s fakedb_test.go is the corpus instance and it is forced into the qualifying path: type table struct { mu sync.Mutex; … } sits beside func (db *fakeDB) table(string) (*table, bool), so the type is Δ-renamed, and Δ-renamed always qualifies — every one of the six t.mu.Lock()/Unlock() sites would emit sql_package.Δtable.Ꮡmu. The qualifier resolves through packageScopeClassName, the same helper that already draws the production/bridge line for package-level value references, so both halves stay addressable from the one bridge file. Production conversions are unaffected by construction — outside -tests there is no class override and the helper returns the production class. (Guarded by the TestTestVariantBoxAccessorNamesBridgeDeclaringClass converter test, which asserts both directions: the bridge class appears and the production class does not.)
Reassigning a captured pointer parameter inside a closure. The repoint-and-re-alias above (Ꮡp = …; p = ref Ꮡp.DerefOrNull();) rebinds a ref-local. Inside a CLOSURE that captured the parameter that is illegal: the re-aliased value var is an ENCLOSING ref-local, and C# forbids referencing an outer ref local inside a lambda (CS8175 — crypto/x509 buildChains’s considerCandidate closure does if sigChecks == nil { sigChecks = new(int) } on the captured *int parameter). The box reassignment Ꮡp = … is legal (it writes the captured box field, hoisted to a closure field), so only the ref-local refresh is dropped inside a lambda:
if (ᏑsigChecks == nil) {
ᏑsigChecks = @new<nint>(); // was: … ; sigChecks = ref ᏑsigChecks.DerefOrNull(); (CS8175)
}
ᏑsigChecks.Value++;
Every in-lambda and post-lambda dereference of a repointed captured pointer routes through the box Ꮡp.Value, so the now-stale value alias is never read — an accepted modeling gap (like the nil-terminated walk’s), not a miscompile. The suppression is sound because no LEGITIMATE re-alias ever occurs inside a lambda: a lambda’s OWN pointer parameter is passed as the box ж<T> (never deref-aliased), and a heap-boxed value local is written THROUGH its box (Ꮡb.Value = …, never box-repointed). Guarded by ClosureReassignsPtrParam (a closure that reassigns a captured *int parameter; a non-nil runtime argument keeps the reassignment branch unreached so output stays deterministic).
The same repoint-and-re-alias applies when the parameter is reassigned from a tuple — (left, x, idx) = binarySearchTree(x, idx, n/2) (runtime mgcstack.go) or pp, _ = pidleget(0) (proc.go). Box-reassignment triggers that matched the RHS element-wise would never fire for a tuple deconstruction (one call RHS, several LHS) — the ж
Assigning nil to the parameter itself is a box repoint, not a write to the pointee. Both triggers above gate on the RHS being pointer-typed, and the untyped nil literal has no type of its own — so on its own p = nil would miss them, render against the deref’d value alias, and emit p = default!, which zeroes the pointed-to struct while leaving the box Ꮡp non-nil: the caller’s != nil would still pass and it would walk a wiped-out object. This is regexp’s makeOnePass, whose p = nil (the “not one-pass after all” bail-out) would hand compileOnePass an onePassProg with an emptied Inst slice instead of a nil pointer — an index-out-of-range in cleanupOnePass on every pattern the one-pass analysis rejects, which is most of them. A nil RHS is treated as pointer-valued whenever the corresponding target is pointer-typed, so it takes the ordinary repoint-and-re-alias form (nil-DEFERRING, as every repoint is):
// regexp/onepass.go — makeOnePass
if !check(pc, m) { p = nil; break }
…
if p != nil { for i := range p.Inst { p.Inst[i].Rune = onePassRunes[i] } }
if (!check(pc, m)) {
Ꮡp = default!; p = ref Ꮡp.DerefOrNull(); // the POINTER goes nil; the pointee is untouched
break;
}
…
if (Ꮡp != nil) { foreach (var i in range(p.Inst)) { p.Inst[i].Rune = onePassRunes[i]; } }
A pointer local assigned nil is unaffected — a local already is the box, so p = default! is correct there. (Guarded by the PointerParamNilWalk extension dropIfShort — nils the parameter, returns it, and the caller then proves the original node’s value survived; the pre-fix converter compiles it and reports a non-nil result with a zeroed pointee.)
Nil-terminated walk. A pointer-parameter walk that stops at a nil terminator — func sumList(p *node) int { for p != nil { total += p.val; p = p.next } } — needs two extra pieces, modeled together:
-
Compare the box, not the value alias. The loop guard
p != nilmust emitᏑp != nil(the box). Each binary operand’s pointer context is otherwise taken from the other operand’s pointer-ness, andnilis not a pointer type — so the param would convert in value form (p != nil, comparing anodestruct value, the wrong thing). The converter forces the box form for a deref’d pointer parameter in a==/!=comparison. This is safe only for a parameter: a pointer local is already the box, and forcing it would emit a non-existentᏑlocal. -
Nil-deferring re-alias. On the final step
p.nextis nil, soᏑp = p.nextrepoints the box to nil; re-aliasing through the plainᏑp.Valuegetter would then throw a nil-pointer dereference before the guard is re-checked. The deref/re-alias instead routes through the golibж<T>extensionᏑp.DerefOrNull(), which bindsUnsafe.NullRef<T>when the box is nil — legal to HOLD, faulting only on USE — rather than throwing at the bind. The entry alias uses it too, so an empty-list call (sumList(nil)) binds without faulting at entry.
internal static nint sumList(ж<node> Ꮡp) {
ref var p = ref Ꮡp.DerefOrNull();
nint total = 0;
while (Ꮡp != nil) {
total += p.val;
Ꮡp = p.next; p = ref Ꮡp.DerefOrNull();
}
return total;
}
DerefOrNull() is not a substitute for a genuine dereference: reading or writing *p on a nil pointer (~Ꮡp / Ꮡp.Value) still panics, preserving Go semantics — and so does a read THROUGH the bound null ref, which is the whole point. Neither piece needs a predicate: piece 1 (the box-form comparison) is a property of the expression, and piece 2 is what EVERY pointer entry alias and repoint emits, because a repoint is not a dereference in Go and a nil argument is not an error in Go (see A pointer PARAMETER is nil-deferring for exactly the reason a receiver is). (Guarded by the PointerParamNilWalk behavioral test — a nil-terminated sum, a mutate-through-the-parameter pass, and an empty-list call — plus DeferTypelessReturns’ deferred nil-argument call. PointerParamWalk covers the never-nil circular walk.)
A package-level global referenced inside a closure is not captured at all — it is a C# static, accessed live. A value snapshot (var gʗ1 = g) would copy the struct (so &gʗ1 has no box → CS0103, and writes through the global from inside the closure would be lost) and is semantically wrong, since Go reads/writes the live global. For an address-taken (heap-boxed) global the closure references the static box Ꮡg directly — a method call routes as Ꮡg.method() and a field address as Ꮡg.of(T.Ꮡfield). (Guarded by GlobalCapturedInClosure; the runtime does this in every systemstack(func(){ … mheap_ … }).)
A func literal that is only ever CALLED emits as a C# LOCAL FUNCTION
A C# lambda that captures anything allocates two heap objects every time the lambda expression is evaluated: a display class holding the captured variables, and a delegate bound to it. That is charged per call of the enclosing function, whether or not the closure is ever invoked. Go allocates neither when its escape analysis proves the closure does not outlive the frame, which is why time’s TestUnmarshalTextAllocations asserts want 0 allocs, and why parseRFC3339’s parseUint := func(…) was charged to that row.
A name := func(…){…} whose variable is only ever the callee of a call is therefore emitted as a C# local function instead:
// Go: ok := true
// parseUint := func(s bytes, min, max int) (x int) { … ok = false … return x }
var ok = true;
nint /*x*/ parseUint(bytes sΔ1, nint minΔ1, nint max) {
nint x = default!;
…
ok = false; // the SAME `ok` — both sites are rewritten to one struct-closure field
…
return x;
}
nint year = parseUint(s[0..4], 0, 9999);
Roslyn compiles a local function that is never converted to a delegate with a by-ref struct closure: the captured variables move into a struct that lives in the enclosing frame and is passed as a hidden ref parameter. There is still exactly one storage location per captured variable — the enclosing method’s own uses are rewritten to the same field — so sharing, write-visibility and the capture-snapshot machinery are all unchanged. Only the heap objects are gone. The result type is rendered by the same helper visitFuncDecl uses, so a named Go result keeps its /*x*/ comment and a local function reads exactly like a declared one; a single-return literal keeps the expression-bodied collapse (byte num2(slice<byte> bΔ1) => …;).
The “only ever called” proof is what keeps that compilation available, not a convenience: converting a local function to a delegate anywhere makes Roslyn fall back to a heap display class, and a local function has no value form to give a store, a return, an argument or a comparison in the first place. Every reference other than the declaring occurrence must be a call callee — which also subsumes reassignment (f = … is a non-call use of f) and address-taking, so the emitted name can never be required as a first-class value. Three further gates: the statement must be a := define with one LHS ident and one RHS literal (a mixed f, err := … re-use records the name in Uses, not Defs, and binds no fresh object); it must be in statement position, since a local function is a declaration and cannot sit in a for/if/switch init clause; and the enclosing function declaration must be known (a literal inside a package-level var initializer is left alone).
A literal that defers or recovers is no bar: its frame is an ordinary local of the local function, declared in the local function’s own body like any other, so the whole shape stays allocation-free —
nint /*r*/ guard(nint n) {
nint r = default!;
GoFrame ᒐ = default;
try {
defer(() => {
{
var e = recover(); if (e != default!) {
r = -1;
}
}
}, ref ᒐ);
if (n < 0) {
throw panic("negative");
}
r = n * 2; goto ᒐdone;
}
catch (Exception ᒐex) when (GoFrame.IsPanic(ᒐex, out PanicException? ᒐp)) { GoFrame.Capture(ᒐp); }
finally { ᒐ.Run(); }
ᒐdone: return r;
}
Go’s two-step recursion idiom (var f func(int) int; f = func(int) int {…}) is an ASSIGN, not a DEFINE, so it is not this shape at all and keeps the lambda — correctly, since the recursive reference reads f as a value. (Guarded by the LocalFunctionEmission behavioral test: the parseUint shape with a named result and a mutated capture, the expression-bodied collapse, a struct-and-array capture mutated through the local function, two nested levels, and the deferring/recovering literal above — plus four negative controls, one per disqualifying reason: value use, reassignment, the recursion two-step, and argument position. The golden pins the emitted form; the stdout comparison against go run pins the capture semantics.)
A variable DECLARED INSIDE a closure is not captured BY it
The escape analysis heap-boxes a local when something outside its frame can reach its storage; a closure is one such route, because the emitted C# serves the shared variable through a ж<T> box. The closure arm of that analysis skips an object whose declaration lies inside the literal: matching on any mention of the object lexically inside a function literal’s body would treat a literal’s own local as if the literal closed over it, since for a variable declared there that mention is its own declaration. Without the skip:
// Go: testing.AllocsPerRun(100, func() { var t Time; t.UnmarshalText(in) })
Δtesting.AllocsPerRun(100, () => {
ref var tΔ1 = ref heap(new Δtime.Time(), out var ᏑtΔ1); // ← a box, and ᏑtΔ1 is never used
tΔ1.UnmarshalText(inʗ1);
});
The box ᏑtΔ1 would be referenced nowhere in the emitted body — UnmarshalText is a this ref Time extension, which binds the variable directly — while the identical two statements written outside a closure emit a plain Δtime.Time tΔ1 = default!;. With the skip, the emission is that plain local.
The narrowing direction of an escape rule is the dangerous one — an under-box drops writes silently — so the proof is stated rather than assumed. Go scoping puts a literal’s own local out of reach of every other frame, so there is nothing for a shared box to make visible; and every route by which such a local can still genuinely escape is decided by an arm that walks the whole enclosing function body, literal bodies included: &x / &x.f / &x[i] (the address-of arm), a pointer argument (the call arm), a go/defer use (their own arms), a capture-mode method call, and a pointer-receiver method value — Go’s (&x).M written without the &. None of them is lost. The skip also keeps descending rather than stopping, so a literal nested inside the skipped one — which does close over the variable — still gets its own turn through the arm and still marks the escape.
(Guarded by the ClosureLocalNoHeapBox behavioral test. Five of its eight probes are the boxes that must SURVIVE, one per escape route, and each writes through the escaping alias and reads the value back so an over-narrowed rule prints a wrong number rather than merely emitting a different shape; the two positive probes are the pointer-receiver-method and copy-only shapes that emit plain locals. Its N3 probe is the nesting case, and it is also the interaction test with the local-function rule above: the nested literal is emitted as a local function and captures the surviving box.)
Capture-mode methods called through a value field of the receiver
A pointer-receiver method that takes the address of one of its own fields (func (c *Counter) Add(d int32) int32 { return bump(&c.n, d) }) is capture-mode: it is emitted with the heap box as its receiver (this ж<Counter> Ꮡc) so &c.n can field-reference the real storage as Ꮡc.of(Counter.Ꮡn). When another struct embeds such a type as a value field and drives it through that field — func (f *Flag) Incr() int32 { return f.c.Add(1) } — the call needs a ж<Counter> aliasing the real f.c. The enclosing method is therefore itself promoted to capture-mode (direct-ж), and f.c.Add(1) is emitted as (&f.c).Add(1):
public static int32 Incr(this ж<Flag> Ꮡf) {
return Ꮡf.of(Flag.Ꮡc).Add(1); // f.c.Add(1) — nested field-address box
}
The nested Ꮡf.of(Flag.Ꮡc).of(Counter.Ꮡn) chain resolves each level through ж<T>.Value (which honors a parent that is itself a field/array reference), so writes land on the real embedded field rather than a copy. A plain (non-capture) value method called through the same field — f.c.Get() — is left as a normal f.c.Get() value call.
This field-address routing applies only to value fields. When the field is itself a pointer — e.g. cpuProfile’s log *profBuf, accessed as cpuprof.log where cpuprof is a heap-boxed global — its C# value is already a ж<profBuf> box, so a direct-ж method binds to it directly: cpuprof.log.close(). Taking the field’s address (Ꮡcpuprof.of(cpuProfile.Ꮡlog)) would double-box to ж<ж<profBuf>> (CS1929). The heap-boxed-receiver routing recognizes that a field selector or indexed element whose own type is a Go pointer is already a box and skips the &-machinery for it. This discriminates a pointer field of a boxed global (already a box) from a deref’d pointer parameter (s in s.Prev(), a value alias whose box is Ꮡs): the latter is a bare identifier, not a selector/index, so it is correctly still routed through Ꮡs. The same exclusion applies when the pointer field is reached through a pointer local rather than a boxed global — s := sl.mspan; s.gcmarkBits.bytep(…) where s is a *mspan local — which otherwise routed through the pointer-local-field address path (s.of(mspan.ᏑgcmarkBits)); the field value (~s).gcmarkBits is already the ж<gcBits>. (Guarded by the PointerFieldOfBoxedGlobal behavioral test, covering both the boxed-global cpuprof.log.write/.close form and the pointer-local s.log.push form; runtime exercises both pervasively, e.g. mspan.sweep.)
The same applies when the value field belongs to a package global rather than a receiver — ctrl.total.Add(5) where var ctrl controller and total is an atomic field. The method’s box address goes through the field-address machinery, Ꮡctrl.of(controller.Ꮡtotal).Add(5), not a bare Ꮡ prefix on ctrl.total (which would bind to the box variable Ꮡctrl, whose value type has no total member → CS1061). This is the form runtime uses pervasively for gcController, sched, memstats, etc. The method call itself triggers heap-boxing the global: when a pointer-receiver method is called on a (possibly nested) value field of a package value global, the escape pass marks that global address-taken so its box exists — the call site needs the box even when the global is never explicitly &-addressed elsewhere. This is gated on the method being ж-only (a pointer receiver): a same-package method known to be capture-mode, or any pointer-receiver method whose package’s capture-mode set is not locally available — the latter covers cross-package atomic methods (func (x *Uint32) Store), which are likewise emitted with only a box receiver, so a plain value/ref of the field cannot bind them (CS1929). The walk to the global root bails at any pointer hop (a field reached through a pointer already has a real address and is handled by the pointer-local / receiver paths), so a receiver/parameter field such as f.c is never disturbed. (Guarded by the AtomicValues behavioral test’s global-atomic-field case; runtime exercises this for prof.signalLock, trace.seqlock, scavenge.gcPercentGoal, etc.)
It also applies when the receiver is an indexed element of such a field — trace.stackTab[i].dump() (boxed global) — where the element’s address goes through the box-field accessor: Ꮡ(trace.stackTab, i).dump() for a slice field, or Ꮡtrace.of(T.ᏑstackTab).at<E>(i).dump() for an array field. The same routing covers an indexed element of an array/slice reached through a pointer — bh.Value[i].Load(), where bh is a pointer and the element is an atomic value — emitted bh.of(T.Ꮡval).at<E>(i).Load(). This is gated on the called method being direct-ж (a box receiver): an ordinary ref method binds to an addressable element directly, so it is left as container[i].method() and only a direct-ж method (which truly needs the box) is routed — avoiding needless churn on the common case. (Guarded by the IndexedElementDirectBoxMethod behavioral test — a direct-ж method on an array-element-through-a-pointer-parameter, with mutation persistence verified; runtime hits this on mprof’s bh.Value[i].Load()/.StoreNoWB().)
A capture-mode method called on a value local of an inherently-heap type — a named slice/map/chan — also forces the box, which identHasHeapBox otherwise refuses. An inherently-heap type is already a reference, so a var of it is normally not boxed even when it “escapes” (the escape pass marks every inherently-heap local escaping and returns early). But a capture-mode pointer-receiver method — internal/trace/internal/oldtrace’s orderEventList (a named []orderEvent) with heap.Interface Push/Pop that forward the receiver to heapUp(h, …)/heapDown(h, …) — is emitted with a ж<orderEventList> receiver, so a plain value cannot bind it (CS1929 — var frontier orderEventList; frontier.Push(…)). The escape pass therefore records the capture-mode reason in that inherently-heap early-return branch (the only place these vars are seen, before the general address-of scan), and identHasHeapBox honors it — emitting ref var frontier = ref heap<orderEventList>(out var Ꮡfrontier) so the calls route Ꮡfrontier.Push(…)/Ꮡfrontier.Pop() through the box. A named slice/map/chan with no capture-mode method called on it stays unboxed (already a reference — no churn). (Guarded by the NamedSliceCaptureMethod behavioral test — a named-slice value local with *stack push/pop that forward the receiver to helpers, mutated and read through the same box, output-compared vs Go.)
A capture-mode method called on a value PARAMETER boxes the parameter at entry — go/format’s format(…, cfg printer.Config) calling cfg.Fprint(&buf, fset, file), where (*printer.Config).Fprint is transitively direct-ж (its body calls the defer/recover-wrapped fprint on its own receiver), so its only emitted receiver form is the box ж<Config> and the raw value parameter cannot bind it (CS1929 ×2). Parameters are deliberately never fed through the full escape analysis, so the escape pass runs only narrow, named parameter checks (markCaptureModeBoxedParams) rather than the general escape walk — this one being bodyCallsCaptureModeMethodOn, the same predicate the local-var arms use. (The companion check for a plain ¶m is objectAddressTaken — see An address-taken VALUE PARAMETER heap-boxes too.) For a marked param the signature renames the incoming value to the ʗp form (the variadic-prologue rename convention) and the parameter preamble declares the boxed alias:
internal static (slice<byte>, error) format(…, printer.Config cfgʗp) {
ref var cfg = ref heap(cfgʗp, out var Ꮡcfg);
…
cfg.Indent = indent + indentAdj; // body writes hit the boxed storage…
var err = Ꮡcfg.Fprint(…); // …the same storage the callee mutates through the receiver
Entry-time boxing is the load-bearing choice: Go auto-addresses the parameter (cfg.Fprint(…) ≡ (&cfg).Fprint(…)), so a body write before the call (cfg.Indent = …) must be seen by the callee, and the callee’s writes through the receiver pointer must be seen by the rest of the body — while the caller’s argument stays untouched (by-value parameter). A call-site Ꮡ(cfg) copy-box compiles but silently drops the callee’s writes for the rest of the function. An ARRAY param folds its Go by-value clone into the box init (ref var b = ref heap(bʗp.Clone(), out var Ꮡb); — the plain b = b.Clone(); preamble line is skipped), and an inherently-heap-typed param records the capture-mode box reason exactly like the value-local arm above. Beyond this trigger and the address-taken one, a param that leaks into identEscapesHeap some other way — a mixed data, pc, line := … define re-uses the param object, so the define walker escape-analyzes it (debug/gosym’s slice) — keeps its unboxed emission (paramNeedsHeapBox re-verifies the predicate against the declaring ident). (Guarded by the CaptureModeValueParam behavioral test — a defer-promoted direct-ж method plus a transitively-promoted one called on a value parameter, with a pre-call write observed by the callee, callee writes read back after, and the caller’s copy proven untouched, output-compared vs Go — and by the CaptureModeValueParamLib/CaptureModeValueParamUser cross-package pair mirroring the format→printer shape: a foreign Config value param, Fprint → defer/recover fprint transitive promotion, trace accumulation across two calls proving write-visibility through the foreign ж<Config> extension.)
When the same function also contains a func literal or defer that references the boxed parameter, the in-lambda references must route through the box — the capture analysis marks such a param box-ref (the same arm family as a deref’d pointer parameter, whose ref var p = ref Ꮡp.DerefOrNull() alias shares the exact shape). The boxed param’s Go name is a ref-local alias, which a C# lambda cannot capture (CS8175), and the general capture-snapshot fallback (var tʗ1 = t; before the lambda) compiles but divorces the closure from the boxed storage Go shares between the closure and the direct-ж callee: a closure read misses the callee’s writes through the receiver pointer, a closure write is invisible to the callee, and a deferred closure observes entry-time values instead of return-time state. With the box-ref mark, a closure read emits nint get() => Ꮡt.Value.total; (a local function — get is only ever called), a closure write Ꮡt.Value.total += 100;, and a deferred observer defer(() => { (result, log) = (Ꮡt.Value.total, Ꮡt.Value.log); }, ref ᒐ); — the box Ꮡt is a plain ж<T> local, captured by reference, so every reference (body, closure, callee) hits the one boxed storage, matching Go’s one-parameter-variable semantics. A deferred direct-ж method value on the param itself (defer t.Add(n)) routes through the box (defer(Ꮡt.Add, n, ref ᒐ)), binding the receiver address at defer time exactly like Go. The composition is user-code-facing. (Guarded by the CaptureModeParamClosure behavioral test — four compositions with write-visibility checks in both directions: a closure read that must see the callee’s later write, a closure write the callee must observe (and vice versa), a deferred closure reading return-time state, and a deferred method value whose writes a sibling deferred observer reads; each output-compared vs Go, with the caller’s copy proven untouched. Under the pre-fix snapshot emission all four compiled and produced wrong values.)
Entry-time boxing extends to a function literal’s own value parameter — otherwise f := func(t Tally, m int) {…; t.Add(m); …} would render the raw Tally value against Add’s only ж<Tally> receiver form (CS1929). The escape pass marks literal params with the same one-narrow-predicate check as declaration params (walking FuncLit nodes before the define walk, so a mixed t, y := … re-use cannot pre-empt the verdict; a leaked-but-not-capture-mode param keeps its unboxed emission via the same declaring-ident re-verification). The literal’s signature takes the incoming value under the ʗp name and its first block statement is the boxed re-declaration — the exact preamble form, injected before the single-return collapse (which it thereby suppresses, correctly keeping the body a block):
(nint, @string) f(Tally tʗp, nint m) { // only ever called, so a local function
ref var t = ref heap(tʗp, out var Ꮡt);
t.total++; // body writes hit the boxed storage…
Ꮡt.Add(m); // …the same storage the callee mutates
return (t.total, t.log);
}
This applies uniformly to every literal form: an assigned literal, a call argument, a defer func(t Tally) {…}(x) / go … argument-passing target (each deferred/goroutine run boxes its own copy at entry), and — unlike the variadic prologue, which excludes them — an IIFE, whose names-only parameter list emits the ʗp name so the rebinding composes with the delegate cast. A literal with both a variadic tail and a boxed param stacks the two ʗp prologues (variadic slice first, matching the declaration preamble order). A nested closure over the literal’s boxed param takes the box-ref route (never a value snapshot, which compiled but orphaned the callee’s writes — var tʗ1 = t; tʗ1.Add(9) lost both directions of write-visibility), while the literal’s own body keeps the plain ref-alias renders above (t.total++, not Ꮡt.Value.total++): a box-ref var whose declaring literal is the lambda currently being converted renders plain, since its box and alias are locals of that very lambda — only genuinely nested lambdas read through the box. This is user-code-facing. (Guarded by the CaptureModeFuncLitParam behavioral test — assigned, IIFE, deferred-argument, nested-closure, and variadic-composition shapes, each with write-visibility checked in both directions and the caller’s copy proven untouched, output-compared vs Go.)
And it applies when the field belongs to a pointer local — h.s.inc() where h is a *holder local and inc has a pointer receiver. A pointer local holds the box ж<holder> directly, so the value ~ dereference of the field ((~h).s) is an rvalue; the this ref method needs an addressable receiver (CS1510 on the generated ref). The field’s box address is taken instead — h.of(holder.Ꮡs).inc() — binding the ж overload. (A pointer parameter is deref-aliased to a value, so p.s.inc() already works without this and is left alone. This is the form runtime uses for (*c).gp.set(…) / .cas(…) in coro.)
Finally, the same rvalue problem occurs when the field belongs to a pointer reached through another field — o.h.wait.add(…) where o.h is a *holder field and wait is a value (atomic) field. o.h dereferences to an rvalue, so (~o.h).wait is not addressable. The receiver is routed through the box-field accessor o.h.of(holder.Ꮡwait), which aliases the real field storage — not a Ꮡ(value) copy, which compiles but silently boxes a copy so the atomic write is lost (a behavioral bug, not a compile error). Both the explicit address form (&o.h.wait) and a pointer-receiver method call on the field are routed this way. This is deliberately scoped to a base that is itself a field selector: a bare-ident base is the method’s own receiver or a deref’d pointer parameter (both emitted as an addressable ref, so f.c.Get() binds directly — routing them through & would emit Ꮡf.of(…) but a value-ref receiver has no Ꮡf box) or a pointer local (handled above). (Guarded by the AtomicFieldThroughPointer behavioral test — a mutate-then-read proves the real field is updated, not a copy; runtime exercises this for atomic fields reached through pointer chains such as sgp.g.selectDone.CompareAndSwap and gp.m.mLockProfile.recordLock.)
The base may also be a pointer rvalue — a pointer-returning call (getg().schedlink.set(…), q.tail.ptr().schedlink.set(…), Δp.chunkOf(ci).scavenged.setRange(…), getg().m.p.ptr().wbBuf.get2()) or a pointer element index (batch[i].schedlink.set(…)). Go auto-derefs the pointer to reach the value field, so the converter renders the read as (~rvalue).field; the ~ deref is an rvalue, so a pointer-receiver method on it cannot bind (CS1510 on the generated ref). Unlike a deref-aliased parameter (whose box is Ꮡp) or a field deref (handled above), the call/index value already is the ж<T> box, so the receiver is materialized straight through it via the box-field accessor — getg().of(g.Ꮡschedlink).set(…), batch[i].of(g.Ꮡschedlink).set(…) — never a Ꮡ(value) copy (which would lose the write). The routing is scoped to a base that is not an ident and not a field selector (those are the param/receiver/local/field cases above) and is not a type conversion: a conversion (*T)(p) renders as a C# cast ((ж<T>)(uintptr)(…)), a low-precedence form on which a trailing .of(…) would mis-bind to the inner operand, so a pointer-reinterpret keeps its existing Ꮡ(…) form (the runtime-unsafe reinterpret seam). (Guarded by the PointerRvalueFieldReceiver behavioral test — a pointer-receiver method on a value field reached through a returning call, a method-call chain, and a pointer-element index, each with write-through verified; runtime exercises this for guintptr.set via getg()/batch[i]/q.tail.ptr(), pallocData.setRange via chunkOf, and wbBuf.get2/discard via getg().m.p.ptr().)
A TYPE-ASSERTION base is a pointer rvalue too. The shape list
that admits a base into that box-field routing enumerates ident / selector / call / index / star, and a
type assertion is none of them — so without its own arm &c.(*UDPConn).conn (net udpsock_test,
reaching the promoted conn.Write through an asserted PacketConn) would drop to the Ꮡ(value)
copy-box fallback and name a .conn member that ж<UDPConn> does not have (CS1061; had it bound,
it would write into a copy). The list is
about C# precedence, not about which node kinds have happened to come up: a base whose rendering is
postfix chains .of(…) cleanly, and a type assertion always renders as the postfix c._<ж<UDPConn>>(),
which is the box. Only the type-CONVERSION CallExpr stays excluded, for the cast-precedence reason
stated above. exprIsValueFieldOfPointerRvalue, the sibling predicate that decides the routing,
accepts a type assertion through its default arm — so the two agree rather than one
routing a shape the other could not render. (Guarded by the
PointerRvalueFieldReceiver extension — iface.(*node).s.set(55), with the write read back through
the original pointer.)
It has a second consumer: compress/flate’s flate_test.go reaches dict.availWrite() through an
asserted *decompressor, which the copy-box fallback would serve as a copy —
Ꮡ((~r._<ж<…decompressor>>()).dict).availWrite() // copy
r._<ж<…decompressor>>().of(…decompressor.Ꮡdict).availWrite() // the real field
— and a copy survives there only because availWrite is a read. That is this family’s signature
exactly: the copy gives the right answer until someone writes through it.
The bare-ident-base exclusion above holds only for ref methods (which bind on the addressable value alias directly). A direct-ж (box-receiver) method — func (s *scavengeIndex) find(…) and the like, emitted with a ж<T> receiver — needs the box, so calling it on a value field-chain rooted at a deref-aliased pointer parameter or (direct-ж) receiver is CS1929: Δp.scav.index.find(force) (root p, a *pageAlloc receiver), mp.trace.seqlock.Load() (root mp, a *m parameter), h.userArena.readyList.remove(s). These are routed through the box-field accessor too — Ꮡp.of(pageAlloc.Ꮡscav).of(pageAlloc_scav.Ꮡindex).find(force) — never a Ꮡ(value) copy (which would lose an atomic write). The &-machinery recurses through the value field-chain to the param/receiver box: &Δp.scav.index builds Ꮡp.of(…).of(…), where the box base is the raw parameter name (Ꮡp, not the shadow-renamed ᏑΔp — a deref param p→Δp is ref var Δp = ref Ꮡp.DerefOrNull(), box Ꮡp). The routing is gated to direct-ж so a ref method on the same chain keeps binding directly (no churn); a receiver root additionally requires the enclosing method to be direct-ж (only then does its receiver box Ꮡrecv exist). (Guarded by the FieldChainBoxReceiver behavioral test — a direct-ж method on a value field-chain rooted at a pointer parameter and at a direct-ж receiver, both with write-through verified; runtime exercises this pervasively for scavengeIndex/mSpanList/timers methods and m.trace atomic fields.)
For the receiver-root case, the enclosing method only becomes direct-ж through the capture-mode pre-pass’s transitive fixpoint: a pointer-receiver method that calls a direct-ж method on a value field-chain of its own receiver — func (p *pageAlloc) free(…) { … p.scav.index.free(…) } — is promoted to direct-ж so its receiver box Ꮡp exists for the routing above. This detection walks the full value field-chain recvName.f1.…fn.method (every hop a value, non-pointer field), not just one level: p.scav.index.free(…) roots free at the receiver p through two value fields (scav→index). A one-level chain (b.u.Load() on an embedded atomic) is the simplest case; the walk covers any depth. A pointer field anywhere in the chain stops the walk — that subexpression is already a box and roots the call elsewhere (the pointer-field paths above), so it must not trigger promotion. The promotion is transitive: once pageAlloc.free is direct-ж, its caller func (h *mheap) freeSpanLocked(…) { … h.pages.free(…) } is in turn promoted (it calls a direct-ж method on h.pages), and so on up the call graph until a root holding the value through a real box/pointer. (The multi-level receiver-root promotion is covered by the FieldChainBoxReceiver test’s deep.bumpDeep case — d.mid.c.inc(), a direct-ж inc on a two-level value field-chain of a receiver with no other direct-ж trigger, write-through verified; runtime exercises it on pageAlloc.free/freeSpanLocked.)
unsafe.Alignof / unsafe.Offsetof name a TYPE, resolved through go/types
Go defines Sizeof, Alignof and Offsetof against the static type of their operand — and never
evaluates that operand (all three are compile-time constants for any type of non-variable size). golib
matches that shape: Alignof(Type, string? fieldName = null) and Offsetof(Type structType, string
fieldName) take a System.Type, so the converter has to turn the Go operand into a type argument.
It does that from go/types: unsafe.Alignof(x) emits @unsafe.Alignof(typeof(T)) for T the
C# rendering of the operand’s static type — one rule for every operand shape, because Go’s
Alignof(s.f) is the required alignment of the field’s own type, which is exactly what golib’s
two-argument overload resolves to anyway. unsafe.Offsetof(s.f) emits @unsafe.Offsetof(typeof(S),
"f"), where S comes from types.Selection.Recv() with any implicit pointer dereference stripped,
and a promoted field is walked down its embedding chain so the offset is measured against the struct
that declares it (Go’s rule: relative to the immediately enclosing struct). The field name is the
emitted identifier with any keyword escape removed, since reflection sees @out as out.
Reading the shape off the converted C# text instead (splitting on .) would mistake any dotted
rendering for a selector: a conversion operand renders with a leading cast, so unsafe.Alignof(uint32(0))
would become (uint32)0.GetType(), which C# parses as (uint32)(0.GetType()) (CS0030). .GetType() is
also the wrong instrument on its own terms — it reports the dynamic type of a boxed or interface-typed
operand where Go uses the static one, and it evaluates the operand, which Go does not.
Guarded by the UnsafeOperations behavioral
test, extended with a conversion operand, an index operand, a selector through a pointer, a two-level
selector, and a field whose name is a C# keyword; all output-compared vs go run.
This shape is the FALLBACK, not the normal path — expression sites fold to the constant (next
section), and only a variable-size operand still reaches the typeof(T) emission. The promoted-field
rule stated above is also wrong about Go, and the fold supersedes it: Go measures a promoted field
against the operand struct, not against the struct that declares it.
unsafe.Sizeof / Alignof / Offsetof FOLD to a constant at expression sites
Go computes all three at compile time from the operand’s static type, never evaluates the operand,
and yields a typed uintptr constant for any operand type of non-variable size. Declaration
sites emit that constant — internal static uintptr offsetX86HasAVX => /*
unsafe.Offsetof(cpu.X86.HasAVX) */ 66; in runtime/cpuflags.cs. Expression sites emit the same
form, so one Go construct has one behavior:
var hdr Header32
data := make([]byte, unsafe.Sizeof(hdr))
// …
f.Type = Type(bo.Uint16(data[unsafe.Offsetof(hdr.Type):]))
var data = new slice<byte>((nint)(/* unsafe.Sizeof(hdr) */ (uintptr)52));
// …
f.Value.Type = (Type)bo.Uint16(data[(int)(/* unsafe.Offsetof(hdr.Type) */ (uintptr)16)..]);
The value comes from go/types, which folds against the types.Sizes for the loaded target
GOARCH — the Go compiler’s own layout rules — so the emitted number is what the Go program
computes, not a measurement of the emitted C#. The literal keeps its uintptr type because Go’s
constant is typed: a bare number would let uadd := unsafe.Sizeof(*t) infer C# int, and an int
variable has no implicit conversion to golib’s uintptr struct (internal/abi’s FuncType.InSlice hands
it to a uintptr parameter — CS1503). The cast is inert everywhere else: C#’s constant-expression conversion
would have bound a bare literal anyway, and a cast binds tighter than every binary operator, so no site
needs extra parentheses.
Three things this fixes, beyond removing a reflection call from a construct Go settles at compile time:
-
A latent throw. golib’s
SizeofridesMarshal.SizeOf<T>, which throws for a non-blittableT— and a converted Go struct is non-blittable as soon as it holds aslice<T>,@string, interface, orж<T>field.debug/elf’sHeader32holds anarray<byte>(abyte[]inside), so itsunsafe.Sizeof(hdr)answered 56 where Go says 52 (a 4-byte-too-long header read), and the reader’s ownSection/Prog— embedded struct +io.ReaderAt+ж<SectionReader>— throwArgumentException: … cannot be marshaled as an unmanaged structure. -
Semantic fidelity. A reflection answer measures the CLR-marshalled layout, which differs from
Go’s whenever golib’s field representation differs —
stringis 16 bytes in Go/amd64,@stringis a managed struct.debug/elfuses these values as on-disk format offsets; they must be Go’s. -
Promoted fields. Go measures
unsafe.Offsetof(e.count)against the operand struct: withcountat 8 inside an embeddedPaddedthat itself sits at 8, the answer is 16. The reflection form could see only one hop of the embedding chain;go/typesfolds the whole path.
A variable-size operand still emits the run-time form (with a converter warning naming the site),
because Go itself does not fold it: since Go 1.18 the operand may be type-parameter-typed, and the
call is then not a constant. A handful of such sites exist in the stdlib — slices’ overlaps
(unsafe.Sizeof(a[0]) on []E), internal/saferio (unsafe.Sizeof(v) on E), and
runtime/minmax — and they are why golib’s @unsafe
run-time forms are retained rather than
deleted.
Guarded by
UnsafeOperations, extended with all three builtins in call-argument, arithmetic, comparison,
assignment and compound-assignment, and make-size positions, over structs whose Go layout is
padding-, embedding- and array-sensitive (and non-blittable once converted); output-compared vs
go run.
The run-time unsafe.Sizeof answers through Go’s layout rule, not the CLR’s marshaller
Constant folding (above) removes the reflection call from every site Go itself settles at compile time,
which leaves the handful of operands whose type is a type parameter — Go’s own spec calls them
variable-size and does not fold them either. Marshal.SizeOf<T> would throw for those, because a type
parameter binds at run time to exactly the shapes Marshal.SizeOf refuses: a generic type
(ж<Section>, slice<T>) raises “The specified Type must not be a generic type”, and a struct
holding a managed reference raises “cannot be marshaled as an unmanaged structure”.
The corpus case is one line, internal/saferio.SliceCap[E], which asks unsafe.Sizeof of an E only
to decide how large a chunk it may pre-allocate, and three packages reach it:
debug/macho (E = the Load interface), internal/xcoff (E = ж<Section>), and
go/internal/gccgoimporter through debug/elf (E = ΔSection, a struct over an embedded header, an
io.ReaderAt and a ж<SectionReader>).
So the run-time form answers through GoReflect.TryGoSizeOf — the same Go-layout walk the
reflection bridge already stamps into a descriptor’s Size_, and the same one reflect.Type.Size()
reads:
public static uintptr Sizeof<T>(T x) {
return GoReflect.TryGoSizeOf(typeof(T), GoReflect.ArrayDimsOfValue(x), out nuint size)
? (uintptr)size
: (uintptr)Marshal.SizeOf<T>();
}
typeof(T) is Go’s rule verbatim — Sizeof is defined against the operand’s static type, and the
converter’s inferred type argument is that type. Dims come from the live value because array<T>
carries its Go length in the instance, not the type. Marshal.SizeOf stays as the fallback for the
shapes whose Go size is not derivable (TryGoSizeOf answers false: an array whose length nothing can
reveal, a struct holding such a field), so every operand Marshal.SizeOf could measure still resolves.
This makes the answer correct as well as non-throwing, which matters beyond the three packages:
Marshal.SizeOf reports a bool as 4 bytes where Go says 1, so it would measure any struct containing
one wrong — silently. A Go size has one definition in the runtime. Corpus reach is small
by construction: the run-time
call sites are few against the folded ones.
Converting a Go pointer to unsafe.Pointer
unsafe.Pointer is the golib class unsafe_package.Pointer : StandardBox<uintptr> (a ж<uintptr> heap box over a numeric address). A uintptr/unsafe.Pointer argument converts through the implicit uintptr ↔ Pointer operators, but a Go pointer argument (*T, emitted as the managed box ж<T>) has no such conversion — a plain cast (@unsafe.Pointer)(ж<T>) is CS0030 (when T is unrelated to uintptr) or a runtime InvalidCastException (the base→derived downcast (@unsafe.Pointer)(ж<uintptr>) compiles but the object is a plain ж<uintptr>, not a Pointer). So unsafe.Pointer(ptr) for a pointer ptr is emitted through the referent-retaining mint:
func (u *UnsafePointer) Load() unsafe.Pointer { return Loadp(unsafe.Pointer(&u.value)) }
public static @unsafe.Pointer Load(this ж<UnsafePointer> Ꮡu) {
return (uintptr)Loadp(@unsafe.Pointer.FromBox(Ꮡu.of(UnsafePointer.Ꮡvalue)));
}
FromBoxcarries the transient numeric address (not GC-stable — the caveat of everyunsafe.Pointer-as-uintptruse) and the source box itself (Pointer.RetainedSource, theNativeBox§4 retention pattern). The retained referent is what makes the bare-unsafe.Pointeratomic primitives real:*(*unsafe.Pointer)(ptr) = valmust write the memory the pointer names, not the argument box’s ownuintptrslot, soStorepNoWB/Loadprecover the referent (StoreThrough/LoadThrough, dispatching through golib’s internalIUntypedSlotAccessseam onж<T>) and reach the very slot the pointer names. A pointer with no recoverable referent panics by name instead of losing the write. A nil box mints the 0 address, never a nil-deref panic — Go’sunsafe.Pointer(nil), matching the pointer-parameter emission. The*unsafe.Pointersiblings (Casp1/storePointer/casPointer) were always correct by signature — they carry an aliasingж<unsafe.Pointer>. A pointer receiver keepsFromRef(athis ref Treceiver has no box to retain) — the one non-retaining mint, a recorded residual, loud at the primitives if ever reached. (The reinterpret pattern*(*U)(unsafe.Pointer(&x))is handled separately and is not affected. Guarded byGolibTests’UnsafePointerRetentionTests, failing-first against theFromRefemission, plusinternal/runtime/atomic’s own test suite.)
A NIL pointer converts to address 0, not a throw. golib’s ж<T> → uintptr (and ж<T> → void*) operator takes the pointed-to storage’s address via a fixed block — but a nil box has no storage to pin, so &value.Value dereferences it and throws. Go’s uintptr(unsafe.Pointer(nil)) is simply 0, and the syscall wrappers pass nil pointers exactly this way: syscall.Write hands writeFile a nil *Overlapped for a synchronous write, then passes uintptr(unsafe.Pointer(overlapped)) (= 0) to the SyscallN trampoline. The operators return 0/null for a nil box before pinning — so on Windows a converted os.Stdout.Write (hence fmt.Println) whose stdout is a pipe reaches the OS WriteFile with its nil overlapped argument. (Guarded by the NilPointerUintptr behavioral output test — uintptr(unsafe.Pointer(nilPtr)) == 0 and a non-nil control, vs Go.)
A PACKAGE-SCOPE uintptr(unsafe.Pointer(...)) must not crash the converter. The unsafe.Pointer conversion path has a special case that rewrites unsafe.Pointer(arg) into the ref-based extension call (uintptr)@unsafe.Pointer.FromRef(ref arg) when the enclosing function is a pointer-receiver method whose single argument aliases the receiver (pointer-receiver methods are emitted as ref-based extension functions, so the pointer must be reconstructed from a ref). A package-level var initializer is converted with no enclosing function, so currentFuncSignature is nil there and an unguarded receiver probe (v.currentFuncSignature.Recv()) would nil-panic during conversion (go/types.(*Signature).Recv). The special case can never apply at package scope — there is no receiver — so it is guarded by v.currentFuncSignature != nil (the same idiom convUnaryExpr and captureModeOperations use), which falls through to the ordinary emission: var gPtr uintptr = uintptr(unsafe.Pointer(&global)) → (uintptr)Ꮡglobal, identical to the in-function non-receiver form. cmp’s cmp_test.go is the corpus case: it declares var nonnilptr uintptr = uintptr(unsafe.Pointer(&negzero)) and var nilptr uintptr = uintptr(unsafe.Pointer(nil)) at package scope.
A null Pointer reference (from unsafe.Pointer(nil)) also converts to 0. Distinct from the nil-box case above: the untyped nil literal in unsafe.Pointer(nil) renders as (@unsafe.Pointer)default!, and default of the reference type Pointer is a C# null, not a ж<T> box. golib’s Pointer → uintptr and void* conversion operators honor the null-tolerance Pointer’s own ==/!= operators have (value?.IsNull ?? true) — value is null ? 0/null : value.Value — rather than dereferencing value.Value and throwing NullReferenceException, so uintptr(unsafe.Pointer(nil)) yields 0 whether the nil arrives as a nil box or a null Pointer reference. (Both the package-scope crash and this null-reference conversion are guarded by the extended NilPointerUintptr behavioral output test — package-level var gPtr = uintptr(unsafe.Pointer(&global)) (non-zero) and var gNil = uintptr(unsafe.Pointer(nil)) (0), vs Go; the pre-fix converter panics on the package-scope declaration and, once past that, the pre-fix golib NREs on gNil.)
A pointer to a Go fixed array resolves to the array’s DATA, pinned across the FFI call. The other half of that fixed-block operator is wrong for a ж<array<T>> (unsafe.Pointer(&arr) where arr is a Go [N]T): &value.Value is the address of the golib array<T> struct wrapper — which holds the backing T[] as a reference field, an offset, and a length — not the address of the array data, and the fixed releases it before the operator even returns. A native syscall handed that address writes over the wrapper’s fields (clobbering the T[] reference), so a later buf[i] reads through a corrupted array and faults. This is exactly the go-isatty MSYS/cygwin-pipe probe: IsCygwinTerminal fills a [262]uint16 with a FILE_NAME_INFO via GetFileInformationByHandleEx(…, uintptr(unsafe.Pointer(&buf)), …), then reads l := *(*uint32)(unsafe.Pointer(&buf)) (the FileNameLength) and slices buf[2 : 2+l/2] — the garbage l drove array<uint16>.get_Item(Range) off the end (AccessViolationException). The converted fatih/color sample went empty on a pipe because of it: fatih/color’s NoColor probe evaluates !isatty.IsTerminal(fd) && !isatty.IsCygwinTerminal(fd), so only a pipe (where IsTerminal is false, unlike a console, and GetFileType is FILE_TYPE_PIPE, unlike a file) reaches the faulting FFI call — file-redirect and real-console output were fine, matching the observed matrix. The operators special-case a value that is a Go fixed array — an IArray that is not an ISlice (a slice<T>’s &s is its header, exactly as in Go, so slices stay on the value-slot path) — and return the pinned address of element 0 of the backing T[], via a PinnedBuffer (a GCHandle.Alloc(…, Pinned)) cached on the box. The pin lives for the box’s lifetime — so the syscall write lands in the real backing array and every managed read afterward (the l reinterpret and the buf[2:] slice) observes it — and is released when the box is collected (the PinnedBuffer finalizer frees the handle). This is golib behavior (the emitted code does not depend on it); non-array pointers keep the transient fixed-address behavior. (Guarded by the FixedArrayBufferPointer behavioral output test — the *(*uint32)(unsafe.Pointer(&buf)) read-back idiom, the array still readable through its own indexer afterward, and address-stability across repeated conversions, vs Go; end-to-end, the converted fatih/color sample prints byte-identically to go run through a pipe on Windows.)
EVERY managed address handed to native code is pinned for the pointer’s lifetime — a fixed block cannot outlive its own statement. The fixed-array entry above is one case of a general rule: a transient fixed address is unsafe for a BLOCKING syscall, because the window between capture and the trampoline’s calli stays open for as long as the kernel takes. os’s TestPipeEOF (on Windows) parks in ReadFile on a pipe for 10 ms per read while the rest of a parallel suite allocates around it, and a gen0 collection in that window would move both the *uint32 byte-count box syscall.Read passes and the caller’s read buffer — measured directly, a heap(new uint32(), out var Ꮡdone) box and a Ꮡ(buf, 0) element pointer BOTH report a different address after one forced collection. Unpinned, the kernel would write to neither: done would stay 0, syscall.Read would return (0, nil), and internal/poll’s FD.eofError would turn that into a premature io.EOF, while the buffer’s half writes 4 KB into freed heap. golib pins before it reads an address: ж<T>’s uintptr/void* operators call EnsureStableAddress, which takes a lifetime GCHandle on the ROOT storage the pointer names — a standard heap box pins its own value slot, an element reference pins the canonical backing array, a field reference recurses to the allocation that contains the field — on exactly the terms pinnedArrayData already used for the fixed-array case, and released when the box is collected. What makes this possible is that a standard heap box’s value STORAGE is a one-element array for a T that contains no references (ж<T>.m_slot): a box is a class with reference fields and GCHandle refuses to pin anything that contains pointers, so the value had nowhere pinnable to live. It is allocated EAGERLY and never migrated — heap<T>(out ж<T>) hands the caller a ref alias before any address is taken (ref var done = ref heap(new uint32(), out var Ꮡdone)), so moving the storage on first address-take would leave that alias on the abandoned copy, which is this very bug one level down. A T that DOES carry references gets no slot and keeps the transient address: its C# layout is not a native layout either, so no syscall can meaningfully be handed its address — the change is additive, and RuntimeHelpers.IsReferenceOrContainsReferences<T>() is a JIT constant, so neither the branch nor the allocation costs such a box anything. This also makes Go’s unsafe.Pointer RULE 3 (pointer arithmetic through uintptr) sound. This is golib behavior; the emitted code does not depend on it. (Guarded by src/tests/GolibTests/NativeAddressStabilityTests.cs, a neutered-fix control across all four box kinds plus the reference-bearing negative case: with EnsureStableAddress removed every address assertion fails on the first forced collection.)
A STRUCT handed to the kernel by address must be blittable — otherwise the wrapper is hand-owned. The fixed-array entry above covers a pointer to a fixed ARRAY; this is the same problem one level up, for a whole struct, and it has no golib-level answer. A generated syscall wrapper passes uintptr(unsafe.Pointer(&s)) and the kernel writes the NATIVE record at that address. That is safe only when the converted struct’s C# layout matches the native one — which it does for a scalar/handle struct (SecurityAttributes is two uint32s and a uintptr, so CreatePipe, and therefore os.Pipe, works through the ordinary converted wrapper), and never when a field is a golib array<T> (Go’s inline [N]T) or a ж<T> (Go’s pointer field): both are MANAGED REFERENCES occupying one word where Windows expects inline bytes or a raw address. The kernel then writes the native-sized record over a smaller managed object — corrupting the heap past its end and leaving fabricated object references in the reference-typed fields. It does not fail at the call. It fails at the next read of one of those fields, arbitrarily far away and with a diagnostic that names the wrong code: syscall.GetTimeZoneInformation writes 172 bytes of TIME_ZONE_INFORMATION (two inline WCHAR[32] name buffers) over a ~64-byte Timezoneinformation, and the crash surfaces as an ACCESS_VIOLATION inside slice<ushort>..ctor on zoneinfo_windows.go’s next syscall.UTF16ToString(z.StandardName[:]) — so every converted program calling time.Now().Weekday() / Location() / Local on Windows would die, in time, with no mention of syscall. The remedy is per-wrapper hand-ownership, not a converter or golib change: a manualConversionFuncs entry turns the generated wrapper into a placeholder, and a *_impl.cs companion supplies a blittable [StructLayout(LayoutKind.Sequential)] mirror (fixed buffers for the inline arrays, so they stay inline), a direct [DllImport], and an explicit field-for-field copy back into the converted struct at the boundary — see src/core/syscall/windows/zsyscall_windows_impl.cs, and src/core/syscall/windows/exec_windows.cs’s StartProcess/_STARTUPINFOEXW for the first instance. Verify at VALUE level, never at fault level: a mirror with the wrong offsets returns garbage without crashing, so “it no longer faults” proves nothing. (Guarded by the LocalTimeZone behavioral output test, which compares the zone ABBREVIATION — which comes from the name buffers — the offset in seconds — from Bias/StandardBias/DaylightBias — and a fixed instant rendered through the local zone — which selects between them via StandardDate/DaylightDate — against go run.) A census of src/core/syscall finds more non-blittable converted structs, and more wrappers passing one by address, than have been fixed ; the ones not yet fixed are latent and are fixed when a test suite reaches them rather than speculatively, since each needs its own value-level verification. src/core/syscall is not the class’s boundary, and the class runs in BOTH directions. internal/syscall/windows holds more wrappers of the same shape , and where the kernel merely READS the record go2cs hands it the diagnosis above inverts: nothing is written over the managed object, so there is no delayed corruption — instead the native reader picks each field out of the C# storage at the NATIVE offset, and because the CLR auto-layouts a struct containing references (grouping them first) an ordinary integer field ends up under a pointer field. internal/syscall/windows.SHARE_INFO_2 is 48 managed bytes against 56 native ones, and netapi32 reads shi2_path at offset 40 — which in C# is MaxUses (1) followed by CurrentUses (0) — so it dereferences the pointer value 1 and the process dies AT the call with 0xC0000005; the same reordering makes shi2_passwd an 8-byte over-read past the end of the record. This shape is loud and immediate rather than distant, which makes it easy to misread as a regression in whatever changed most recently. Note also what is NOT the defect: a managed reference standing in for an LPWSTR is survivable on its own — it is a readable address, and a control that keeps the native field ORDER returns ERROR_INVALID_NAME instead of faulting — so the reordering, not the reference, is what a remedy must answer. os’s own readdir is the worked precedent for the read direction (src/core/os/windows/dir_windows_impl.cs walks the kernel’s buffer at native offsets rather than reinterpreting it as the managed surrogate). The write direction has no answer yet where the wrapper receives an opaque *byte: Reinterpret correctly declines to alias a reference-bearing struct as byte, and the address route it falls back to has already discarded the managed identity that a field-for-field copy would need.
Second and third members of the class: findFirstFile1 / findNextFile1. The same seam over a bigger record — WIN32_FIND_DATAW is 592 bytes with cFileName[260] and cAlternateFileName[14] INLINE (520 and 28 bytes of storage), where the converted win32finddata1 carries two one-word array<uint16> references — and the first member a real test suite actually reached: path/filepath.EvalSymlinks → toNorm → normBase asks FindFirstFile for the on-disk spelling of every path element, so the whole EvalSymlinks family would take the C# test host down mid-run, leaving every later test with no result at all. Both faces of the corruption appeared from that one package — an IndexOutOfRangeException inside PinnedBuffer where the clobbered reference still resolved to something (normBase’s UTF16ToString(data.FileName[:])), and an outright ACCESS_VIOLATION in slice<ushort>..ctor where it did not (copyFindData’s src.FileName[..]). Only the two *1 wrappers are hand-owned: Go itself puts the native-layout boundary exactly there — syscall_windows.go’s FindFirstFile allocates a win32finddata1, calls the wrapper, then copyFindDatas out — so the public FindFirstFile/FindNextFile and copyFindData above them are pure Go logic and convert faithfully. Two details generalize to the next member of the class: the name argument is pinned with a fixed block wrapped around the call rather than handed golib’s TRANSIENT ж→uintptr address (Value on an element box resolves to a ref INTO the caller’s backing array, so fixed pins that array and the whole NUL-terminated name stays contiguous behind the pointer for the call); and both inline buffers are copied WHOLE, NULs included, because Go reads them as UTF16ToString(data.FileName[:]) — which stops at the first NUL — while the SAME win32finddata1 is reused across every FindNextFile of an enumeration, so a copy that stopped at the terminator would leave the previous entry’s runes behind it. Reserved0 (offset 36) is copied verbatim but deliberately not asserted: Windows documents it as the reparse-point tag only when FileAttributes carries FILE_ATTRIBUTE_REPARSE_POINT and UNDEFINED otherwise, so a reparse-free tree has no stable value to compare — its offset is instead pinned from both sides by the verified FileSizeLow (32) and FileName (44), which leave it and Reserved1 the only eight bytes between them. (Guarded by the FindFirstFileData behavioral output test, which builds a purpose-made tree and compares, per entry, the long name in both ASCII and non-ASCII — FileName at 44 — the 8.3 short name — AlternateFileName at 564 — the directory bit — FileAttributes at 0 — the byte size — FileSizeLow at 32 — and a DISTINCT fixed instant stamped per entry — LastWriteTime at 20 — plus ERROR_FILE_NOT_FOUND for a missing file, the ERROR_NO_MORE_FILES end of an enumeration, and filepath.EvalSymlinks restoring canonical case through the whole path/filepath path, all against go run.)
Fourth member, and the first carrying TWO defects: the sockaddr family. net.Listen on Windows is what forced this one, and it never even reached the seam — it died one layer earlier. Go writes the port in network byte order through a two-byte alias over the raw struct’s port field, p := (*[2]byte)(unsafe.Pointer(&sa.raw.Port)), and the auto conversion of that rebuilds an array<byte> from a raw address, which materializes default(array<byte>) — a LENGTH-ZERO array — so p[0] panics with index out of range [0] with length 0. An array<T> is a managed container with its own header, not two inline bytes, so no address reinterpret can ever produce one; the encoders are hand-owned and write the field arithmetically instead, leaving raw in exactly the state Go leaves it. Behind that sat the ordinary form of this class: RawSockaddrInet4’s Addr [4]byte / Zero [8]uint8 are array<byte> managed references, so unsafe.Pointer(&sa.raw) names a ~24-byte object with object references where Windows wants a 16-byte sockaddr_in with the octets inline — the case golib’s own ж.cs describes when it explains why a reference-bearing pointee gets no pinnable storage (“such a value’s C# layout is not a native layout either, so no syscall can meaningfully be handed its address”). Two departures from the precedent above are worth cribbing. The mirror is a LOCAL at each call site rather than a field the way Timezoneinformation’s is: a sockaddr’s native image is needed for the duration of one call, and a stack buffer is trivially stable for exactly that long, where a managed field’s address would need a pin whose lifetime nothing owns. And no new [DllImport]/[LibraryImport] is declared at all — golib models unsafe.Pointer as a box over a plain address (unsafe’s Pointer : ж<uintptr>, whose uintptr operator returns the stored address), so the package’s OWN generated bind/connect/connectEx wrappers already accept any address and were never the broken part; handing them the mirror’s address reuses their errno handling verbatim and keeps the hand-owned surface to the layout translation, which is the only thing that was wrong. Getsockname/Getpeername are the exception, and for a precise reason: their generated wrappers take a typed ж<RawSockaddrAny> rather than an address, so those two call the package’s Syscall trampoline directly, mirroring the generated wrappers’ error handling. (Guarded by the SockaddrRoundTrip behavioral output test — socket/bind/getsockname/listen/connect/getpeername on loopback over both IPv4 and IPv6 — which prints kernel-derived values and cross-checks them rather than checking for absence of a fault: the client’s getpeername must equal the listener’s bound address field for field, closing encode → kernel → decode → encode → kernel → decode. Ephemeral ports are never printed, only whether the two ends agree about them, so the output is host-independent.)
The DECODE is hand-owned too, which a converter capability makes possible. (*RawSockaddrAny).Sockaddr carries the same port alias as the encoders and panics identically. Its body holds the only ΔSockaddr casts in the package, so displacing it would drop the three GoImplement<…>(Pointer = true) records and net would mint duplicates (the trap written up below) — except that recordSamePackageImplements records the POINTER method set, so the records do not depend on that body. net’s ACCEPT path is what needs it: it decodes the GetAcceptExSockaddrs output through this one method, the single route to a Sockaddr that the hand-owned Getsockname/Getpeername do not cover. Its shape differs from every other member of this class, and that is the transferable part. The others translate a MANAGED struct into a native image to hand the kernel; this one runs in the opposite direction with no kernel call in it at all — it FLATTENS the managed RawSockaddrAny back into the 116-byte native image its fields are a transcription of (Family at 0, Addr.Data covering 2..15, Pad covering 16..115) and hands that to readNativeSockaddr, the same decode Getsockname/Getpeername already route through. Two things force the flatten rather than a field-by-field read: the auto body’s Reinterpret<RawSockaddrAny, RawSockaddrInet4> cannot alias one reference-bearing struct as another (their managed layouts share no offsets at all), and a sockaddr_in6’s 16-byte address spans offsets 8..23 — Addr.Data[6..13] and Pad[0..7] — so a decode written against the managed fields has to know that boundary anyway. Reusing the one native decode keeps that knowledge in a single place. (Guarded by four lines in SockaddrRoundTrip driving the method directly on hand-built RawSockaddrAny values: IPv4, an IPv6 case whose address deliberately crosses the Data/Pad boundary, an AF_UNIX name, and an unknown family that must answer EAFNOSUPPORT. The Go side writes those fields BY NATIVE OFFSET, so the test states the layout it depends on instead of assuming it.)
An ARRAY of native records adds a stride and a size that is both an INPUT and an OUTPUT — WSAEnumProtocols / WSAStartup. Every earlier member of this class passes ONE struct; WSAEnumProtocols passes an array of them, and the two things that follow from that are the transferable part. (1) The size is a byte count the CALLER computed, and it is already native. Go writes len := uint32(unsafe.Sizeof(buf)) over its [32]WSAProtocolInfo and the converter answers with Go’s own arithmetic — 20096, i.e. 32 × 628, correct as a native size and four fifths of it past the end of the ~3.8 KB managed array the same call hands over (WSAPROTOCOL_INFOW ends in szProtocol[256] INLINE and nests a GUID’s [8]byte and a WSAPROTOCOLCHAIN’s [7]uint32, three inline arrays the conversion collapses to one-word references, so the managed record is ~120 bytes). This is Process32First’s dwSize rule — the size a native call is told must be the NATIVE size — but it arrives from the caller rather than from the wrapper, so the mirror does not compute it: it SIZES ITS OWN STAGING BLOCK FROM IT, which makes the two agree by construction and needs no cross-check. (2) The same field is an OUTPUT, and must pass back UNTRANSLATED. On WSAENOBUFS Windows rewrites it with the size the catalog needs, in NATIVE strides, and the Go caller compares that against its own unsafe.Sizeof arithmetic — so a boundary that helpfully “corrected” it to managed sizes would be the defect, not the fix. Both directions are native-side arithmetic; the mirror is only a layout translator and touches neither. The record COUNT Windows returns is likewise a count of native records, so the copy-back walks the staging block at sizeof(NativeWSAProtocolInfoW) and steps the caller’s pointer with unsafe.Add, which resolves through the backing array (the mechanism the netpoll stageBuffers already relies on for &o.bufs[0]). A third property is new and worth naming: the address was not merely wrong, it was TRANSIENT. Ꮡbuf.at<WSAProtocolInfo>(0) cannot be pinned at all — PinnedBuffer.PinOnly refuses a reference-bearing element type — so unlike Timezoneinformation, where a stable-but-mislaid-out address was handed over, here no address the box can produce survives the call. That closes the door on any remedy short of a native staging block; there is nothing to pin.
⚠ And a struct-passing defect can be SILENT because nothing READS the out-struct — WSAStartup is the demonstration. It is the same class over WSADATA (408 bytes native, szDescription[257] and szSystemStatus[129] inline, against a ~40-byte managed WSAData whose CLR auto-layout groups its two array<byte> references first), it runs inside internal/poll’s InitWSA once per process that imports net, and an unfixed wrapper writes ~408 bytes over that ~40-byte object. Nothing notices, for one reason only: InitWSA discards the WSAData it passes. The moment a program READS data.Description the corruption surfaces as an ACCESS_VIOLATION in slice<byte>..ctor — the exact GetTimeZoneInformation signature — and it does so BEFORE the WSAEnumProtocols on the next line is reached, which is why the two are hand-owned together. The lesson generalizes past Winsock: a passing suite is not evidence that a wrapper of this shape is sound, and neither is the absence of a fault — only a read of the out-struct’s far fields is. (Both guarded by the WsaProtocolInfo behavioral output test, which asserts only host-INVARIANT properties so Go and the conversion must agree on any machine: the negotiated 2.2 version words and a printable NUL-terminated Description for WSAStartup; and, because the query names IPPROTO_TCP alone, SocketType == SOCK_STREAM, Protocol == IPPROTO_TCP and an INET address family for EVERY returned entry — three fields that sit past the nested GUID and WSAPROTOCOLCHAIN whose inline arrays the managed layout collapses, so a record read at managed strides cannot agree with them by luck — plus a printable ProtocolName pinning the far end of the record 512 bytes further on, a required size that is a whole number of native records, and the XP1_IFS_HANDLES verdict itself, which is what useSetFileCompletionNotificationModes → FD.skipSyncNotif is derived from.)
A LINKED output cannot be handed back at all — GetAddrInfoW / FreeAddrInfoW. Every earlier member of this class copies ONE record, or an array of them, into storage the CALLER already owns. GetAddrInfoW returns a chain the KERNEL owns: native ADDRINFOW records (48 bytes on x64 — four ints, a size_t, and three raw pointers) linked by ai_next, each pointing at a sockaddr whose type is selected by ai_family. The wrapper is defective in the established way at both ends — the converted AddrinfoW holds Canonname, Addr and Next as MANAGED REFERENCES, so the hints Windows reads are garbage and the *ADDRINFOW it writes lands in a reference slot, which is the Fatal error. 0xC0000005 inside Syscall6 that crypto/tls’s TestVerifyHostname measured — but fixing the call alone would not have been enough, and that is the transferable part. net’s consumer reads the sockaddr THROUGH the result ((*syscall.RawSockaddrInet4)(unsafe.Pointer(result.Addr))), and RawSockaddrInet4’s Addr [4]byte / Zero [8]uint8 are golib array<T> values — a backing-array REFERENCE plus bounds — so reading that struct out of a native sockaddr_in fabricates two managed references from address and port bytes, which is the fabricated-reference deref of the sha3 case below and which has no general fix. So the hand-own transcribes the WHOLE chain into managed ж<AddrinfoW> boxes and the sockaddr with it, typed by ai_family exactly as the native ai_addr is, and frees the native chain EAGERLY at the copy — which makes FreeAddrInfoW a hand-owned no-op, because nothing native escapes the call and handing a managed object’s address to ws2_32’s real free would release memory it does not own. Two properties are worth carrying past Winsock. (1) A managed pointer CAN cross an unsafe.Pointer field, through the token table. AddrinfoW.Addr is Go’s syscall.Pointer (type Pointer *struct{}), and the consumer projects it to a scalar and converts the scalar back to a typed pointer — precisely the round trip golib’s ManagedPointerTokens exists to make work — so the sockaddr box is registered under its own pointer-order token and the field carries that token. The hand-owned reflection bridge is the other minter of those tokens, and this shape is the one the table was written for rather than a new use of it. The token must be wrapped in a Pointer built over a NATIVE-address ж<EmptyStruct>, because the generated named-pointer wrapper’s uintptr conversion takes the address of the storage its box addresses — which for a native box IS the number handed in, so the token survives the field round trip unchanged. (2) The token table is WEAK by design, so the strong reference is the hand-own’s business. A ConditionalWeakTable keyed on the record box holds each sockaddr for exactly as long as its record is reachable, which is the Go lifetime; without it the box behind a live token could be collected and the consumer’s cast would silently fall back to a native-address box over a token — a wild read where the defect had been a loud one. (Guarded by the LookupServicePort behavioral output test, which asks net.LookupPort for tcp/udp/tcp4/tcp6 services: that is the one reach into this pair needing neither DNS nor a network, and it exercises the hints mirror, the chain copy, the token handoff and BOTH sockaddr flavors — a byte-order slip prints a swapped port rather than failing, so the comparison is at value level. Proven failing-first: with the hand-own removed the guard reports exit code mismatch: C# -1073741819 vs Go 0, i.e. the access violation itself.)
⚠ A **T OUT-parameter arrives at the kernel as NULL, silently — a SECOND class beside the layout one, with a census of its own. ж<T> → uintptr answers 0 for a box whose IsNull is true, and IsNull is the VALUE-PEEKING question: for ж<ж<T>> — a heap-boxed POINTER, which is what &p is when p is a nil *T — the held reference is null before the call, so the operator reports the box as nil and returns 0. That is deliberate and correct for the case it was written for (syscall.Write hands writeFile a nil *Overlapped, and uintptr(unsafe.Pointer(nil)) is 0 in Go), and it is silently wrong for the shape every out-parameter takes: the wrapper tells the kernel “I do not want this output”, the kernel obliges, the call SUCCEEDS, and the caller reads back the nil it started with. The measured instance is crypto/x509’s Windows system verifier: createStoreContext calls CertAddCertificateContextToStore(handle, leafCtx, CERT_STORE_ADD_ALWAYS, &storeCtx) — whose ppStoreContext is documented OPTIONAL — gets err == nil, returns a nil storeCtx, and systemVerify’s next line panics on (*storeCtx).Store with a Go-shaped nil-pointer dereference that names golib’s ~ operator and not the wrapper three frames back (crypto/tls’s TestQUICHandshakeError). Note that the 0 is accidentally the SAFER outcome: had the address been passed, the kernel would have written a native pointer into a slot the collector reads as an object reference. So the remedy is the same per-wrapper hand-own as the layout class — receive into a native slot, then build a native-address ж<T> from it — never a change to the operator. A census of the emitted wrappers finds these of this shape : CertAddCertificateContextToStore, CertGetCertificateChain, ConvertSidToStringSid, ConvertStringSidToSid, DnsQuery, _DnsQuery, GetFullPathName, getQueuedCompletionStatus, GetQueuedCompletionStatus, NetGetJoinInformation and NetUserGetInfo in syscall, plus CreateEnvironmentBlock and NetUserGetLocalGroups in internal/syscall/windows — and two already hand-owned for other reasons (GetAddrInfoW, GetAcceptExSockaddrs). Each is fixed when a test suite reaches it, as for the layout census.
The out-parameter class is CLOSED at the boundary, because the operator has TWO wrong answers and neither is fixable. The entry above is right about the mechanism and stops one measurement short. Asked directly, golib answers (uintptr) for a ж<ж<T>> 0 while the held pointer is null — the silent “no output wanted” above — and a live MANAGED address the moment it is not, which is the worse of the two: a slot the collector reads as an object reference, handed to a kernel that writes eight raw bytes into it, silent until the next collection. (EnsureStableAddress does not even pin such a slot: GCHandle pins only reference-free storage, so the address is transient as well as wrong.) That pair settles the layer question by measurement rather than preference: no single address is both kernel-writable as eight raw bytes and managed-readable as a ж<T>, so there is nothing the operator could return that would serve the boundary. Reconciling the two representations needs a SYNC POINT — the moment the raw word becomes a pointer box — and that moment is “after the call returns”, which only the wrapper knows. So ж.cs is unchanged, and the remedy is one shape applied per wrapper: a native cell local to the call, its address handed over, and a publish afterwards through ValueSlot, never Value (the nil guard on Value value-peeks, so it would panic on the very write that fills the slot in — the same reason GetAcceptExSockaddrs writes its two **RawSockaddrAny results that way). A ZERO report publishes the nil pointer with no special case, because ж<T>’s native constructor already treats address 0 as nil (Go’s (*T)(unsafe.Pointer(uintptr(0))) == nil), which is what lets each wrapper write one unconditional assignment for both the written and unwritten cases. A nil slot is a real caller, not a defensive branch — crypto/x509’s createStoreContext passes literal nil for every intermediate it adds. The census’s wrappers that a test suite reaches are hand-owned: ConvertSidToStringSid/ConvertStringSidToSid (the round trip), NetGetJoinInformation (over a third DLL with a different free routine) and NetUserGetInfo in src/core/syscall/windows/zsyscall_windows_ptrout_impl.cs; the two crypt32 members crypto/x509 reaches, CertAddCertificateContextToStore and CertGetCertificateChain, in zsyscall_windows_certchain_impl.cs; and NetUserGetLocalGroups in src/core/internal/syscall/windows/windows/zsyscall_windows_ptrout_impl.cs. _DnsQuery is hand-owned differently (zsyscall_windows_dnsrecord_impl.cs): its **DNSRecord pointee is a linked native chain whose converted record holds managed references, so publishing the address alone would replace a silent nil with a fabricated-reference landmine; the chain is transcribed into managed boxes and freed eagerly instead, the GetAddrInfoW way. The rest are left for stated reasons. Verify at VALUE level, exactly as for the layout class: the guard (PointerOutParameter behavioral output test) round-trips four well-known SIDs string→*SID→string, requires a malformed SID to still be REJECTED, and WALKS NetGetJoinInformation’s returned buffer to its NUL rather than testing it for nil. Proven failing-first, and the failure is not a crash — with the cell address neutered the program exits 0 and prints stable: true false, two empty strings agreeing with each other and with nothing else, plus a plausible-looking ERROR_INVALID_PARAMETER from advapi32 (whose out-parameter, unlike ppStoreContext, is REQUIRED). The golib contract itself is pinned from the other side by GolibTests’ AddressOfAPointerToPointerIsNotAKernelWritableSlot and PublishingANativeAddressThroughValueSlotIsTheBoundaryRemedy, so a later reader cannot “fix” the operator into the dangerous answer. ⚠ The out-parameter alone does not make crypto/x509’s system verifier work: CertGetCertificateChain is also a member of the LAYOUT class. CertChainPara is passed by address while holding ж<ж<byte>> UsageIdentifiers and ж<Filetime> CacheResync, and systemVerify writes the NATIVE Size (80) into a much smaller managed object, so an unmirrored call blocks inside the kernel with every field past the first read at the wrong offset. zsyscall_windows_certchain_impl.cs therefore hand-owns CertGetCertificateChain, CertVerifyCertificateChainPolicy and CertFreeCertificateChain together: the parameter record is transcribed into native memory for the call, and each pointer crypt32 hands back becomes a managed transcription that remembers its ORIGINAL native address in a weak side table, because the later policy and free calls need that pointer back (a transcription alone would leak every context; a native box alone reads every field at the wrong offset). See also An OPAQUE *struct{} conversion mints a recoverable token.
⚠ A hand-own removes its body’s EMISSION, and with it any [assembly: GoImplement] its body witnessed — check before listing a function in manualConversionFuncs. This is a general trap the sockaddr work surfaced, not a sockaddr detail. Every GoImplement record the converter writes comes from a CAST it converted, so a function whose body holds the only casts of a type to an interface is also the only thing recording that pair. RawSockaddrAny.Sockaddr is exactly that function for all three Sockaddr types, and hand-owning it, before the method-set recorder below, dropped the three GoImplement<…, ΔSockaddr>(Pointer = true) records from syscall’s package_info.cs. The consequence is not a build failure, which is what makes it dangerous: a MEASURED reconvert of net against the shortened package_info showed net quietly minting its own syscall_SockaddrInet4жΔSockaddr adapters instead of using syscall’s — the SECOND-IDENTITY regression samePackageImplements.go exists to prevent, where reflect and fmt see the wrapper in place of the value’s own type and a direct-boxed value compares unequal to an adapter-wrapped one. Declaring the records in the *_impl.cs does NOT fix it: a dependent package’s converter run reads package_info.cs, not the manual file. Recording them from the method set is the answer: recordSamePackageImplements covers the POINTER method set as well as the value one, so these three pairs are recorded from types.Implements(*T, Sockaddr) and do not depend on any body being converted. The general trap is narrower but not gone, and the check is unchanged for the cases the recorder’s gates exclude — a pair whose interface or target is UNEXPORTED, a named FUNC target, a generic, or a promotion deeper than one embed hop is still witnessed only by its casts, as is any pair whose interface is declared in a DIFFERENT package (the recorder is same-package only, so a body holding the only *T → io.Reader cast is exactly as load-bearing as before). The cheap check before hand-owning anything: grep the body for interface conversions, and if it has any, reconvert one DEPENDENT package and diff.
The netpoll seam behind the sockaddr one. With bind succeeding, net.Listen walks on to internal/poll’s pollDesc.init and the ten bodyless //go:linkname netpoll entry points (runtime_pollServerInit and its siblings), which the PartialStubGenerator would emit as “external (assembly or cgo) function is not implemented”. The converted runtime has the counterparts (runtime/netpoll.cs carries poll_runtime_pollServerInit), but nothing wires a linkname across assemblies, and wiring it would not be enough either: the Windows implementation bottoms out in netpollinit → stdcall2(_CreateIoCompletionPort) → asmstdcall, itself a stub. So the ten runtime_poll* contracts are hand-owned against .NET’s own completion-port machinery in internal/poll/windows/runtime_netpoll_impl.cs — the managed-API-boundary pattern sync’s Mutex and runtime’s traceback surface use — rather than emulating Go’s poller; see The managed netpoller.
The same fork one step further out: REINTERPRETING an OS byte buffer as a Go struct (os.readReparseLink). The entries above are about a struct handed TO the kernel; this is the mirror — a byte slice the kernel FILLED, reinterpreted as a Go struct whose trailing inline array stands in for variable-length data written after it. internal/syscall/windows’s SymbolicLinkReparseBuffer / MountPointReparseBuffer both end in PathBuffer [1]uint16, and Path() reads the real name through (*[0xffff]uint16)(unsafe.Pointer(&rb.PathBuffer[0]))[n1:n2]. In the conversion that field is a golib array<uint16> — an 8-byte MANAGED REFERENCE where the OS wrote 2+ bytes of inline UTF-16 — so golib’s PointerExtensions.Reinterpret correctly declines to alias managed storage for a reference-bearing struct and falls back to the raw-address route; &rb.PathBuffer[0] then resolves an object reference synthesized out of path bytes and faults with an ACCESS_VIOLATION inside array<uint16>.get_Item. That would kill the C# test host mid-run at os’s TestReadlink, the same silent loss of every later result findFirstFile1 causes for path/filepath. No converter or golib change can rescue it — a managed array reference can never be laid out like an inline OS array — so it takes the dir_windows_impl.cs remedy rather than the blittable-mirror one: manualConversionFuncs turns os.readReparseLink (and readReparseLinkHandle, which Go 1.24 split out of it and os.Root reaches directly) into placeholders and src/core/os/windows/file_windows_impl.cs decodes the record straight out of the byte slice at its documented offsets (REPARSE_DATA_BUFFER header 8 bytes; then SubstituteNameOffset/Length at +0/+2, PathBuffer at +12 for the symlink shape and +8 for the mount point), with every read an ordinary bounds-checked slice index — no pointer, no pinning, no unsafe block. openSymlink and normaliseLinkPath stay auto: they pass scalars, handles and strings. syscall.Readlink has the same shape over its own private reparseDataBuffer / symbolicLinkReparseBuffer / mountPointReparseBuffer copies and is not hand-owned: its emission reads the record’s scalar fields through the field property (data.SubstituteNameOffset, see A field reached through a pointer REINTERPRETED from unsafe.Pointer) and the name through an AliasPointer window.
The same fork, in the idiom Go used before unsafe.Slice existed: (*[N]T)(unsafe.Pointer(p))[:] over a NATIVE pointer. This one is worth naming separately because the Go source looks utterly ordinary — it is the standard pre-1.17 way to read a NUL-terminated *uint16 the OS handed back, and Go’s own standard library still writes it. golib’s array<T>.AliasPointer answers it correctly when the pointer has managed element storage behind it: it windows the real backing array, clamped, so an overrun is a Go-style index panic. When the pointer is a genuinely NATIVE address there is no T[] to window, and it falls back to (ж<array<T>>)(uintptr)element — a native-address box over array<T>, which is itself reference-bearing. Dereferencing that fabricates a T[] reference out of address bytes, and the slice constructor built from it dies with Fatal error. System.AccessViolationException at go.slice'1[UInt16]..ctor — measured, from a guard’s first draft reading NetGetJoinInformation’s result. The fallback is not a defect in AliasPointer, which has nothing better available; the emitted form simply has no correct realization for a native operand, exactly as with the sockaddr decode. The DNS answer walk in net/windows/lookup_windows.cs is this idiom (SRV Target, NS Host, TXT StringArray, all *uint16/**uint16 written by DnsQuery), and it is safe because of the established remedy: _DnsQuery transcribes the native record chain into managed boxes at the boundary (as GetAddrInfoW does for ADDRINFOW), so each AliasPointer windows real managed storage. The syscall and internal/syscall/windows uses of AliasPointer are all over MANAGED element pointers (Ꮡ(tv, 0), Ꮡ(rb.PathBuffer, 0)) and take the safe route.
The same fork at its largest, and the first member whose defect is in NO wrapper: net.adapterAddresses and the IP_ADAPTER_ADDRESSES chain. Every entry above names a wrapper. This one does not, and that is the point worth carrying: internal/syscall/windows’s GetAdaptersAddresses is handed a byte buffer and fills it, which is exactly what a byte buffer is for — the wrapper is correct and stays auto-converted. The defect is entirely in the CALLER, net’s own adapterAddresses, which walks the filled buffer as a linked record (for aa := (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])); aa != nil; aa = aa.Next). IpAdapterAddresses is the corpus’s most reference-dense converted struct — nine ж<T> fields, an array<byte> PhysicalAddress and an array<uint32> ZoneIndices where the native record has raw pointers and inline storage — so golib declines to alias the byte run as it and the reinterpret falls to a native-address box, after which the loop’s OWN nil test fabricates a managed reference out of adapter bytes: ACCESS_VIOLATION in ж<IpAdapterAddresses>.op_Equality, killing the process. It was the wall standing behind the GetAddrInfoW fix — dnsReadConfig is getSystemDNSConfig’s only source of DNS servers on Windows — so without it no converted program on Windows could resolve a host name at all, and it also backs net.Interfaces, net.InterfaceAddrs and Interface.MulticastAddrs. The remedy is GetAddrInfoW’s, one structure size up: manualConversionFuncs turns net.adapterAddresses into a placeholder, and src/core/net/windows/interface_windows_impl.cs holds the buffer in NATIVE memory that never escapes the function, transcribes the whole chain into managed ж<T> boxes, and frees the native buffer eagerly in a finally. Three things generalize past this record. (1) The output is a chain of chains. Each adapter record carries SIX nested linked lists (unicast, anycast, multicast, DNS-server, prefix, WINS-server and gateway — five of which share one native shape), and every consumer reaches THROUGH a record into them, so a top-level copy alone would have moved the fabrication one hop out instead of removing it. (2) Whether a managed pointer needs a ManagedPointerTokens handoff is decided by the GO DECLARATION, not by the data. AddrinfoW.Addr is an untyped syscall.Pointer that the consumer casts by hand, so it needs a token; SocketAddress.Sockaddr is a TYPED *syscall.RawSockaddrAny, which converts to a ж<syscall.RawSockaddrAny> field carrying a managed box directly — no unsafe.Pointer round trip to survive, and therefore no token, no weak table and no anchor. Reaching for the token machinery here would have been ceremony. (3) The transcription writes the image an EXISTING hand-own reads. The consumers call .Sockaddr() on that field, which is syscall’s hand-owned decode — it flattens the managed RawSockaddrAny back to its 116-byte native image and decodes that — so the sockaddr transcription is the same inverse-flattening syscall’s own GetAcceptExSockaddrs performs for the accept path (Family at 0, Addr.Data covering 2..15, Pad covering 16..115), duplicated rather than shared because exposing it would put a non-Go symbol on a published package’s public surface. Finally, the copy is FAITHFUL rather than minimal: Length, AdapterName, DnsSuffix, Description, Flags, ZoneIndices, FirstPrefix, the link speeds and the WINS list are carried although no consumer reads them today, because this record is the public shape behind net.Interfaces and a declared field left nil would be a SILENT divergence for the next consumer rather than a loud one. (Guarded by the IpAdapterAddresses behavioral output test. Its assertions are derived facts both sides compute over the same live host — the host’s own adapter set is never printed, since it varies by machine and can change between the two runs — and they pin named native offsets: the loopback MTU is exactly 0xffffffff reported as -1, FriendlyName at +72 must be non-empty, printable, unique and round-trip through InterfaceByName, PhysicalAddressLength at +88 must stay within the inline [8]byte at +80, and 127.0.0.1/8 plus ::1/128 are end-to-end proofs of both sockaddr flavors including the OnLinkPrefixLength read 56 bytes into a unicast entry. Proven failing-first: with the auto body restored the guard reports exit code mismatch: C# -1073741819 vs Go 0.)
The same fork, with NO kernel anywhere in it: reinterpreting one MANAGED array as an array of another element type (sha3.xorIn/copyOut). The two entries above both involve the OS, which makes it easy to file this whole family under “syscall marshalling”. It is not about the OS at all. Go’s sponge implementation views its own [25]uint64 state as raw bytes —
ab := (*[25 * 64 / 8]byte)(unsafe.Pointer(&d.a))
subtle.XORBytes(ab[:], ab[:], buf) // absorb
copy(b, ab[:]) // squeeze
— purely managed storage on both sides. A byte[] view over a uint64[] has no managed spelling (the same fact that made crypto/subtle’s words() — a uintptr[] view over a byte[] — hand-owned), so pointerReinterpretManagedSource excludes pointer-to-ARRAY targets and the site keeps the raw-address route: (ж<array<byte>>)(uintptr)(new unsafe.Pointer(Ꮡd.of(state.Ꮡa))). That box is fine as an address; the defect is what happens when it is dereferenced. ~ab reads an array<byte> STRUCT — a backing-store REFERENCE plus bounds — out of the keccak state’s own DATA, i.e. fabricates a managed reference, and the first use of it is an AccessViolationException inside slice<byte>’s constructor that kills the process. At Go 1.23 crypto/tls reached it on every TLS 1.3 ClientHello (mlkem768.NewKeyFromSeed → kemKeyGen → sha3.Sum512, Go 1.23’s crypto/internal/mlkem768).
The emission is not wrong – there is no correct alternative, so at Go 1.23 the site was hand-owned (src/core/vendor/golang.org/x/crypto/sha3/xor.cs, [module: GoManualConversion]), and the remedy is crypto/subtle’s: MemoryMarshal.AsBytes over the array’s own span is a genuine ALIASING view of the same backing storage, so the absorb’s XOR lands in the real state and the squeeze reads it. Go’s cpu.IsBigEndian branch is left exactly as converted; only the reinterpreting branch changes. (Guarded by GolibTests.Sha3ReinterpretVectorTests – FIPS-202 vectors plus lengths straddling SHA3-256’s 136-byte rate and an unaligned sub-slice, checked against the OS SHA-3 implementation; the vendored package has no _test.go in GOROOT and is not importable from a behavioral test, and an MSTest tier binding a converted package follows GolibTests’ own reference to core/sort (GolibTests.csproj). Neutering the fix is a RUN control: the auto-converted xor.cs does not merely fail the vectors, it KILLS the test host with the AccessViolation above.) (Since Go 1.24 golang.org/x/crypto/sha3 is not vendored and this file does not exist; the standard library’s Keccak lives in crypto/internal/fips140/sha3, whose hand-owned keccakf.cs/keccakf_impl.cs take the reverse view — the [200]byte state as [25]uint64 — by the same MemoryMarshal.Cast remedy. The MSTest guard was re-pointed to crypto/sha3, whose Sum256/Sum512/SumSHAKE256 drive the same xorIn/copyOut pair; the disposition is recorded at src/tests/GolibTests/GolibTests.csproj:231-245.)
The same fork one RANK up — a NESTED array view, and why a native-backed array view does not reach it (internal/chacha8rand.setup/block_generic). sha3’s is a [200]byte view of a [25]uint64: same rank, different element type. chacha8rand opens one [32]uint64 allocation as (*[16][4]uint32)(unsafe.Pointer(buf)) and that again as (*[16][2]uint64)(unsafe.Pointer(b32)) — different element type and different RANK. The failure is the mild end of the fabrication: the buffer is ZEROED at the first call, so the array<array<uint32>> struct read out of it has a null backing, i.e. length zero, and the first index panics index out of range [0] with length 0 rather than access-violating. TestBlockGeneric is the test that reaches it, and the panic site — golib’s array<T>.get_Item — is the same one html’s map-miss produced from a completely unrelated root, which is worth stating plainly: get_Item is the DETECTOR of an unshaped array, never the defect; two correlated crashes at one golib line were two different producers.
A native-backed array view (docs/phase4/DESIGN-native-array-view.md) would not reach it either, and the reason generalizes: a native-backed array<T> can hold blittable elements in raw bytes, and array<uint32> is not blittable — a [16][4]uint32 view needs sixteen array<uint32> STRUCTS, which is exactly the thing that cannot live in the buffer. So the two functions are hand-owned: manualConversionFuncs["internal/chacha8rand"] suppresses both functions and chacha8_impl.cs provides them, taking MemoryMarshal.Cast<uint64, uint> over the array’s own span — the sha3/subtle remedy, one rank up. block was already hand-owned there (Go implements it in ASSEMBLY on amd64/arm64) in a scratch-and-pack shape, and the new block_generic computes IN PLACE through the aliasing view using the package’s own auto-converted qr, so TestBlockGeneric still compares two implementations that share no code — what it newly proves is that the aliasing view WRITES THROUGH. The seam’s kernel-free reproduction is preserved directly in golib by GolibTests.ArrayShapeReinterpretTests, which measures the raw route’s length-zero answer and pins the span view’s write-through in both directions — so the witness stays in golib’s own tests whatever the conversion does.
The same fork with a LOUD failure instead of a silent one — a reinterpret to a LARGER struct, and an assert that kills the process (reflect.rtype.FieldByIndex). Every entry above turns on a fabricated managed reference; this one is decided a step earlier, by SIZE, and it is worth stating separately because the remedy is cheaper and the diagnosis is misleading in a new way. Go’s (*structType)(unsafe.Pointer(t)) re-views an *rtype as the struct-kind specialization that begins with the same abi.Type. ReinterpretAliasesStorage requires the destination to FIT in the source, and structType is strictly LARGER than rtype — it carries PkgPath and Fields past the embedded Type — so the alias arm never engages and the pair takes the raw-address route. The derived box therefore names storage that is not a structType, and the descent Ꮡt.of(structType.ᏑType) yields an abi.Type with no managed cargo: sysType is null. The very first statement of the auto body, toType(&t.Type), then trips canonType’s synthType-was-bypassed assertion — a System.Diagnostics.Debug.Assert, which in a Debug build terminates the process (0x80131623).
That failure mode is the part worth carrying. It is not a fault at a later read, and it is not a test failure: the host DIES, so every remaining test in the file reports no result at all. encoding/xml reaches it from getTypeInfo → addFieldInfo on any Unmarshal of a struct with a promoted field, and every test after the first such call would report no result rather than a failure — which reads like a missing or mis-run suite rather than one dead call, the same silent loss findFirstFile1 and readReparseLink cause. A comment in value_impl.cs asserted this branch was dead (“synthType always stamps sysType… every canonType caller feeds a synthType box or nil”); it was not, and an assert that documents itself as unreachable is exactly the one to distrust.
The remedy is smaller than the blittable-mirror and byte-buffer answers above, because nothing native is involved and no layout has to be transcribed: the reinterpret exists only to reach a descriptor the receiver ALREADY holds. manualConversionFuncs turns rtype.FieldByIndex into a placeholder and value_impl.cs seeds from common() — the rtype’s own synthType-stamped abi.Type, which IS the value Go’s &t.Type names after the reinterpret — so no structType is ever synthesized. Go’s index walk is kept verbatim, and each hop goes through the already-hand-owned rtype.Field, so the promoted-field projection stays the one GoReflect provides. The general lesson for this family: when the reinterpret’s target is a struct-kind SPECIALIZATION that only extends the source, prefer reaching the shared prefix directly over re-viewing the whole record — the specialization’s extra fields are exactly what makes the alias unrepresentable, and they are usually not what the code wanted.
⚠ What did NOT land, and why it is recorded rather than shipped. The failure MODE is the expensive half: an AccessViolationException cannot be recovered, carries no diagnostic, and names whichever consumer first touched the fabricated reference rather than the reinterpret that built the box. A refusal was therefore built and measured – ж<T> raising a contained panic instead of reading. Two counter-examples, both found by gates rather than by reasoning, sank it. (1) RuntimeHelpers.IsReferenceOrContainsReferences<T>() alone is too wide: time.syncTimer’s ~Ꮡc.Reinterpret<channel<Time>, unsafe.Pointer>() lands on the same address route because unsafe.Pointer is a CLASS, and reading it yields the real channel object – type-CONFUSED rather than fabricated, which is the managed-referent model the corpus is built on. Refusing it took down time.NewTimer and every crypto/tls test that opens a pipe. Adding a typeof(T).IsValueType term fixed that. (2) The narrowed form still regressed ArrayCastDerefClone, the behavioral guard for this very fork: its *(*Row)(unsafe.Pointer(&r)) over a ZERO-valued array reads an array<nint> whose fabricated backing reference is null, which array<T>’s null-safe zero value absorbs – so the site produced garbage harmlessly, which is exactly the “compiles and does not crash” bar raw-metal stubs are held to, and the refusal turned it into exit 2. The distinction the remedy actually needs is not the pointee’s TYPE but whether the fabricated reference comes out NULL – and that cannot be tested without first materializing a T with a wild reference in a stack slot, which is itself unsafe. So the class keeps the AccessViolation for now; the corpus files carrying the shape (most of them under runtime, plus internal/syscall/windows/registry’s SetDWordValue/SetQWordValue and reflect/runtime’s name.pkgPath) are each fixed when a suite reaches them, and the null-check sketch is the starting point for whoever revisits it.
Atomic pointer ops on a MANAGED pointer field read/write the reference, not a uintptr. The lock-free-cache idiom atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&x.field))) / atomic.StorePointer(…, unsafe.Pointer(v)) — where x.field has type *T and so holds a ж<T> reference — cannot go through the literal conversion: new @unsafe.Pointer(v) round-trips the managed reference through its (transient) address, and (ж<@unsafe.Pointer>)(uintptr)(FromRef(ref …field)) dereferences raw memory, losing GC identity (it NRE’d on the very first read — x/sys/windows’s LazyDLL/LazyProc proc caches at package-init). convCallExpr.managedAtomicPointerIdiom recognizes the idiom (the callee is sync/atomic.LoadPointer/StorePointer and the argument is (*unsafe.Pointer)(unsafe.Pointer(&Z)) with Z of pointer type) and emits golib’s managed-referent overloads on the field box directly: atomic.LoadPointer(Ꮡx.of(T.Ꮡfield)) → ж<ж<T>> → Volatile.Read returning ж<T>, and atomic.StorePointer(Ꮡx.of(T.Ꮡfield), v) → Volatile.Write of the plain ж<T> (the stored value unwrapped from its unsafe.Pointer(…) conversion). The overloads are additive — a ж<ж<T>> argument never matches the existing ж<@unsafe.Pointer> (= ж<Pointer>) signature, so ordinary unsafe.Pointer atomics are untouched. The load stays unsafe.Pointer-typed to Go, so a caller’s == nil still renders (uintptr)… == nil; the ж<T> → uintptr operator (above) yields 0 for a nil box, so the nil test is correct with no change to the surrounding emission. Blast radius is only the packages using the idiom (x/sys/windows and a handful of stdlib sites), each a pure re-shaping to the managed overload; the behavioral corpus is byte-identical. (Guarded by the ManagedAtomicPointer behavioral output test — a *proc-field lock-free cache initialized once and re-read, vs Go; it NRE’d before the fix.)
The mint depends on how the pointer argument renders. A genuine box — an address-of expression, a local pointer variable, a pointer field, a call result, and a pointer parameter, which renders as its box Ꮡp in pointer context — mints through a retaining door (FromBox / FromPinnedBox). A deref-aliased pointer receiver — a this ref T receiver, which has no box and renders as the pointed-to value alias — takes its ref directly: FromRef(ref p). Detection reuses exprIsDerefAliasedPointer (the same discriminator the pointer-reinterpret block uses). The guintptr/muintptr receiver family (runtime2.go (*uintptr)(unsafe.Pointer(gp)) inside guintptr.cas) takes this form too — ref (gp).Value would bind the generated wrapper’s Value property (CS0206); the CAS it feeds (atomic.Casuintptr) is a partial asm stub, so the copy-box semantics match the established reinterpret precedent (compile-milestone bar; the faithful managed-referent ж<T> model for those types remains a separate effort). (The bare unsafe.Pointer(p) pin stays exercised across the stdlib — runtime select.go/heapdump.go, and runtime2.go’s genuine *guintptr→*uintptr reinterpret here, whose differing element types keep it off the identity path. The UnsafePointerParamPin behavioral output test guards the same-type identity collapse of the (*uintptr)(unsafe.Pointer(p)) shape it originally used — see A SAME-TYPE reinterpret … collapses to the pointer itself below — where the whole conversion elides to the box; a (*byte)(unsafe.Pointer(&value))-style DIFFERENT-type reinterpret still pins through FromRef.)
Returning an unsafe.Pointer parameter whole is a plain value return. The return path boxes a pointer parameter returned whole (return p → return Ꮡp — the value alias cannot bind the pointer result), and the pointer-result check counts the UnsafePointer basic as a pointer. But an unsafe.Pointer parameter renders as a plain value param (@unsafe.Pointer zero) with no box, so a Ꮡ prefix would reference a nonexistent Ꮡzero/Ꮡv/Ꮡfd (CS0103 — runtime map.go mapaccess1_fat/mapaccess2_fat’s return zero, mem_windows.go, and panic.go readvarintUnsafe’s tuple return). The box form applies only when the returned parameter’s own type is a genuine *T (deref-aliased, so Ꮡp exists); an unsafe.Pointer param returns as-is. (Guarded by the UnsafePointerParamPin extension — the whole-return, tuple-return, and genuine-*T-control shapes, values vs Go.)
The reverse direction — reinterpreting a raw address as a pointer, (*T)(p) where p is an unsafe.Pointer (or uintptr) — is the reinterpret pattern referenced above. Its result is the pointer type ж<T>. A plain (ж<T>)p cast is CS0030: because unsafe.Pointer is Pointer : ж<uintptr>, reaching ж<T> needs the two chained user-defined conversions Pointer → uintptr → ж<T>, and C# performs at most one user-defined conversion in a cast. The converter routes explicitly through uintptr — (ж<T>)(uintptr)(p) — which yields a pointer at p’s address via golib’s explicit operator ж<T>(uintptr value) (with uintptr(Pointer) => Value, the address the pointer holds) — a native-address box, or the very box a managed pointer’s order token names (see A reinterpreted raw address ALIASES native memory instead of boxing a copy). The deref *((*unsafe.Pointer)(k)) then adds .Value: ((ж<@unsafe.Pointer>)(uintptr)(k)).Value — Go’s read of the unsafe.Pointer stored at k. This is the identical routing the dereference path ((*int)(p) inside *(...)) uses via its isPointerCast flag, extended to the two shapes that do not set that flag: a bare call argument atomicwb((*unsafe.Pointer)(ptr), new) (runtime atomic_pointer.go) and an extra-paren deref *((*unsafe.Pointer)(k)) (internal/runtime/maps’ indirect key, *((*unsafe.Pointer)(slotKey)) — convStarExpr’s dereference branch sees a ParenExpr, not the CallExpr, so it never marks the cast). Gated to a pointer-result conversion whose argument is a raw address (unsafe.Pointer/uintptr basic); the pointer-to-named-type value conversion (*Base)(defPtr) (below) has a *T argument, is handled earlier, and is not affected. Like every reinterpret through the uintptr round-trip, this is memory-layout-dependent code whose runtime values are not the contract — golib’s own map<K,V> is what actually runs; the converted map implementation only needs to compile. (Guarded by the UnsafePointerReinterpret behavioral Compile + Target test — both the extra-paren deref and the bare-argument shapes.)
A SAME-TYPE reinterpret (*T)(unsafe.Pointer(p)) where p is already *T collapses to the pointer itself. Converting a *T to unsafe.Pointer and back to the same *T is a no-op identity in Go — the language spec makes (*Builder)(abi.NoEscape(unsafe.Pointer(b))) exactly b.addr = b (strings.Builder’s copy-by-value guard; the type’s own TODO says to revert it to that once escape analysis improves). The uintptr round-trip above is wrong for this shape: a round-trip that copies would make b.addr a fresh box over a copy of the receiver, never reference-equal to it — and the guard’s own b.addr != b self-check would FALSE-PANIC on the second call to any strings.Builder method (a Grow then a WriteString, or strings.Join’s repeated WriteString): panic: strings: illegal use of non-zero Builder copied by value. convCallExpr.pointerReinterpretIdentitySource intercepts this exact shape at the top of the conversion path — a (*T)(…) whose source, after peeling an optional escape-analysis identity wrapper (abi.NoEscape or a package-local noescape, matched by name and unsafe.Pointer→unsafe.Pointer signature), is unsafe.Pointer(p) with p of the identical pointer type *T — and emits p’s box directly (in the isPointer context, so a deref-aliased receiver/param renders Ꮡb, not its value alias b):
internal static void copyCheck(this ж<Builder> Ꮡb) {
ref var b = ref Ꮡb.DerefOrNull();
if (b.addr == nil) {
b.addr = Ꮡb; // was (ж<Builder>)(uintptr)(abi.NoEscape((uintptr)@unsafe.Pointer.FromRef(ref b)))
} else if (b.addr != Ꮡb) {
throw panic("strings: illegal use of non-zero Builder copied by value");
}
}
This preserves pointer identity AND shared storage (a write through the reinterpreted pointer flows back, unlike a copy). A different element type is a genuine reinterpret and keeps the uintptr round-trip; the interception is (*T)-target- and same-element-type-gated (types.Identical(srcElem, targetElem)), so it fires ONLY for the identity. Across the stdlib it gives a small set of sites (strings.Builder.copyCheck, internal/reflectlite, internal/syscall/windows/registry, os, syscall, and some runtime sites) the correct box form, and nothing else changes; the bare unsafe.Pointer(p) pin and the genuine-reinterpret round-trip both remain and stay compile-guarded by the full build. (Guarded by the PointerReinterpretIdentity behavioral output test — a Builder-style copyCheck self-reference called repeatedly must NOT panic, and a genuine copy-by-value MUST still be caught, vs Go — plus the identity-collapse arms of UnsafePointerParamPin (param/receiver/field), PointerSelectorDeref, and PointerCastSliceRange.)
The identity also collapses when the source pointer is reached DIRECTLY — *(*T)(p) / (*T)(p) with p already *T. This is the same no-op, minus the unsafe.Pointer hop: Go’s way of re-reading a pointer at a fixed type. Unrecognised, the deref path would make it worse than the round-trip above: convStarExpr’s casted-pointer-deref branch sets isPointerCast, and taking that flag alone as licence to emit the raw-address bridge (ж<T>)(uintptr)(p) is wrong, because isPointerCast means only “this conversion is the operand of a deref” — it says nothing about the source being an address, and the bridge is only ever correct for one that is. A typed Go pointer is a managed box, and a deref-aliased pointer parameter renders as that box’s value alias, so the (uintptr) leg has no conversion at all:
// Go: func derefStruct(p *Pt) Pt { return *(*Pt)(p) }
internal static Pt derefStruct(ж<Pt> Ꮡp) {
ref var p = ref Ꮡp.Value;
return ~(ж<Pt>)(uintptr)(p); // CS0030: cannot convert 'Pt' to 'uintptr'
}
internal static Pt derefStruct(ж<Pt> Ꮡp) {
return ~Ꮡp; // the box, dereferenced in place
}
pointerReinterpretIdentitySource accepts either source form — the direct pointer, or one unwrapped from unsafe.Pointer(p) — so the identity is intercepted before the bridge is ever considered. Emitting the box is correct for all three uses at once: a value read copies (~Ꮡp, plus the array .Clone() where the element is an array), an lvalue write lands on the real storage ((Ꮡp).Value = …) rather than on the round-trip’s copy, and pointer identity is preserved. Note the recognition must stay pinned to a genuine (*T)(…) conversion — its Fun must denote a type. Matching on argument type alone collapses any one-argument call that takes and returns the same pointer type, silently deleting it (advance(a) → a, Ꮡp.Swap(Ꮡa) → Ꮡa). The corpus-wide footprint is tiny — time.NewTimer/AfterFunc’s (*Timer)(newTimer(…)), where newTimer already returns *Timer, loses a redundant identity cast — because the CS0030 shape needs a pointer parameter, which the stdlib’s own reinterprets never use; the defect bites converted end-user code and behavioral guards. (Guarded by the TypedPointerCastDeref behavioral output test — struct, named-numeric, via-unsafe, non-deref, lvalue, and local-pointer shapes, plus the one-argument-call over-match control — and by the strengthened ArrayCastDerefClone; both verified to FAIL with the fix neutered, with that exact CS0030.)
Still routed through the bridge (a known gap): a typed-pointer source whose element type differs but shares an underlying — Go permits (*T)(p) there — is only partly covered by the named↔named / named↔basic / named↔array re-box routes below. A tag-differing struct pair (types.Identical counts tags, Go’s conversion rule does not), an unnamed-array ↔ named-array pair, and a named ↔ unnamed struct pair all fall through to the raw-address bridge and mis-render. None occurs in the stdlib corpus, and narrowing the bridge gate without a correct box→box route for them merely trades one broken form ((ж<Row>)(uintptr)(p)) for another ((ж<Row>)p), so the gate is left as-is and the shapes are recorded here.
A deref whose starred inner is a func type (or any non-identifier type) — *(*func())(add(…)), runtime panic.go’s deferred-slot read return *(*func())(add(p.slotsPtr, i*…)), true — misses the identifier-gated cast-deref branch and falls to the default deref path, which must wrap the cast before .Value: C# postfix binds tighter than a cast, so a naked .Value re-binds onto the cast’s inner operand ((ж<Action>)(uintptr)(add(…)).Value reads the inner @unsafe.Pointer’s uintptr — CS0029 ж<Action>→Action in the tuple return). The default deref wraps any type-conversion operand: (((ж<Action>)(uintptr)(add(…))).Value, true). Indexing a reinterpret result directly needs the same wrap: for (*[2]uint64)(x)[0] = 0 (runtime malloc.go), appending the pointer-to-array auto-deref .Value and the index to the cast render — (ж<array<uint64>>)(uintptr)(x).Value[0] — would read the inner @unsafe.Pointer’s uintptr and index it (CS0021), so the index emission wraps a type-conversion base the same way: ((ж<array<uint64>>)(uintptr)(x)).Value[0]. (Guarded by the UnsafePointerReinterpret extensions — the func-type deref in a tuple return and the indexed reinterpret write/read.)
The unsafe builtins unsafe.Add, unsafe.Slice, and unsafe.String accept a length/offset of any integer type (Go’s IntegerType constraint, which includes uintptr/uint). golib’s implementations therefore take a generic IBinaryInteger length, truncated to the int offset — so unsafe.Slice(p, uintptrLen) binds without an explicit cast (a plain nint parameter rejected a uintptr/uint argument with CS1503). (Guarded by UnsafeBuiltinIntegerLen.)
Passing an unsafe.Pointer argument to an unsafe.Pointer parameter keeps the @unsafe.Pointer struct value — add(p, x), not add(p.Value, x). The struct is an exact match for the parameter. (Guarded by UnsafePointerArgPassing.)
Array-backed defined types reinterpret through storage-sharing Value refs, not value copies. The fiat field-arithmetic shape (crypto/internal/edwards25519 scalar.go) reinterprets &s.s (a fiatScalarMontgomeryDomainFieldElement, written directly over [4]uint64) as (*[4]uint64) — and as its sibling (*fiatScalarNonMontgomeryDomainFieldElement) — then writes element-wise through the reinterpreted pointer (fiatScalarFromBytes parses INTO &s.s on a virgin receiver). Neither the copy-boxing named↔named route (each /*[N]elem*/ wrapper converts only to array<E>; a sibling cast needs two chained user conversions — CS0030) nor a plain ж<> cast (distinct instantiations) works, and any copy-based route would materialize the wrapper’s lazy backing on a temp and orphan every write. The emission derefs through the ref-returning ж<T>.Value and invokes the wrapper’s Value property in place — Ꮡ((Ꮡs.of(Scalar.Ꮡs)).Value.Value) (underlying-array form) / Ꮡ((nonMont)((…).Value.Value)) (sibling form, one implicit conversion from array<E>) — materializing the backing on the ORIGINAL storage and boxing an array<E> struct that shares its T[]: element reads and writes flow through. Gating consults the type’s written RHS (a new per-package pre-pass records each TypeSpec’s declared right-hand side, which Named.Underlying()’s full resolution loses): only types written directly over an unnamed array take this route, so chain-defined view wrappers (type pallocBits pageBits) keep the existing copy-box route byte-identically; the same written-RHS gate lets isTypeConversion claim the pointer-to-type-literal target (*[4]uint64)(…) (no types.Object exists for a composite type) without disturbing the pointer-cast slice form ((*[1<<20]Method)(p)[:n:n], internal/abi). Caveat (documented, no stdlib site): a whole-value write through the reinterpreted box (*p = q) rebinds only the boxed struct. (Guarded by the NamedArrayWrapper extensions — a virgin-field write through the underlying reinterpret, a sibling reinterpret aliasing the same storage read-during-write, and a heap-boxed local, all output-compared vs Go.)
The uintptr → ж<T> raw-address reinterpret operator is explicit by design. It mints a pointer from an arbitrary address (the runtime-unsafe reinterpret seam) — never something to happen silently, and every converter-emitted reinterpret already uses explicit cast syntax ((ж<T>)(uintptr)(p)). As an implicit conversion it also poisoned overload resolution: a uintptr argument converted to both an @unsafe.Pointer parameter (via the numeric uintptr ↔ Pointer operators, which stay implicit) and any ж<T> parameter, so a free function and a same-named pointer-receiver method — runtime’s func add(p unsafe.Pointer, x uintptr) (stubs.go) vs func (p *notInHeap) add(bytes uintptr) (malloc.go), both emitted as static add overloads in the package class — were ambiguous (CS0121) at every free-call site whose argument is a pin of a boxless receiver: inside a ref method, unsafe.Pointer(b) emits the uintptr-typed (uintptr)@unsafe.Pointer.FromRef(ref b) (runtime mprof.go’s stack-record walkers, add(unsafe.Pointer(b), …); at Go 1.23 also map.go’s b.keys()/b.overflow()/b.setoverflow() ). With the operator explicit, the uintptr argument binds only the @unsafe.Pointer overload. The reverse ж<T> → uintptr (box → address) operator remains implicit — producing a number is not a silent deref. (Guarded by the FuncVsMethodOverload behavioral output test — the free add + direct-ж method add overload pair with the boxless-receiver pin call shape, plus both method-call forms, values vs Go.)
A cross-package type reference emits its using <alias> = <namespace>; even when the file did not import the package under a usable name. A foreign type renders in short-alias form — pkg.Type (time.Duration, abi.Kind) for a named type, @unsafe.Pointer for the unsafe.Pointer basic — which resolves only through a file-local alias (using time = time_package;, using @unsafe = unsafe_package;). That alias is normally generated from a canonical (unaliased) import, but a file can reference a foreign type with no such import through three routes: type inference — a same-package function returns a foreign type, so the caller infers a local of that type but never writes pkg. and need not import the package (runtime preempt.go: fd := funcdata(f, i), where funcdata returns unsafe.Pointer); a blank import (_ "pkg", side-effects-only — no using is emitted for it at all: a using _ = <ns>; would hijack C#’s _ DISCARD for the whole file, so a deconstruction discard ((w, _) = w.ensure(…), runtime tracetime.go) would bind the namespace alias instead (CS0118 + CS0029); the import is recorded as a comment, and a genuine type reference still gets its canonical alias from this machinery — e.g. symtabinl.go’s _ "unsafe" for //go:linkname); or an aliased import (import u "unsafe", whose alias u differs from the canonical pkg.Name() prefix the type reference uses). Without a supplied alias each is CS0246, so the converter walks every emitted type (collectTypePackages, called from getAliasQualifiedTypeName — named types by pkg.Path(), an unsafe.Pointer basic by the pseudo-path "unsafe", recursing through pointer/slice/array/map/chan/generic/func-signature so a []time.Duration element registers too) and, at file close (visitFile), supplies the canonical using <alias> = <namespace>; for every referenced foreign package the file did not already import canonically. It is idempotent-safe — a canonical import records its path in canonicalAliasImported, so visitFile never re-emits (duplicates) it — and a non-canonical alias (using u = unsafe_package;) coexists with the added canonical one without conflict. It is also collision-guarded: the synthesized using <alias> = <namespace>; is skipped when its canonical <alias> was already bound to a different namespace by a real import — cryptobyte’s asn1.go imports both encoding_asn1 "encoding/asn1" (referenced by type, so it reaches this loop) and the subpackage .../cryptobyte/asn1 (unaliased → alias asn1), so synthesizing using asn1 = encoding.asn1_package would duplicate the subpackage’s using asn1 (CS1537). The real imports’ emitted aliases are tracked per file (importAliasesEmitted); the parent stays reachable through its encoding_asn1 alias, so skipping the canonical one is safe (a non-colliding canonical alias is still supplied — no churn). (The separate defect that the type reference itself renders asn1.ObjectIdentifier rather than the file’s encoding_asn1.ObjectIdentifier — getAliasQualifiedTypeName uses the canonical alias, not the file’s non-canonical one — is tracked independently.) This is the type-reference analog of the method-call addMethodPackageNamespaceUsing. (Guarded by UnsafePointerInferredNoImport — the unsafe.Pointer basic arm, scalar/composite/blank-import variants — and InferredForeignTypeNoImport — the generic named arm, an inferred *strings.Reader in an fmt-only consumer.)
That supplied alias must carry the collision rename. When the referenced package’s using alias is Δ-renamed because a same-named CHILD namespace is visible from the import closure (go.sync, contributed by sync/atomic; go.unicode, by unicode/utf8 — the same CS0576 collision that renames a canonical import’s alias, above), getAliasedTypeName already renders the short-form type reference through the renamed qualifier (Δsync.Mutex, Δunicode.Range16). Composed from packageUsingAlias alone — the bare, unrenamed name — the visitFile supply loop’s alias would be using sync = sync_package; (or using unicode = unicode_package;) while the reference reads Δsync.Mutex: the alias would bind nothing (CS0246), and would itself collide with the child namespace (CS0576). So the supplied alias is routed through importQualifier (getSanitizedImport(importQualifier(alias)), the same rename every canonical import applies), so the emitted using Δsync = sync_package; matches the reference. importQualifier is a no-op for any package whose alias is not renamed, so a non-colliding supplied alias stays byte-identical. The trigger is a file that reaches a renamed package’s type through the supply route rather than a canonical import — overwhelmingly a dot import (. "sync" / . "unicode"), where the dot brings names in via using static yet the converter still qualifies the type, and no canonical using <pkg> = … is emitted to carry the rename. Production stdlib code essentially never dot-imports, so a wrong supplied alias stays latent as an unused one (reflect’s value.go/makefunc.go infer sync without importing it, and never reference the alias); it matters in an EXTERNAL (_test) variant that dot-imports the package under test, which unicode’s letter_test.go does (. "unicode" + qualified Range16/RangeTable/CaseRange). (Guarded by the DotImportRenamedPackage behavioral test — . "sync" with a *Mutex type reference forcing the qualified Δsync.Mutex position, output-compared vs Go; neutering the fix reproduces the reported CS0246: 'Δsync' could not be found.)
uintptr(unsafe.Pointer(x)) builds no Pointer object — the operand converts straight to uintptr. Rendering Go’s most common syscall idiom as (uintptr)new @unsafe.Pointer(x) would build a provably dead object. golib’s Pointer is a ж<uintptr> whose value-taking constructor takes only a uintptr, so the operand is ALREADY converted — by implicit operator uintptr(ж<T>), the very operator the enclosing cast would use — before the wrapper exists; the wrapper stores that finished number in its own one-element slot, and the cast reads it straight back out. The round-trip is the identity, exactly: uintptr(Pointer) returns value.IsNull ? 0 : value.Value, and the constructor marks the box nil precisely when the address is 0 (base(value, value == 0), with ж<uintptr>’s value-peeking IsNull arm unable to fire for a value-typed pointee). So the wrapper is not emitted. (Outside this peephole a POINTER operand of unsafe.Pointer(x) mints through a retaining door such as @unsafe.Pointer.FromPinnedBox(x), and Pointer has an INilPointer constructor; neither changes the peephole’s own result.) In a syscall funnel call the operand is also captured for the statement-scoped GC.KeepAlive drain described below:
r1, _, e1 := SyscallN(procCopySid.Addr(), uintptr(destSidLen), uintptr(unsafe.Pointer(destSid)), uintptr(unsafe.Pointer(srcSid)))
var ᴋ0 = ᏑdestSid;
var ᴋ1 = ᏑsrcSid;
var (r1, _, e1) = SyscallN(procCopySid.Addr(), (uintptr)destSidLen, (uintptr)ᴋ0, (uintptr)ᴋ1);
System.GC.KeepAlive(ᴋ0);
System.GC.KeepAlive(ᴋ1);
The pin is what makes this safe to elide, and it is not the wrapper’s. Every managed address handed to native code is pinned for the POINTER’s lifetime (EVERY managed address handed to native code is pinned…, above) — but EnsureStableAddress / pinnedArrayData set m_pin on the operand box, released when the operand is collected. The Pointer wrapper owns no pin, holds no reference to the operand, and tracks no lifetime; its ж<uintptr> slot holds a number. Eliding it therefore cannot shorten any pin, move any address, or change any value — a strictly-dead allocation, which is why this is a peephole and not a semantic change.
The rule is keyed at the wrapper’s own emission site (unsafePointerBoxEmission, marked per-CallExpr by markDeadUnsafePointerBox from the enclosing conversion), so it is self-limiting: the unsafe.Pointer arms that render a raw address by other means — @unsafe.Pointer.FromRef(ref x) for a deref-aliased pointer receiver, ((@unsafe.Pointer)(uintptr)v) for a named uintptr/pointer operand, and (@unsafe.Pointer)default! for the literal nil — never consult the mark and are byte-identical. The enclosing conversion must be one that renders a (uintptr) cast around the operand, which is the basic uintptr(…) target and the named-over-uintptr target that hops through its underlying (Handle(…), syscall/security_windows.cs’s LocalFree defers). An operand that binds looser than a cast is parenthesized (unsafe.Pointer(uintptr(p) + off)); an address-of operand is not, because &x renders as the primary box form Ꮡx / Ꮡ(…) / Ꮡs.at<T>(i).
The peephole takes three wrapper objects off every zsyscall wrapper call chain; a seeded A/B whole-stdlib reconvert changed exactly those sites and nothing else. (The hand-owned crypto/internal/fips140/subtle/xor_generic.cs keeps its three sites, as it must: the converter regenerates it into the xor_generic.cs.auto review sibling instead of over it.) (Guarded by the UintptrUnsafePointerIdiom behavioral output test — address identity across two takes of one global, a pointer parameter, the nil pointer’s 0, an array-element stride, an existing unsafe.Pointer value round-trip, a dereferenced pointer-to-pointer operand, and a write read back through the same pointer, all vs go run — plus the re-baselined NilPointerUintptr, NilPointerParamUnsafePointer, FixedArrayBufferPointer and UnsafeOperations goldens, which diverge the moment the peephole is neutered.)
Sixth member — the DATAGRAM pair, and the first where the SEND and the RECEIVE need different remedies. internal/syscall/windows’ WSASendtoInet4/6 are bodyless linkname partials, so without a hand-own every UDP send on Windows throws the PartialStubGenerator’s stub.
The SEND is the family’s ordinary shape and takes the family’s ordinary remedy: the dead generated body passed Ꮡto.sockaddr() — a pointer INTO A MANAGED BOX — straight to WSASendTo, so the hand-own writes the address image into a stackalloc buffer through syscall’s GoWriteNativeSockaddrInet4/6 seam first. One definition of the layout, shared with the Linux half (internal/syscall/unix/linux/net_linux_impl.cs), so neither can drift.
What is new is the SUBMIT. A Windows datagram send is an overlapped operation, and the three helpers it needs (operationFor/Rearm/stageBuffers) are private to syscall’s WSA hand-own while the declarations they serve live in internal/syscall/windows. Publishing them would put a Go-shaped seam on syscall’s public surface, which that file’s own header exists to refuse. The resolution (netpoll design §4.7) extends golib’s platform-neutral GoAsyncIO by two primitives stated without naming Winsock:
public static nuint RearmOperation(nuint descriptor, object key, nint mode); // the native OVERLAPPED, prepared for a new submit
public static nuint StageOperationBuffer(object key, int byteCount); // N bytes whose lifetime is the operation's
The caller writes the WSABUF bytes into the staged memory itself, exactly as it writes the sockaddr image. syscall’s own helpers become in-package callers of the same two primitives, so there is ONE record store rather than two that must agree — which is the property that makes this an extension rather than a parallel mechanism.
The RECEIVE is the same family and a DIFFERENT remedy, and the distinction is worth carrying. The generated WSARecvFrom hands the kernel the pinned address of a MANAGED RawSockaddrAny (40 bytes, with array<int8> references where Go has inline octets) and tells it the buffer is 116 bytes, so the kernel’s write would overflow the managed object into the GC heap. Because a received address arrives asynchronously, the stack-buffer remedy does not apply: syscall’s hand-owned WSARecvFrom (zsyscall_windows_wsa_impl.cs) lets the kernel write into the operation’s native staging and transcribes that image into the managed RawSockaddrAny at HARVEST time — where internal/syscall/windows.WSAGetOverlappedResult observes the completion — the same pair AcceptEx uses for its output buffer; a synchronous completion is transcribed inline. (Guarded by UdpLoopbackRoundTrip.)
Pointer-derived funnel arguments and bridged unsafe.Pointer arguments keep their box alive across the call — the statement-scoped GC.KeepAlive drain
Go’s //go:uintptrkeepalive contract (cmd/compile’s escape.rewriteArgument, cmd/compile/internal/escape/call.go): an argument that is a uintptr(<operand of type unsafe.Pointer>) conversion to a syscall funnel keeps the pointed-to object alive for the duration of the call — the inline uintptr(unsafe.Pointer(&x)), the two-step _p0 = unsafe.Pointer(&p[0]) … uintptr(_p0) that mksyscall emits for every []byte argument, and a pointer-typed variable passed as uintptr(unsafe.Pointer(p)) alike. The funnel set is package-qualified through go/types (syscallFunnelCall, syscallKeepAliveAnalysis.go): syscall.Syscall/Syscall6/9/12/15/18/SyscallN/RawSyscall/RawSyscall6, internal/runtime/syscall.Syscall6, darwin’s lowercase libc trampolines syscall, syscall6, syscall6X, syscallX, syscallPtr, rawSyscall, rawSyscall6 (runtime linknames declared by package syscall), and crypto/x509/internal/macos’s own syscall (corefoundation.go). In C# the argument is a NUMBER over a pinned box, and nothing references the box once the argument is evaluated, so a collection during the call retires the pin the kernel is reading through. Emitted form: each pointer-derived argument is captured into a ᴋN temp declared before the statement, the call takes (uintptr)ᴋN, and System.GC.KeepAlive(ᴋN); follows the statement (convSyscallFunnelCall, drained by visitStmt):
func recvfrom(fd int, p []byte, flags int, from *RawSockaddrAny, fromlen *_Socklen) (n int, err error) {
var _p0 unsafe.Pointer
if len(p) > 0 {
_p0 = unsafe.Pointer(&p[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := syscall6(abi.FuncPCABI0(libc_recvfrom_trampoline), uintptr(fd), uintptr(_p0), uintptr(len(p)), uintptr(flags), uintptr(unsafe.Pointer(from)), uintptr(unsafe.Pointer(fromlen)))
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
internal static (nint n, error err) recvfrom(nint fd, slice<byte> p, nint flags, ж<RawSockaddrAny> Ꮡfrom, ж<_Socklen> Ꮡfromlen) {
nint n = default!;
error err = default!;
@unsafe.Pointer _p0 = default!;
if (len(p) > 0){
_p0 = @unsafe.Pointer.FromPinnedBox(Ꮡ(p, 0));
} else {
_p0 = @unsafe.Pointer.FromBox(Ꮡ_zero);
}
var ᴋ16 = _p0;
var ᴋ17 = Ꮡfrom;
var ᴋ18 = Ꮡfromlen;
var (r0, _, e1) = syscall6(abi.FuncPCABI0(libc_recvfrom_trampoline), (uintptr)fd, (uintptr)ᴋ16, (uintptr)len(p), (uintptr)flags, (uintptr)ᴋ17, (uintptr)ᴋ18);
System.GC.KeepAlive(ᴋ16);
System.GC.KeepAlive(ᴋ17);
System.GC.KeepAlive(ᴋ18);
n = (nint)r0;
if (e1 != 0) {
err = errnoErr(e1);
}
return (n, err);
}
The MANAGED-callee member of the same class (runtime’s TestSmhasherWindowed is the corpus case ): the converter bridges every value-consumed call whose RESULT is unsafe.Pointer with (uintptr) (convCallExpr), so memhash32(noescape(unsafe.Pointer(&i)), seed) hands the callee a number with the Pointer’s retained source stripped, and the frame-minted box Ꮡi has no reference left after FromPinnedBox returns. A bridged unsafe.Pointer-returning CALL argument (not a conversion, not a builtin) over unsafe.Pointer(&x) of a frame-minted local — a parameter re-minted as a box included — names Ꮡx for a System.GC.KeepAlive(Ꮡx) after the statement (bridgedWrapperKeepAliveBoxes); a return f(…) hoists the call into a ᴛN temp so the KeepAlive sits between the call and the return:
func int32Hash(i uint32, seed uintptr) uintptr {
return memhash32(noescape(unsafe.Pointer(&i)), seed)
}
internal static uintptr int32Hash(uint32 iʗp, uintptr seed) {
ref var i = ref heap(iʗp, out var Ꮡi);
var ᴛ2 = memhash32((uintptr)noescape(@unsafe.Pointer.FromPinnedBox(Ꮡi)), seed);
System.GC.KeepAlive(Ꮡi);
return ᴛ2;
}
Boundaries, each guarded in syscallFunnelSet_test.go: a BARE f(unsafe.Pointer(&x)) argument is not bridged (the Pointer retains its box through the call) and records nothing; a package-level variable’s box is a static field and records nothing; an outer CONVERSION (n := uintptr(noescape(unsafe.Pointer(&i))), runtime’s stdcallN / mstart0 shape) records nothing, because a stored number is outside the class — a KeepAlive after the store would hold the box exactly as far as the store. The drain is per STATEMENT: a box named inside an if condition is kept alive by the first statement after the call (inside the body or after the if — a use on any path after the call keeps the local live at it); a for INIT/POST clause, emitted inside the C# header where no statement can follow, is refused by name (rejectForClauseKeepAlive), as a deferred or spawned call is (the contract is statement-scoped and a defer runs at unwind); a function literal converts its body against an EMPTY pending list and restores the enclosing statement’s afterwards, so a box the enclosing call named before its literal argument was converted is never drained inside the lambda. src/syscall-keepalive-census.ps1 walks the converted emission: every captured temp has exactly one KeepAlive (arm 1), hand-own pointer arguments are held across their call (arm 2), and no raw pointer-derived funnel argument remains (arm 3) .
A numeric value pun READ is a bitcast — *(*uint64)(unsafe.Pointer(&f)) boxes nothing
Go’s math.Float64bits is one line, return *(*uint64)(unsafe.Pointer(&f)), and Go compiles it to a
register move. The general pointer-reinterpret emission (this section’s neighbours) would treat it like
any other address-of:
-
fwould be heap-boxed because its address is taken; - it would be read back through
~Ꮡf.Reinterpret<float64, uint64>().
That is three counted objects per call where Go has none: the box, its pinnable slot, and the
reinterpreting field reference (GolibTests’ ValuePunBitcastTests measures that body at exactly 3), paid
by every caller, including encoding/binary, strconv’s float formatting, log/slog’s float Values and gob.
A pun that is only READ, between two same-size predeclared sized numerics (int8…int64, uint8…uint64,
float32, float64), renders as golib’s value bitcast (Unsafe.BitCast) instead. Its &x does not count as
address-taken, so x stays an ordinary parameter or local when that was its only address use:
func Float64bits(f float64) uint64 { return *(*uint64)(unsafe.Pointer(&f)) }
public static uint64 Float64bits(float64 f) {
return bitcast<float64, uint64>(f);
}
The shapes that keep the aliasing reinterpret, deliberately:
- a pun that is WRITTEN, e.g. runtime/minmax.go’s
*(*uint32)(unsafe.Pointer(&x)) |= …, whose write must land inx; - a pun whose result is addressed;
- a named type;
-
int/uint/uintptr, whose width is the target’s; -
bool, complex and every composite.
A pun read whose x is ALSO addressed elsewhere still bitcasts, and its box stays for that other use.
The recognition is a per-package pre-pass (valuePunOperations.go), and escape analysis skips the &x
it consumes. The //go:cgo_unsafe_args lift is the precedent for both.
Stdlib footprint: the production pun reads, identical on windows, linux and darwin, are:
- math’s float bit conversions;
- runtime’s
float64bits/float64frombitsand its two histogram infinities; - reflect’s two float32 register moves;
- internal/runtime/atomic’s
Float64.Load/Store.
The census is in docs/phase4/probes/c2-value-pun-census/.
Guarded by:
- the
ValuePunBitsbehavioral test (an accepted read, an address kept elsewhere, and a write), andUnsafeOperations, whose golden moves on its own three pun reads; - the converter’s
TestValuePunReadRendersAsABitcastWithoutABox, controlled both ways; - GolibTests’
ValuePunBitcastTests.
The pointer-word read — *(*unsafe.Pointer)(unsafe.Pointer(&x)) emits a CARRYING pointer, never a byte pun
Go’s idiom for reading the pointer word stored at some location — time.syncTimer’s
*(*unsafe.Pointer)(unsafe.Pointer(&c)) reading a channel’s header word, runtime/stack.go’s
*(*unsafe.Pointer)(&pp) re-typing a uintptr — has ONE destination the managed storage
reinterpret can never serve: unsafe.Pointer itself is a CLASS in the surrogate model, so golib’s
alias gate refuses it, and a fallback that deref-copies whatever bits sit in the source’s first
reference-sized slot INTO a Pointer reference materializes a reference from bytes — a CLR
type-safety break: junk dispatch on a quiet heap, an AccessViolationException when the punned bits
land unmapped, reachable on every NewTimer.
reinterpretManagedEmission (convCallExpr.go) therefore special-cases a target pointee of the
unsafe.Pointer basic and emits the word the two ways the managed model can carry one:
u := uintptr(0xC0FFEE)
q := *(*unsafe.Pointer)(unsafe.Pointer(&u)) // the word IS the number
c := make(chan int, 1)
p := *(*unsafe.Pointer)(unsafe.Pointer(&c)) // the word is a REFERENCE
@unsafe.Pointer q = ~Ꮡ(new @unsafe.Pointer(~Ꮡu)); // exact fidelity: Pointer(value)
@unsafe.Pointer p = ~Ꮡ(new @unsafe.Pointer((uintptr)Ꮡc)); // the source BOX's pin token
The uintptr arm is exact Go semantics. The managed arm carries the source box’s
ManagedPointerTokens pin token — non-nil for a live value, stable for the box’s lifetime, and
provenance-resolvable back to the storage it names. One corner is knowingly inexact and stated at
the emission: a nil channel’s word is 0 in Go while a box token is non-zero; the one stdlib
consumer (newTimer) reads only the nil-bit and recomputes it from the GODEBUG setting, and no
stdlib site passes a nil channel through this shape.
Deliberately NO golib-side guard backs this up, and the history is the load-bearing part: a
runtime refusal (return a zero-holding box for any reference-typed destination) was tried first and
the full behavioral suite failed PointerCastSliceRange — the ж<T> → ж<U> DOUBLE-pointer pun
(reflect MapOf’s **(**mapType)(unsafe.Pointer(&imap))) reads one ж instantiation’s reference
slot as another and works precisely because the generic layouts coincide, a distinction golib
cannot draw generically and the converter draws exactly. Guarded by
tests/Behavioral/UnsafePointerWordRead (both arms vs go run, token stability across re-reads);
the corpus regen moved every member of the shape (sleep.cs and the runtime sites) and nothing
else . The runtime sites are dormant raw-metal; a site that WRITES
through the derived pointer
stores into a detached box rather than corrupting punned memory; real write-through semantics for it are
not implemented.
A pointer reflect handed out as an unsafe.Pointer must convert BACK — the order token is remembered, not redefined
A Go pointer to managed storage has no machine address to report, so every projection of one to a scalar answers with a stable order token instead: INilPointer.PointerOrderToken, whose own remarks say plainly that tokens “are order keys, never an identity substitute”. reflect.Value.Pointer and reflect.Value.UnsafePointer both project through it (reflect/value_impl.cs’s reflectPointerToken), and that contract is exactly right for the consumers it was written for — fmt’s %p, and internal/fmtsort’s ordering of pointer-keyed map entries, which compares two tokens arithmetically.
It is not sufficient for the other direction Go permits: converting the scalar back to a pointer and dereferencing it. go/types’ own test suite does precisely that to reach the unexported Config._Trace field:
func boolFieldAddr(conf *Config, name string) *bool {
v := reflect.Indirect(reflect.ValueOf(conf))
return (*bool)(v.FieldByName(name).Addr().UnsafePointer())
}
*boolFieldAddr(&conf, "_Trace") = manual && testing.Verbose() // check_test.go:166
which converts to
internal static ж<bool> boolFieldAddr(ж<types.Config> Ꮡconf, @string name) {
var v = reflect.Indirect(reflect.ValueOf(Ꮡconf.OrTypedNil()));
return (ж<bool>)(uintptr)(v.FieldByName(name).Addr().UnsafePointer());
}
ж<T>’s explicit operator ж<T>(uintptr) builds a native-address box over whatever number it is handed (m_nativeAddr), so the .Value store writes a bool at the numeric value of an order token — an access violation when that page is not mapped, and silent heap corruption when it is. Without the table below it kills go/types’ converted test host outright at the first test that reaches the idiom, TestCheck/blank.go; the fault arrives as a bare System.AccessViolationException in testFilesImpl with no frame below it, because the faulting store is inlined at the call site.
The information needed to do better is never actually lost. reflect.Value.Addr surfaces the real aliasing box — an addressable Value already carries addrBox, minted by GoReflect.FieldAliasBox — and only the projection to a scalar discards it. So golib’s ManagedPointerTokens (golib/ж.PointerTokens.cs) remembers the association the projection drops: the token that was handed out, and the box it named. reflectPointerToken registers on the way out, and the uintptr → ж<T> operator consults the table first, so a token that came from there recovers its own box and aliases the original storage exactly as Go’s pointer would. Everything else keeps the native-address route.
Two properties are deliberate. The token VALUE does not change. The obvious alternative — minting self-identifying handles from a reserved numeric range — would also change what %p prints and what order pointer-keyed maps print in, because those read the very same token; carrying the association out of band instead leaves every existing observable byte-identical, and costs one dictionary probe on a conversion that was already allocating an object. Entries are weak, and verified on resolve: the table must never be the reason a box stays alive, and a resolve re-derives the box’s current token and requires equality, so a stale entry cannot answer. The type-descriptor path (typeDescriptorOrderToken, which packs type NAMES so fmtsort orders them lexically) returns before registration and is untouched — those tokens are shared by every descriptor with the same name and are not identities to recover.
Corpus footprint of the idiom: in all of GOROOT it is go/types’ check_test.go (bool and string), its cmd/compile/internal/types2 twin, and four descriptor-walking sites in reflect/type.go; sync/atomic’s two uses only test alignment (p&7 != 0) and never dereference. (Guarded by the ReflectFieldAddrWrite behavioral output test — writes to a bool, a string and an int field through Addr().UnsafePointer(), each read back through the ORIGINAL struct to prove aliasing rather than a detached copy, two derivations of one field compared for pointer equality, and the untouched neighbours asserted unchanged, all vs go run. It faults with an access violation on pre-fix golib.)
An OPAQUE *struct{} conversion mints a recoverable token — syscall.Pointer(unsafe.Pointer(p)) keeps its referent
Windows type definitions use type Pointer *struct{} for a “pointer to one of many types” field — CERT_CHAIN_POLICY_PARA.pvExtraPolicyPara, WSAMsg.Name — and Go populates it with T(unsafe.Pointer(p)) over whatever record the API expects. The numeric chain (T)(ж<EmptyStruct>)(uintptr)(new @unsafe.Pointer(p)) would project p’s box to a scalar at the @unsafe.Pointer constructor (the class holds only a number — Pointer : ж<uintptr>). For a pointee carrying managed references that scalar is a transient GC-heap address with no recoverable box behind it: golib’s uintptr operator pins only reference-free storage, so the number that reaches the boundary is neither stable nor resolvable, and the referent’s liveness ends at the JIT’s discretion.
The corpus case is crypto/x509’s checkChainSSLServerPolicy. Its SSLExtraCertChainPolicyPara holds ServerName *uint16, a managed reference, so through the numeric chain the SSL policy parameter would cross into CertVerifyCertificateChainPolicy as a wrong-layout transient address: Fatal error 0xC0000005 inside Syscall6.
The conversion emits golib’s referent-preserving mint whenever the target’s underlying type is *struct{} and the source is unsafe.Pointer(p) over a Go pointer whose box the emitter has in hand:
sslPara := &syscall.SSLExtraCertChainPolicyPara{ AuthType: syscall.AUTHTYPE_SERVER, ServerName: servernamep }
para := &syscall.CertChainPolicyPara{ ExtraPolicyPara: (syscall.Pointer)(unsafe.Pointer(sslPara)) }
var sslPara = Ꮡ(new syscall.SSLExtraCertChainPolicyPara(AuthType: syscall.AUTHTYPE_SERVER, ServerName: servernamep));
var para = Ꮡ(new syscall.CertChainPolicyPara(ExtraPolicyPara: ((syscall.Pointer)ManagedPointerTokens.MintOpaque(sslPara))));
ManagedPointerTokens.MintOpaque (golib/ж.PointerTokens.cs) keeps the numeric route byte for byte for every pointee that route already answered exactly — nil is 0, a native-backed box is its real address, a reference-free pointee pins and reports stable storage — and diverges only for the reference-bearing class, where three things happen at the mint: the scalar becomes the box’s own PointerOrderToken; the token is Registered so a boundary wrapper recovers the box with Resolve (the same round trip the reflect projection above and the ADDRINFOW hand-own’s sockaddr fields take — this is the table’s third minter); and the minted ж<EmptyStruct> holds the referent reachable for its own lifetime through a ConditionalWeakTable, so the opaque pointer keeps its pointee alive exactly as the Go pointer it stands for would — the referent is otherwise reachable only through a local the JIT is free to retire before the syscall that consumes the token.
The consuming side is syscall’s hand-owned CertVerifyCertificateChainPolicy (zsyscall_windows_certchain_impl.cs): a scalar the table recognizes as an SSLExtraCertChainPolicyPara box is transcribed — server name and all — into native mirrors for exactly the duration of the call; a scalar it does not recognize passes through unchanged, which is both the nil pointer and any genuinely native (or pinned reference-free) address, for which pass-through is the correct native call.
Scope is deliberate on three edges. The target must be *struct{}-underlying: such a type names an opaque pointer by construction (there is nothing to dereference), so no reader can depend on the scalar being a dereferenceable address; wider named-pointer targets keep their existing routes. The source must be the unsafe.Pointer(p) call form — a stored unsafe.Pointer variable or a uintptr has already lost its referent and keeps the numeric chain. And the emission-level fix, rather than a hand-own of the one x509 function, is what covers every author of the Go shape — internal/poll’s six WSAMsg.Name mints (whose RawSockaddrAny referents stay alive for the message’s lifetime, ready for the WSA-msg wrappers), converted test suites, and user code. (Guarded by the SystemCertVerify behavioral output test’s policy rows, which drive the same mint from test code against a trusted-by-waiver chain: with CERT_CHAIN_POLICY_ALLOW_UNKNOWN_CA_FLAG set, a MATCHING server name answers 0 and a MISMATCHED one answers CERT_E_CN_NO_MATCH — an answer crypt32 can only give if the name crossed the boundary intact — and with nothing waived the same chain answers CERT_E_UNTRUSTEDROOT. Pre-fix, the first policy call dies with the access violation above.)
Pointer DISPLAY never dereferences out-of-range; unsafe.StringData("") is nil
Printing a pointer through PrintPointer (what ж<T>.ToString() and builtin print/println use; Go’s own
%p goes through the converted fmt) only needs an address-like 0x… token, and reading ptr.Value to derive
one would fail —
and an array/slice-ELEMENT reference can legally sit outside its backing store’s valid range (the
zero index of an EMPTY pinned buffer, or one-past-the-end pointer arithmetic), where that read
throws IndexOutOfRangeException and kills the host (strings’ TestClone).
PrintPointer checks an element reference’s index against its backing store first and prints
the BACKING STORE’s identity when the element is unreadable — stable per pointer box, never a
throw. Relatedly, unsafe.StringData of an EMPTY string returns nil: Go documents the
empty-string result as unspecified-may-be-nil, its runtime returns nil (probed — so distinct empty
strings’ data pointers compare EQUAL, which TestClone asserts), and golib’s
pin-a-fresh-buffer-per-call implementation could never satisfy that identity. Addresses differ run
to run, so behavioral coverage checks printed SHAPE and nil-identity (UnsafePointerPrint); the
out-of-range print itself has no Go-parity spelling from converted code (the
unsafe.Add-through-unsafe.Pointer seam loses the element box), so that property is guarded at
the golib level by GolibTests.PointerPrintTests — a golib UNIT-test project (beside
ChannelTests under src/tests/) for runtime properties no Go↔C# output comparison can
reach.
unsafe.String(ptr, 0) reads nothing, so it must not pin ptr either. That same empty-buffer
element reference reaches the string builtin, by a route the standard library takes constantly on
Windows: syscall.UTF16ToString truncates its argument at the first NUL and returns
unsafe.String(unsafe.SliceData(buf), len(buf)), so an all-NUL [N]uint16 — every unset
WCHAR field of a Win32 record (WIN32_FIND_DATAW.cAlternateFileName on a volume with 8.3 name
generation disabled, MIB_IFROW.wszName, PROCESSENTRY32.szExeFile, STARTUPINFO.lpDesktop) —
arrives as a zero-length buf. SliceData is documented to return a non-nil pointer to an
unspecified address for a non-nil slice of capacity 0 (only a nil slice yields nil), which this
model materializes as an index-0 box into a zero-length backing array; pinning
that referent (fixed (byte* p = &ptr.Value)) to build the string would throw
IndexOutOfRangeException where Go returns "", so unsafe.String returns the empty string for the zero-length case
before the pointer is touched, which is precisely Go’s rule: a run-time panic occurs only when
ptr is nil and len is not zero, so a length of zero dereferences nothing whatever the pointer
is. SliceData is deliberately left alone — Go specifies the non-nil-pointer-to-unspecified-address
result for cap == 0, so returning nil there to dodge the deref would trade a throw for a wrong
answer. unsafe.Slice(ptr, 0) needs no matching guard: its element-window route
(golib’s ElementWindow.TryGet) already yields an empty aliasing window over a zero-length backing without a
deref. (Guarded by the UnsafeStringEmpty behavioral output test — the all-NUL [14]/[260]
buffers, terminated/unterminated/leading-NUL controls that keep truncation honest, and the
SliceData/StringData zero-length matrix with non-zero-length positive controls, compared vs
go run; verified to throw with the guard removed.)
unsafe.SliceData is an INTERIOR POINTER, not a pin. Go defines it as &slice[:1][0], so the
faithful model is the array-element reference Ꮡ(s, 0) — the exact box the converter emits for
&s[0]. A pinned-buffer box over slice.buffer would be wrong three ways at once:
| The pin | The consequence |
|---|---|
GCHandle.Alloc(…, Pinned) refuses storage whose element type carries a managed reference |
SliceData over any such slice threw ArgumentException: Object contains references. log/slog’s GroupValue is the corpus witness — groupptr(unsafe.SliceData(as)) over []Attr, rebuilt by unsafe.Slice in Value.group() — and it infrastructure-errored every grouping path in the package (5 testing/slogtest rows). |
| The pin covered the whole backing array from index 0, ignoring the slice’s LOW bound |
SliceData(s[2:]) addressed element 0 rather than element 2, and did not compare equal to &s[2] as Go’s pointer identity requires. |
PinnedBuffer implements IArray<byte> alone |
ж<T>.Value’s array is IArray<T> test failed for every element type but byte, so the derived pointer was undereferenceable — InvalidOperationException instead of the element. |
Pinning was never what SliceData means. An address is needed only when the pointer is converted
to uintptr/void*, and ж already pins on demand at exactly those conversions
(EnsureStableAddress), declining gracefully for storage that cannot be held still. The element
reference additionally makes the round trip alias rather than snapshot — unsafe.Slice’s
element-window arm (ElementWindow.TryGet) rebuilds a window over the original backing, so a write through the
rebuilt slice reaches the source, which is Go’s semantics. StringData keeps its pin: a @string
is a byte[], always pinnable, and its emptiness identity is the property TestClone asserts.
(Guarded by the UnsafeSliceDataAliasing behavioral output test — a []struct{string;int}
round-tripped through SliceData/Slice and written through, SliceData(s) == &s[0] identity, a
re-sliced source’s low bound, an []int64 and a []*T deref, and the plain []byte control —
plus UnsafeStringEmpty, which pins the zero-length behavior above unchanged.)
Reinterpreting a pointer to a defined type with identical underlying — (*Base)(p)
A Go conversion (*Base)(p) where p is a *Def and Base/Def share an identical underlying type (one is a defined type over the other, e.g. type pinnerBits gcBits, or both over the same type) reinterprets the pointer. C# has no conversion between the two distinct generic instantiations ж<Def> and ж<Base>; only the generated wrapper’s value conversion Def ↔ Base exists. The arm’s first form performed the reinterpret on the value and re-boxed it (these arms ALIAS — see These three arms ALIAS instead of boxing a copy below):
func (s *mspan) newPinnerBits() *pinnerBits { return (*pinnerBits)(newMarkBits(s.nelems * 2)) } // newMarkBits returns *gcBits
internal static ж<pinnerBits> newPinnerBits(this ref mspan s) {
return Ꮡ((pinnerBits)(~newMarkBits(((uintptr)s.nelems) * 2))); // deref the ж<gcBits> box, value-convert, re-box
}
The argument is dereferenced first (~box) when it renders as a genuine pointer box — a call result, a local box, or a pointer field — because the value conversion operates on the underlying value, not on ж<Def> (a plain (pinnerBits)(ж<gcBits>) is CS0030). A deref-aliased pointer parameter/receiver already renders as the pointed-to value (Δp, not a box), so it value-converts directly with no ~ — the original (*atomic.Uint32)(p) receiver case (runtime/mprof goroutineProfileStateHolder). Both forms boxed a copy (Ꮡ): the shared underlying is the wrapped value, and a defined-over-struct wrapper holds it in a readonly field, so there is no write-through to lose; this matches the long-standing copy semantics of this branch (the runtime intrinsics behind these are assembly stubs). Both ships stay in managed ж<> land — no raw-address round-trip. (Guarded by NamedPointerReinterpret.)
The third direction — a pointer to a BASIC type reinterpreted to a defined type over that
basic — takes the same value-convert-and-re-box route: fmt’s (*stringReader)(&str) (type
stringReader string) would emit Ꮡ((stringReader)(str)) — the address-of collapsing with the value
deref — and instead aliases like the others: Ꮡstr.Reinterpret<@string, stringReader>(). (While it
copied, the copy was faithful for the pattern: the source string is never re-read.)
Guarded by NamedPointerReinterpret (tail/consume). The deferring-receiver rule is a
sibling of these box-form decisions: a method that defers or recovers at FUNCTION level and also
references its receiver takes the direct-ж receiver (this ж<T> Ꮡx) rather than this ref T,
whose deref alias then emits inside the frame’s try (bodyWrappedInDeferContext; fmt ss.Token,
guarded by DeferCallOrder acc.add). The direct-ж form is the alloc-free, race-free one, and it
is also what a deferred closure needs, since a lambda cannot capture a ref local.
The same block also covers a named-numeric pointer reinterpreted to its underlying basic type — (*uint64)(head) where head is a *lfstack (type lfstack uint64). This is the runtime’s atomic-on-a-named-integer pattern: atomic.Load64((*uint64)(head)) / atomic.Cas64((*uint64)(head), …) on the named atomic types lfstack (uint64, lfstack.go), sweepClass (uint32, mgcsweep.go), profAtomic (uint64, profbuf.go), and sysMemStat (uint64, mstats.go). ж<lfstack> and ж<uint64> are distinct generic instantiations with no conversion (CS0030); the reinterpret condition is generalized from Named↔Named to also fire when the result elem is a basic type whose underlying equals a named argument elem’s (namedToBasic), and again for the reverse (basicToNamed).
These three arms ALIAS instead of boxing a copy
A value-convert-and-re-box copy can look justified at particular call sites — “no write-through to
lose”, “the intrinsics are asm stubs”, “the source string is never re-read” — but that is a property of
the call sites, not of the conversion, and Go’s (*U)(p) says the opposite: the derived pointer names
p’s own storage, so a write through it is visible through p. encoding/gob is where the difference
is not theoretical. Gobber.GobDecode decodes straight back through a reinterpret used as a call argument —
type Gobber int
func (g *Gobber) GobDecode(data []byte) error {
_, err := fmt.Sscanf(string(data), "VALUE=%d", (*int)(g)) // writes THROUGH the reinterpret
return err
}
— which is neither a deref context nor a raw-address source, so it takes this arm. As a copy it would
emit fmt.Sscanf(…, Ꮡ((nint)(g))): Sscanf’s write would land in a throwaway box, g would never
change, and the decoder would return 0 for 23. So all three arms route through golib’s aliasing
reinterpret — reinterpretManagedEmission, the same emission this file produces for the identical
conversion in a deref or raw-address context:
public static error GobDecode(this ж<Gobber> Ꮡg, slice<byte> data) {
var (_, err) = fmt.Sscanf((@string)data, "VALUE=%d"u8, Ꮡg.Reinterpret<Gobber, nint>().OrTypedNil());
return err;
}
PointerExtensions.Reinterpret is where the “can the managed model express this alias?” decision already
lives (ReinterpretAliasesStorage), so the converter delegates rather than re-deciding; it reports false
for the two shapes that must keep the re-box — an IDENTITY conversion (intercepted upstream by
pointerReinterpretIdentitySource) and an array pointee, whose lazily-materialized backing store a
storage reinterpret bypasses — so the chain-defined type pallocBits pageBits pair the written-RHS gate
deliberately leaves on this arm falls through unchanged. A useful second-order effect: the entry deref
alias these functions would carry (ref var head = ref Ꮡhead.DerefOrNull();) is dead, because the body
names only the box, so the alias-liveness scan drops it.
The copy is never harmless. Beyond the runtime’s asm-stub atomics (lfstack, sweepClass,
profAtomic, sysMemStat, pinnerBits), a copy would break real write-through in flag (every
newBoolValue/newIntValue/… returns (*boolValue)(p), so a parsed flag would never reach the user’s
variable), crypto/tls (clientShares.ReadUint16((*uint16)(&ks.group)) would parse key-share groups
and signature schemes into a copy), crypto/cipher ((*cbcEncrypter)(newCBC(b, iv)), whose IV
mutates per block), image/png ((*encoder)(buffer) over a pooled EncoderBuffer), go/types
((*term)(t)), and crypto/internal/boring/bbig. (Guarded by the extended NamedNumericPointerReinterpret
behavioral output test: the
read
path it always covered, plus write-back through an argument-position reinterpret — the gob shape — a
named→named struct reinterpret held in a local and written through a field selector, and a basic→named
(*namedString)(&s); verified to FAIL on stdout with the fix neutered.)
Nine coupled rules from the flag, flate, binary and syntax packages
Nine coupled rules, each first needed by one of those packages:
-
Named func types implementing interfaces (flag’s
funcValue): a delegate cannot be a partial struct — the generator routes Delegate records to the VALUE adapter (new funcValueᴠValue(v)), whose Go methods are package extensions binding on the wrapped copy; non-struct record kinds SKIP rather than throw (a throw kills the package’s entire generator run). Guarded byFirstClassFunctions(handler.tag). -
Ref receivers never take box renders: a
refreceiver has NO box — the escape-heap and lambda-capture convIdent arms fall through to the value alias (flate init’sd.fill = (*compressor).fillStoreemitted a nonexistentᏑd, CS0103). Guarded byFirstClassFunctions(worker). -
Func-field callees drive argument treatment:
getFunctionSignatureresolves a FUNC-typed field’s signature (d.fill(d, b)— the receiver arg renders as the box for aж<T>slot, CS1503). -
Integer wrappers carry the UntypedInt bridge (
(token)(endBlockMarker)— C# never chains two user conversions, CS0030). Guarded bySortArrayType(levelToken). -
Package aliases shadowed by method names qualify through the
_packageclass (sort_package.Sort(…)— flate’sbyLiteral.sortbound the method group, CS0119). Guarded bySortArrayType(PeopleByAge.sort). Same-package tests also participate in that shadow set even during an ordinary production conversion. Under recompile fallback an in-package_test.godeclaration lands in the SAME C# package class and can shadow a production alias; under white-box reference it lives in a bridge, but external-test references still need one stable production spelling — yet go/packages’ production package omits test files. hash/maphash’ssmhasher_test.godeclaresfunc (k *bytesKey) bits() intwhilemaphash_purego.goimportsmath/bits, sobits.Mul64(a, b)binds the method group in the test assembly (CS0119, plus CS8130 on both deconstructed results). Every production package conversion therefore performs a cheap, build-constraint-aware directory scan of its in-package_test.gofunction/method names and folds them intopackageFuncMethodNames; it loads and type-checks no test dependency graph. External-package tests are excluded because they emit into another C# package class. This makes production output mode-stable: ordinary and-testsconversion both emitgo.math.bits_package.Mul64(a, b). When a test-only declaration is specifically responsible, the statement also explains the otherwise surprising spelling:// Fully qualified to avoid alias shadowing by the same-package test declaration "bits". var (hi, lo) = go.math.bits_package.Mul64(a, b);This remains a REFERENCE-spelling change only — never a symbol rename, so production names stay pinned (see
testMethodRenames). On entry toprocessTestConversionthe sibling list is cleared: the in-package variant sees the declaration in its own typed universe, while the external variant’s declarations live in a different C# class. Guarded byTestSiblingTestDeclaratorsContributeAliasShadow, including build-excluded and external-test negatives plus the exact explanatory comment; hash/maphash’s normal-vs--testsproduction file is also byte-identical. A Δ-renamed foreign CONST reached through that fallback must still substitute the renamed member: the composed lookup key (time_package.Second) misses the alias map (keyed on the plain package name,time.Second), sogetAliasedTypeNameretries with thePackageSuffixstripped and, on a CONST hit, keeps the_packagequalifier while substituting the alias —time_package.ΔSecond(crypto/tls’sConfig.timemethod × time’sSecondconst-vs-Time.Second()collision; the raw name bound theSecond(this Time)extension method group, CS0019 ×2). Gated to consts: const entries exist only for collision-renamed members, while type entries cover every exported type, whose raw_package-qualified renders already bind. Guarded byShadowedImportConstLib/ShadowedImportConstUser(the lib Δ-renamesPeakfor its ownMeter.Peakcollision; the user’sgauge.ShadowedImportConstLibmethod shadows the import andSpan(2) * ShadowedImportConstLib.Peakreaches the renamed const through the fallback, output-compared vs Go). -
Blank params synthesize names when the body discards (
_ = b[7]bound the blanklittleEndianreceiver, CS0029) — encoding/binary’s bounds-check hints. Guarded byTypeSwitch(marker.tag). -
Defined-over-named-struct composites wrap the underlying (
decoder{order: o}→new decoder(new coder(order: o)), CS1739). Guarded byNamedPointerReinterpret(view{}). -
Labeled switches declare their break target (
break_BigSwitch:;after the switch — both switch visitors mirror visitForStmt, CS0159). Guarded bySwitchBreakInCase(pick). -
Short declarations keep the named-numeric cast (
p := printFlags(0)re-imposes((printFlags)0), CS1503) and empty-interface switch tags compare via AreEqual (switch err := recover(); err { case ErrLarge: }, CS0019).
Slice-to-array: the VALUE form copies, the POINTER form ALIASES
Go has two slice-to-array conversions and they are different conversions, so each gets its own golib entry. Both panic Go-style on a short slice.
- The Go 1.20 value form
[4]byte(slice)emitsnew array<byte>(s, 4)— the COPY constructorarray<T>(slice<T>, nint). Go’s conversion copies, so this is exactly faithful (netipAddrFromSlice, CS1955). - The Go 1.17 pointer form
(*[4]byte)(slice)emitsᏑ(array<byte>.Alias(s, 4))— an ALIASING window over the slice’s own backing store. The Go spec is explicit that here “the slice and array share their underlying array”, so the array is a view, andarray<T>carries a(low, length)window for exactly this one construction; every other construction spans its whole backing, where the window is inert.
A copy here would be a silent wrong answer, not a performance detail — every write through the
pointer would be discarded. image/png’s encoder is the corpus
witness: its cbTCA8 row loop converts each four-byte destination window and writes the
un-premultiplied pixel through it,
d := (*[4]byte)(dst)
s := (*[4]byte)(src)
…
d[0] = uint8((uint32(s[0]) * m / a) >> 8)
so against a copy every pixel write goes nowhere and any RGBA image that is not fully opaque
encodes as an all-zero image. TestWriteRGBA’s “50/50 Transparent/Opaque RGBA” and “RGBA with
variable alpha” subtests catch it, and the two other subtests would pass against a copy by luck: the
opaque one takes the cbTC8 path entirely, and the fully-transparent one wants all-zero output,
which is what a lost write also produces.
The window is where the aliasing is, not merely the sharing: the png loop converts cr[0][1:]
and re-slices it forward, so the array’s element 0 is not element 0 of the backing store. Three
consequences follow and all three are Go’s:
-
array<T>.Clone()— Go’s by-value array copy — materializes the window, so a copy is a full, offset-free array again; -
p[:]bounds its capacity at the array’s end (cap4, not the rest of the slice); -
&(*[4]byte)(s)[0] == &s[0]: the element box’s canonicalization (ElemRefBox<T>) resolves an element pointer taken through a window to the same absolute element as one taken through the slice.
Guarded by SliceToArrayPointerAlias (offset write-through, read-back through the pointer,
value-conversion copy, deref copy, p[:] aliasing and len/cap, element identity, and the
png-shaped windowed loop) and by NamedPointerReinterpret (sliceToArray). The POINTER-sourced
sibling (*[N]T)(unsafe.Pointer(p)) is
below.
An element pointer reinterpreted as an array pointer ALIASES the element’s storage
(*[N]T)(unsafe.Pointer(p)) with p a *T is the same conversion reached from a POINTER instead
of a slice, and it emits the same kind of window — array<T>.AliasPointer(p, N), which yields the
ж<array<T>> directly. It is what Go’s stdlib reaches for whenever a *T really names a run of
elements: os’s TestReadStdin fake fills internal/poll’s read buffer with
copy((*[10000]uint16)(unsafe.Pointer(buf))[:n:n], s16), and syscall.Readlink decodes a reparse
record through (*[0xffff]uint16)(unsafe.Pointer(&data.PathBuffer[0]))[:n:n].
The raw-address route ((ж<array<T>>)(uintptr)(…)) cannot express the shape in either of its two
forms. Dereferenced, that box reads an array<T> STRUCT — a
backing-store reference plus bounds — out of the pointed-at data, i.e. a fabricated managed
reference. Sliced, the isPointerCast fusion in convSliceExpr catches it first and produces a
slice<T> COPY of the memory, so every write through it is discarded: TestReadStdin’s subtests
would all read back zeros (have [0 0 0…] want [abc…]).
Three rules bound the arm, and each is a real limit of the managed model rather than a convenience:
-
Same element type only.
array<T>.AliasPointerwindows the pointer’s ownT[]backing; aT[]view over differently-typed storage does not exist in C#.(*[2]byte)(unsafe.Pointer(&x.Port))over auint16keeps the address route — that is the raw-metal fork, unchanged. - The window is decided at RUNTIME. Only a pointer that actually addresses managed array/slice storage has a backing to window; a heap box, a struct field or a native address falls back to the identical address route inside golib, so nothing that worked before changes shape.
-
Nis CLAMPED to the storage that exists. Go’sNin this idiom is a promise, not a length — it is spelled10000/1<<16/0xffffover buffers a few elements long and the result is always immediately re-sliced to the real count. Honoring it literally would put the window’s own bounds past its backing store, where a full-slice expression is an exception rather than a window. Solen(*p)reports the addressable extent, not Go’sN, in the huge-Ncase; an overrun is then a Go-style index panic instead of the silent corruption the same overrun is in Go. (WhereNfits — the ordinary(*[4]byte)(unsafe.Pointer(&s[i]))—lenis exactly Go’s.)
Explicit bounds over ANY window — Alias’s as well as AliasPointer’s — resolve through the window,
in SliceExtensions.slice(this array<T>, …) as in the Range indexer: slicing the RAW backing store,
ignoring m_low/m_length, would address the source’s elements rather than the array’s (p[1:3] of a
window over buf[1:] would yield buf[1:3], not buf[2:4]). The explicit-bounds path is the one the
[:n:n] idiom takes.
Guarded by ArrayPointerElementAlias (write-through at element 0 and at an offset, the huge-N
[:n:n] copy shape, indexed write-through, read-back through the pointer, deref copy, p[:] and
p[1:3] aliasing with len/cap, element identity, a Go fixed-array source, and a re-sliced
view).
A direct-ж method on a value field-chain boxes through the &-machinery
A direct-ж (box-receiver) method called on a field of a plain VALUE param — netip’s
ip.addr.halves(), where Go auto-addresses &ip.addr — routes the receiver through the
&-machinery: Ꮡ(ip).of(netip_package.ΔAddr.Ꮡaddr).halves(). This boxes a COPY, which is faithful because
the enclosing Go value param is itself a copy: writes through the method could only ever reach
the local copy in Go too. (Pointer-rooted chains and indexed elements take their own
long-standing arms; this is the remaining value-rooted case.) Guarded by
StructPointerPromotionWithInterface (rig/probeRig).
Field address of a collision-renamed heap-boxed local uses the raw box name
A heap box always keeps the RAW Go identifier (ref var Δslice = ref heap<T>(out var <box>slice)), so taking the address of a FIELD of a collision-renamed boxed local routes through boxBaseName – the raw-name box, never the Δ-renamed alias (CS0103; reflect SliceOf’s &slice.Type). This matches the whole-value &p form and the renamed receiver/parameter boxes:
ref var Δp = ref heap<box>(out var Ꮡp);
…
bump(Ꮡp.of(box.Ꮡn));
The local p collides with the type p, so its value alias is Δp, and &p.n goes through the RAW-name
box Ꮡp, never ᏑΔp. Guarded by CollisionRenamedLocalBox (bump(&p.n) on the renamed local p).
Its callees keep the pointer they are given, so neither is ref-lowered and the box stays; a callee that
only writes through its pointer is ref-lowered, and its caller passes ref Δp.n with no box at all.
A capture-mode method on a shadow-renamed heap-boxed local uses the rendered box name
A capture-mode method — one that escapes its receiver’s address, e.g. cryptobyte.Builder.AddASN1, which hands &b to a callback — called on a heap-boxed VALUE local routes through the receiver box: var b Builder; b.AddASN1(…) → Ꮡb.AddASN1(…). Unlike a deref-aliased pointer parameter (whose box keeps the RAW name, Ꮡp), a heap-boxed value LOCAL keeps its box under the RENDERED name — an escaping local is ref var b = ref heap(new T(), out var Ꮡb), so when the local is SHADOW-renamed its box takes the renamed name. crypto/x509 marshalCertificate’s inner serialiseConstraints closure declares var b cryptobyte.Builder, renamed bΔ1 to dodge the enclosing method’s own var b declared LATER (a C# lambda cannot re-declare an enclosing-scope local, CS0136); its box is ᏑbΔ1. Emitting the raw-name box Ꮡb there both mis-references the outer b’s box (declared later in the method → CS0841/CS0103) and, where a same-named outer box does resolve, calls the method on the wrong operand — go/types conversions.go called x.convertibleTo on the receiver box Ꮡx instead of the inner operand box ᏑxΔ2:
ref var bΔ1 = ref heap(new cryptobyte.Builder(), out var ᏑbΔ1);
…
ᏑbΔ1.AddASN1(cryptobyte_asn1.SEQUENCE, (ж<cryptobyte.Builder> bΔ2) => { … }); // was Ꮡb (CS0841)
The receiver-box render resolves the box base through boxBaseName with the lambda capture-remap DISABLED, so it yields: the shadow-rendered declaring name (bΔ1) for an escaping local; the raw name (Ꮡp) for a pointer parameter; and — critically — the declaring name for a variable CAPTURED by the closure, not its value-snapshot capture name. A heap-boxed local captured by a closure has its box captured directly (Ꮡonce in sync OnceFunc’s returned closure), so the capture-remapped Ꮡonceʗ1 (a non-existent box) must not appear. Guarded by ShadowedHeapBoxReceiver (an inner closure’s var b capture-mode method, shadow-renamed against an outer same-named var b declared later).
Nested dereferences parenthesize before the outer .Value
A deref whose operand is ITSELF a deref renders with the prefix ~ form, on which a naked postfix .Value mis-binds (postfix beats unary: ~X.Value is ~(X.Value)). The outer deref wraps the inner one – reflect MapOf’s **(**mapType)(unsafe.Pointer(&imap)):
var back = (~Ꮡip.Reinterpret<ж<array<nint>>, ж<array<int64>>>()).Value.Clone();
Guarded by PointerCastSliceRange (compile-shape).
Function literals returning unsafe.Pointer state their return type
A literal with a single unsafe.Pointer result can mix return arms of DIFFERENT C# types (reflect deepEqual’s ptrval: (uintptr)v.pointer() on one arm, the raw v.ptr on the other), which defeats C# lambda return-type inference (CS8917). The emitted lambda states its return type explicitly; each arm then converts implicitly through the golib operators:
@unsafe.Pointer pick(bool u) {
(pick is only ever called, so it emits as a C# local function, which states its return type by
construction; a literal in value position takes the lambda form @unsafe.Pointer (bool u) => { … }.)
Guarded by PointerCastSliceRange (compile-shape).
Interface-returning literals with distinct arm types state their return type too
The same inference gap hits an interface result whose arms return DIFFERENT concrete types — net ipsock.go’s inetaddr := func(ip IPAddr) Addr returns three pointer-adapter classes (TCPAddrжΔAddr / UDPAddrжΔAddr / IPAddrжΔAddr), which share only the interface (CS8917). When a single non-empty-interface result’s return arms carry two or more distinct types, the lambda states the return type explicitly (Addr (IPAddr ip) => …); each arm then converts implicitly. Single-typed literals keep the inferred form (zero churn). (Guarded by the InterfaceCasting extension makeAnimal — an adapter arm plus a value arm, runtime-verified.)
And the opposite end of the same gap: arms that are ALL untyped nil. client := func(*TCPConn)
error { <-serverDone; return nil } (net net_test) renders its only arm as default!, which carries
no natural type at all — so where the rule above has too many candidate types, this has none, and it
is the same CS8917. The interface arm also states the return type when the literal is in
assignment position, has a return, and every single-result arm is untyped nil. Drift-free by
construction: an all-default! arm set never had an inferable natural type, so every site the rule
touches is a site that did not compile. It is the single-result twin of the multi-result
!hasFullyTypedArm rule immediately below, and it stays out of argument/return position for the same
reason that one does — those literals are target-typed by their delegate, so nothing is inferred.
(Guarded by the FuncLitStringConcatReturn extension: an all-nil literal in both := and var
form, plus a mixed-arm control that must KEEP inferring.)
Multi-value literals with no fully-typed arm state their return type — named results included
The single-result inference gaps above generalize to any MULTI-result literal where EVERY return arm carries a typeless element — return nil, nil, nil, nil, err on the error arms and return dnsNames, ips, emails, uriDomains, nil on the success arm (crypto/x509 parseNameConstraintsExtension’s getValues := func(subtrees) (dnsNames []string, ips []*net.IPNet, emails, uriDomains []string, err error)). A C# tuple literal with any untyped element has no natural type, so no arm fixes the lambda’s return type and delegate-type inference fails (CS8917). The lambda states its tuple return type explicitly, and each nil then takes its target element type:
(slice<@string> dnsNames, slice<ж<net.IPNet>> ips, slice<@string> emails, slice<@string> uriDomains, error err) getValues(cryptobyte.String subtrees) { … }
(getValues is only ever called, so the corpus site emits as a local function with the tuple as its
declared result; a literal in value position takes the lambda form (…) (cryptobyte.String subtrees) => { … }.)
NAMED results are included: the trigger — a multi-result literal with a return arm but NO fully-typed arm — is identical whether the results are named or not. A bare return (which returns the named results) never matches the result arity, so it neither marks has-return nor a false fully-typed arm; a named literal that DOES have a fully-typed explicit arm keeps inferred typing (no return-type prefix, no churn). Guarded by NamedResultLambdaInfer (a five-result named-result closure whose error arms return nil,nil,err and success arm e,o,nil).
String-returning literals in assignment position state their return type
A literal with a single Go string result can mix return arms of DIFFERENT C# types even though every arm is a Go string: a bare string literal is a "…"u8 ReadOnlySpan<byte>, a literal+variable concat binds golib’s operator +(@string, @string) (so it is @string regardless of u8 suppression), and a call into a hand-written stub can return C# string (the baseline fmt.Sprintf does). @string and string convert implicitly in BOTH directions, so a lambda mixing those arms has no unique best common type and its delegate type is not inferable — CS8917 on pick := func(v any) string {…} whose case string: arm returns "string:" + t alongside fmt.Sprintf arms. In assignment position (var pick = …, where C# must infer the delegate type), the lambda states its return type explicitly and each arm then converts to @string in place:
@string pick(any v) {
(The guard’s pick is only ever called, so it emits as a local function with the declared result; a
value-used literal takes var pick = @string (any v) => { … };.)
Argument/return/composite-element literals are target-typed by their receiving delegate type (no inference to fail — and an explicit return type could only add an identity-match constraint against stub delegate types), so they keep the plain form; the Go var declaration form emits an explicit delegate type (Func<@string, bool, @string> pad = …) and is likewise immune. Gated to the basic string kind — a named string type would need its own conversions. Guarded by FuncLitStringConcatReturn (:= literals mixing concat, u8-literal, and stub-Sprintf arms — including a type-switch body and a right-side literal concat — plus the var form; runtime-verified).
The TUPLE-ELEMENT sibling: in a MULTI-result literal, a bare string literal element is worse than typeless — it is wrongly typed. Inside a tuple the literal emits as a bare C# string (u8 spans cannot be tuple elements), so an arm with no nil and no string variable — internal/fuzz fuzzOnce’s return dur, coverageSnapshot, "" (func(entry CorpusEntry) (dur time.Duration, cov []byte, errMsg string)) — counted as “fully typed” in the multi-result scan above and suppressed the explicit tuple return type, letting inference succeed with the wrong element type: the destructured errMsg was C# string, which has no != against a "…"u8 span (CS0019 rather than CS8917). A basic-string constant literal element whose declared result element is Go string also marks its arm not-fully-typed, so the same explicit-tuple emission fires:
(time.Duration dur, slice<byte> cov, @string errMsg) fuzzOnce(CorpusEntry entry) { … }
(fuzzOnce is only ever called, so it emits as a local function with that declared result;
the lambda form is var fuzzOnce = (time.Duration dur, …) (CorpusEntry entry) => { … };.)
and each "" converts to @string in place via target typing. Same assignment-position gate; a literal whose string elements are all variables keeps inferred typing. Guarded by the FuncLitStringConcatReturn extensions fuzzish (named results, != "" on every destructured element) and sget (unnamed (string, error)); the pre-fix converter fails them with exactly CS0019 ×4.
The NUMERIC sibling: an untyped numeric constant literal element against a differently-SIZED declared result element is wrongly typed the same way. The literal emits bare, so the arm infers the literal’s natural C# type — an INT literal is C# int, a FLOAT literal C# double — where the Go result is e.g. int64: net/http ServeContent’s sizeFunc := func() (int64, error) { …; return 0, errSeeker } had no nil/string-literal element on its error arms, counted “fully typed”, and inferred Func<(int, error errSeeker)> — rejected at the serveContent(…, sizeFunc, …) call because delegate types are invariant (CS1662/CS0029/CS1503, and the leaked errSeeker element name rides the inferred tuple). Such an element also marks its arm not-fully-typed, so the same explicit-tuple emission fires:
var sizeFunc = (int64, error) () => { …; return (0, errSeeker); };
A declared element the literal’s natural type already matches (int32 for an INT literal, float64 for a FLOAT literal) infers correctly and stays inferred, and Go int (C# nint) is deliberately exempt — return 0, err against (int, error) results is pervasive and correct (the element converts implicitly at every use site), so marking it would churn stdlib-wide for no observed defect, the same reasoning that keeps lambdaConstReturnCastType away from signed single results. A SUB-negated literal (return -1, …) is unwrapped and marked the same way. (Guarded by the FuncLitNumericTupleReturn behavioral test — the sizeFunc shape and a float64 shape both PASSED to typed function parameters, the -1 arm, and the int/float64-identical controls that must keep inferred typing; the pre-fix converter fails it with exactly the fs.cs trio CS1662/CS0029/CS1503 ×2.)
That Go-int exemption is narrowed, not absolute: it holds only while every numeric arm at the declared-int position is a literal. When such a position carries BOTH a bare-0 arm (naturally C# int) AND a non-literal Go-int arm (i + 1 → C# nint) — and the other tuple slots on the non-literal arms are typeless (default!), leaving a single naturally-typed literal arm to drive inference — C# infers the delegate’s first element as int, so the nint arm then fails to convert (CS0029/CS1662) and the assignment-inferred delegate is rejected at the invariant use site (CS0407). This is bufio ExampleScanner_*’s onComma := func(…) (advance int, token []byte, err error) { …; return i+1, data[:i], nil; …; return 0, data, ErrFinalToken; }. The multi-result scan therefore additionally records, per declared-int position, whether an int-LITERAL arm and a non-literal nint-expression arm both occur; when they do, the explicit (nint, …) return type is forced ((nint advance, slice<byte> token, error err) (…) => …) and each arm converts in place. The all-literal return 0, err shape has no non-literal arm, so it keeps its inferred emission untouched — the corpus is undisturbed (the behavioral corpus is byte-identical ). (Guarded by the FuncLitNumericTupleReturn extension — the mixedIntArms/onComma shape assigned and passed to a typed parameter, alongside the unchanged all-literal intControl; the pre-fix converter fails it with CS0029/CS1662/CS1503.)
Numeric-returning literals with untyped-constant arms state their return type
The SINGLE-result numeric sibling of the string arm above : a literal with a declared numeric result whose return arm references a named untyped constant — strings/bytes TestMap’s maxRune := func(rune) rune { return unicode.MaxRune }. The const reference emits as a golib Untyped* wrapper reference (Δunicode.MaxRune, an UntypedInt static), and the wrapper’s implicit conversions run in both directions with every numeric type. So in natural-inference position an all-const arm set infers the wrapper delegate — var maxRune = (rune r) => Δunicode.MaxRune; is Func<rune, UntypedInt>, rejected at the invariant-delegate Map(maxRune, …) call (CS1503) — and a mixed const/typed arm set (TestMap’s encode, mixing unicode.MaxRune/utf8.RuneSelf with the rune parameter) has no unique best common type at all (CS8917). When any top-level return arm is a bare named untyped-const reference, the lambda states the declared return type explicitly and each arm converts in place:
var maxFn = rune (rune _) => maxRune;
Same gates as the string arm: assignment position only (argument/return/composite-element literals are target-typed — no inference to fail), and a BASIC numeric result (a named numeric type would need a second user conversion the wrapper cannot chain — the lambdaConstReturnCastType named-type rationale).
Literal-only arm sets are NOT automatically safe. They are concretely typed — but a BARE int literal is C# int, which is the declared type only when the Go result is int32. crypto/tls TestCipherSuites’ comparator is the counterexample:
isBetter := func(a, b uint16) int { …; return -1; …; return +1; …; return 0 }
…
if !slices.IsSortedFunc(prefOrder, isBetter) { … }
Every arm rendered as C# int against a Go int (C# nint) result, so the inferred delegate was Func<ushort, ushort, int>. Every call of the variable accepted it (int converts to nint); the delegate-VALUED use did not, delegate types being invariant — CS1503: cannot convert from 'System.Func<ushort, ushort, int>' to 'System.Func<ushort, ushort, nint>', one of the package’s four build errors. So a third arm joins: when every top-level single-result return arm is an INT literal and the declared basic result is an integer type other than int32, the declared type is stated:
var isBetter = nint (uint16 a, uint16 b) => { …; return -1; …; return +1; …; return 0; };
The gate is keyed to what the converter EMITS, not to the literal’s Go-side natural type. Two wider cuts were written and each measured to over-apply before this one:
| shape | emitted arm | C# infers | prefix? |
|---|---|---|---|
func(…) int { return -1 } |
-1 (bare) |
int — wrong |
yes |
func(bool) int64 { return 9 } |
9 (bare) |
int — wrong |
yes |
func(bool) int32 { return 100 } |
100 (bare) |
int32 — right |
no |
func() float32 { return 0.5 } |
0.5F |
float — right |
no |
func() float64 { return 3 } |
3D |
double — right |
no |
Only a declared integer width leaves the literal bare: a floating result carries its width into the literal, and int32/rune is the bare literal’s own C# type. Three further things bound the rule. Any arm the predicate cannot classify suppresses it — every non-INT-literal expression is assumed to carry the declared type — so mixed arm sets keep their present emission, as does a bare return against named results. A literal bound to a name that is only ever CALLED never reaches this code at all: localFunctionDefine has already emitted it as a C# local function carrying an explicit result type, so the arm can only fire where the delegate type is genuinely observable. And both literal SIGNS are stripped (numericBasicLit unwraps unary + as well as unary -): Go writes an explicitly-positive literal precisely where it pairs with a negative one, which is the comparator shape this arm exists for, and treating +1 as a non-literal would blind the predicate to half its own arm set. Not covered, and with no measured instance: a MIXED arm set whose declared type is NARROWER than the type C# picks (func(…) uint16 with one 0 arm and one ushort arm, where ushort widens to int); that needs the natural C# type of an arbitrary expression, which the predicate deliberately does not attempt. (All five table rows sit side by side in the FuncLitUntypedConstReturn behavioral test, so the split stays pinned to the emission rather than to this table.)
A constant operator expression arm containing a named untyped constant counts the same as the bare reference (bytes TestMap’s invalidRune := func(r rune) rune { return utf8.MaxRune + 1 }): the operator result keeps the wrapper type, so the inferred delegate would be Func<int, UntypedInt> against Map’s Func<int, int> parameter (CS1503). The arm test (returnArmKeepsUntypedWrapper) walks paren/unary/binary trees for an untyped-named-const leaf, except when a constant fold (overflowingConstLiteral / floatContextConstLiteral) rewrites the whole arm to a plain literal — that emission is concretely typed and needs no prefix. All other gates unchanged. (Guarded by the FuncLitUntypedConstReturn behavioral test — the single-arm CS1503 shape, the mixed-arm CS8917 shape, an int64 result with a beyond-int32 const arm, the const-expression arm (return maxRune + 1), plus literal-only and argument-position controls that must keep the plain form; output-compared vs Go.)
A literal in GENERIC-RESULT inference position states its return type
The arms above all describe natural-inference position — a literal assigned to a var, where no
delegate target exists. A literal passed as an ARGUMENT is normally target-typed by its parameter and
needs no prefix, and the earlier rules say exactly that. There is one argument shape where that is
false: the callee is generic and the parameter’s declared signature returns a type parameter
— sync.OnceValue[T any](f func() T), sync.OnceValues[T1, T2 any](f func() (T1, T2)). There the
parameter type is not yet a concrete delegate; C# must infer the type argument from the lambda’s
own return expressions, so the Go result type go/types already resolved is ignored. Two shapes then
break (both live in sync’s oncefunc_test.go):
-
No arm yields a C# type at all — a body terminated by
panic(func() int { calls++; panic("x") }emits a statement lambda whose only exit is athrow), or one whose sole arm is an untypednil(return default!). Inference has nothing to work from: CS0411 ×4. -
An arm’s NATURAL C# type differs from the declared Go result —
func() int { return 42 }is naturallyFunc<int>, where Go’sintisnint: the wrong delegate is inferred and the declaration it initializes rejects it (CS0029).
A func literal in that position states its declared result type, which fixes the type argument to exactly Go’s for every arm shape (so, unlike the natural-inference arms, no arm inspection is needed):
var onceValue = sync.OnceValue(func() int { return 42 })
f := sync.OnceValue(func() int { calls++; panic("x") })
g := sync.OnceValues(func() (any, any) { buf[0] = 1; return nil, nil })
internal static Func<nint> onceValue = Δsync.OnceValue(nint () => 42);
var f = Δsync.OnceValue(nint () => { Ꮡcalls.Value++; throw panic("x"); });
var g = Δsync.OnceValues((any, any) () => { bufʗ3[0] = 1; return (default!, default!); });
The gate is the result position specifically. A type parameter appearing only in the func-typed
parameter’s own PARAMETER list — the slices.SortFunc(x, func(a, b E) int) shape — is inferred from
the lambda’s already-typed parameters and stays unprefixed; marking those would churn every such call
site in the corpus for no defect. (Guarded by
GenericResultLambdaInfer — the nint/any/panic-terminated/two-result shapes, a concrete
multi-result instantiation, and the parameter-position negative control, output-compared vs Go.)
A returned FUNC LITERAL is typeless in C#
Every arm above asks the same question — does this return expression carry a natural C# type? — and
each was written against the Go-side shapes seen so far: an untyped nil, a bare constant, an untyped
const wrapper. A Go function literal is a fourth shape, and it is typeless for a reason none of
those tests notice: it is fully typed in Go, and it renders as a bare C# lambda, which has no natural
type at all.
A function’s own returns need no help — a declared C# result type target-types them, so a method whose
body sits in a frame simply returns the literal (context’s afterFuncContext.AfterFunc, which
returns a Func<bool> literal from a defer-holding body). The one site that does need help is a
literal returned from inside another literal. lambdaConstReturnCastType already casts a bare
integer literal returned inside a lambda for exactly this reason (CS8917). Its sibling
lambdaFuncLitReturnCastType names the declared result type of a returned func literal, under the same
gates — inside a lambda conversion only (a named function’s returned literal is target-typed by its
declared C# return type), and only when the declared result is a NAMED func type, whose emitted
delegate name is what a cast needs:
mergeCancel := func(ctx, cancelCtx Context) (Context, CancelFunc) {
…
return ctx, func() { stop(); cancel(Canceled) }
}
(context.Context, Action) mergeCancel(context.Context ctx, context.Context cancelCtx) {
…
return (ctx, (Action)(() => {
stopʗ1();
cancelʗ2(context.Canceled);
}));
}
(mergeCancel is itself only ever called, so it takes the local-function emission above — still a
lambda conversion, which is what the gate tests.) Without the cast the tuple has no natural type, so
neither does the enclosing conversion — CS8917 on the declaration and CS8130 at every deconstruction
of its result. Naming the type is also the more faithful rendering: Go’s declared result there is
CancelFunc, a methodless func type, which renders inline as its base delegate Action. An UNNAMED func() bool result would need the synthesized
Func<…>/Action spelling and has no corpus site today, so it is deliberately left.
Publicization decides WHAT a type’s modifier is; the test-bridge arm only decides WHERE
visitTypeSpec writes a converted type declaration’s access modifier from one of two sources, and they
answer different questions. packagePublicizedTypes answers what the modifier must be — an
unexported type reached by an exported field, var, or callable signature has to be public or C#
rejects the referrer (CS0050/CS0051/CS0052). testInlineTypeAccess answers where it is written: a
white-box bridge type carries its modifier inline rather than through package_info.cs’s
<TypeAccessibility> section, because its metadata anchor can be a different test class.
Asking the inline arm FIRST would make it answer both — from the name alone — so a publicized bridge type
would stay internal. context’s internal test file declares type testingT interface{…} and the
exported func XTestParentFinishesChild(t testingT) that x_test.go calls; the publicization pre-pass
records testingT (it runs over the test-augmented package, so the exported *types.Func arm fires),
but the emission ignored it: internal partial interface testingT under a public method, CS0051 ×4.
So publicization outranks, and the inline arm supplies the DEFAULT:
public partial interface testingT { // was: internal
This is why signatureReferencesUnexportedProductionType (which downgrades an exported test-file
function whose signature names an unexported PRODUCTION type) is correctly restricted to production
types: a test-declared type is handled by publicization.
A white-box PRODUCTION↔PRODUCTION pointer pair is already implemented — do not record it again
Under the whitebox-reference test model the internal bridge is the SAME Go package as production, so
a *prodT → prodIface cast inside an internal _test.go reads as local and records its own
[assembly: GoImplement<T, Iface>(Pointer = true)]. Production, though, is a referenced assembly
that already generated that adapter from its own record — and InternalsVisibleTo <assembly>.tests
makes even an unexported adapter class reachable. The duplicate record makes go2cs-gen emit a SECOND
adapter under a test anchor, and that copy resolves its forwarding members in the TEST class’s scope:
context’s contains(pc.children, cc) converts *cancelCtx/*timerCtx to canceler for a map key,
and the duplicate bound Done to the unrelated afterFuncContext.Done extension (CS1929) while
emitting cancel with an empty body — a silently degraded override, not merely a build error.
The converter suppresses only the RECORD, which is what makes the rule small: resolveAdapterNameMarkers resolves
a pair that reached no record to the unqualified name it would have had, and that name is production’s
own adapter, so the cast site repoints with no other change.
!contains((~pc).children, new global::go.context_package.cancelCtxжcanceler(cc)) // production's, not a copy
It is gated on production ACTUALLY carrying the pair — its package_info.cs is loaded by
convertTestVariant into importedPointerImplements — never assumed: a pair only the test converts
still needs its local record. Reaching that set relies on canonicalImplementRecordIfaceName, which keys an interface
name from its <pkg>_package segment onward, so a leading global::go. (which names no package and
never appears in a parsed record) and an intermediate namespace (net.http_package.ΔHandler against the
record’s http_package.ΔHandler) both reduce to the record’s spelling. This is the POINTER twin of
the value arm’s whiteboxProductionTarget carve-out — note the pointer target arrives as a
*types.Pointer, so the shared whiteboxProductionTarget flag (computed from the unwrapped VALUE
form) is structurally false there and the check must unwrap and ask directly.
A GoImplement record is gated on the method set actually satisfying the interface
Every [assembly: GoImplement<T, Iface>] record makes the ImplementGenerator emit implementation glue whose members forward to T’s like-named methods — so a record whose Go method set does NOT satisfy the interface generates a forwarder to a method that does not exist. The corpus case: net/http’s err = http2GoAwayError{LastStreamID: …, ErrCode: cc.goAway.ErrCode, …} — the keyed composite’s sparse-array ident context leaks the error-typed LHS onto each FIELD value, and the ErrCode field’s value recorded GoImplement<http2ErrCode, error> even though http2ErrCode has only String()/stringToken() (its generated Error() => this.Error() was CS1929). convertToInterfaceType folds a types.Implements check over the recorded form’s method set (T for a value record, *T for a ж<T> record) into recordableBase, which gates both the record and the matching adapter-wrapping emissions. A conversion the Go checker admitted always passes the check, so the gate can only drop pairs a caller composed from mismatched types; a type-param-carrying target skips the check (types.Implements is undefined for uninstantiated generics, and the open-generic conversion emission must stay). (Guarded by the NEGATIVE KeyedLiteralIfaceAssign behavioral test: a keyed literal assigned to an error variable whose field-value type has String() but no Error() — a reintroduced record fails the compile phase.)
Promoted methods through a FOREIGN pointer embed forward via the embedded box
A struct embedding a FOREIGN pointer (net/http’s http2timeTimer struct { *time.Timer }, net/http/internal’s FlushAfterChunkWriter struct { *bufio.Writer }, bufio.ReadWriter { *Reader; *Writer }) promotes the embed’s pointer-receiver methods, but those land as ж-extensions visible to the consuming assembly only through METADATA — direct-ж primaries (Reset(this ж<Timer> …)) and the public RecvGenerator twins of this ref methods (Write(this ж<Writer> …)). The ImplementGenerator’s syntax-tree hop scan sees none of them, so without more the value-form partial would deref to the value (this.Timer.Value.Reset(d) — CS1929, the extension receiver strands) and the FOREIGN-struct pointer adapter would fall back to the struct itself (m_box.Value.Write(p) — CS1061, ReadWriter declares nothing). So both arms probe the embed’s type SYMBOL: the single-hop paths union the foreign element’s metadata box methods into the hop’s box-method set (this.Timer.Reset(d); m_box.Value.Writer in the local pointer arm), and the foreign-struct pointer arm routes each member still on the plain fallback through the UNIQUE pointer embed whose metadata box methods declare it (m_box.Value.Writer.Write(p); Go’s depth-one promotion ambiguity rules make the unique-embed requirement faithful). An embed package class outside extension-lookup reach (not the emitting namespace, the shared root go, or an enclosing segment) forwards through its package-class static with the box as the receiver argument, mirroring the foreign-extension arm. (Guarded by the ForeignPtrEmbedIfaceLib/ForeignPtrEmbedIfaceUser pair — a local struct embedding a foreign pointer adapted by value AND by pointer, plus a foreign two-pointer-embed struct adapted by pointer, output-compared vs Go.)
Promoted methods through embedded INTERFACE fields route per-member to the declaring field
A struct whose embeds are INTERFACE fields (httputil’s dumpConn struct { io.Writer; io.Reader } adapted to net.Conn) satisfies interface members through the fields’ method sets. Gated to a SINGLE field, the pointer adapter’s embedded-interface-field arm would give a multi-field struct no forwarding at all (m_box.Read(…) — CS1061), so the arm resolves per member: each still-unbound interface member forwards through the UNIQUE embedded field whose interface declares it (m_box.Value.Reader.Read(p) / m_box.Value.Writer.Write(p)); a member declared by several fields is left unbound (Go’s promotion ambiguity rules reject it unless the struct overrides, and a struct override is already resolved earlier). The single-field behavior is unchanged (zip’s nopCloser, slogtest’s Δ-renamed Handler field). (Guarded by the IfaceFieldEmbedAdapter behavioral test — a two-interface-field struct adapted by pointer to a third interface needing members from both fields plus one declared on the struct, output-compared vs Go.)
A pointer parameter used only through its box gets no deref VALUE alias
Every named pointer parameter is emitted as its box ж<T> Ꮡp with an entry-time value alias
ref var p = ref Ꮡp.DerefOrNull() (so a value use p.field reads through p). But a parameter that the body
touches only through its box — unsafe.Pointer(p) → @unsafe.Pointer.FromPinnedBox(Ꮡp), p == nil →
Ꮡp == nil, or passing p on as a *T argument → Ꮡp — never references that value alias, so the
alias is a dead local, and a dead local that dereferences the box at entry (the strict
ref var p = ref Ꮡp.Value this rule was written against) NREs on a nil argument even though Go never
touches the pointee: on Windows, syscall.writeFile(…, overlapped *Overlapped), called with a nil
overlapped and using it only as unsafe.Pointer(overlapped), would fault at entry — the failure of any
converted fmt.Println whose stdout is a pipe (syscall.Write → writeFile). So the converter skips the
alias for a named param whose value alias is unreferenced, as it does for an unnamed/blank pointer
param (never referenced). After the body is converted, bodyReferencesIdentAsValue scans the emitted body text for
the value-alias name as a standalone identifier — the address marker Ꮡ is a Unicode letter, so
the box form Ꮡp is excluded by the preceding-letter boundary, while a genuine value use always emits
the bare name and matches. The scan only ever ADDS spurious matches (a field selector x.p, a string, a
comment), which keep the alias, so a live alias is never dropped (no CS0103) — it removes strictly unused
locals. Because a param with no alias leaves implicitPointers empty (which otherwise triggers the
signature rebuild that renames pointer params to the box Ꮡ<name> the body references), a
skippedDeadPointerAlias flag forces that rebuild. Full-stdlib/behavioral blast radius is broad
(many functions forward a pointer param onward as a pointer), all pure alias removals.
This shares its root with the receiver case below (an eager deref alias NREs on a nil pointer); the
converse live-alias nil case — invoking a pointer method/function that does dereference a nil
receiver/param — is the nil-deferring DerefOrNull() entry alias’s (see
A pointer PARAMETER is nil-deferring for exactly the reason a receiver is below). (Guarded by the
DeadPointerParamAlias behavioral test — a pointer param used only in p == nil, one forwarded as a
pointer, and one dereferenced (alias kept), each called with nil and non-nil, output-compared vs Go; the
nil calls NRE’d before the fix.)
⚠ Keeping a dead alias is not free, so the scan excludes the one spurious match that is systematic: a
C# NAMED-ARGUMENT LABEL (isNamedArgumentLabel). The converter renders a Go composite literal’s field
keys as named arguments to the fieldwise constructor go2cs-gen generates, and Go’s own idiom is to name
the field after the value it is initialized from — so a pointer parameter whose name matches one of the
literal’s FIELDS matched the whole-word scan on the label alone. internal/concurrent’s
newIndirectNode(parent *indirect) { return &indirect{node: …, parent: parent} } never dereferences
parent (the C# passes the box, parent: Ꮡparent), yet parent: kept the alias alive;
ref var parent = ref Ꮡparent.Value then deref’d the box at ENTRY, and the ROOT node — created as
newIndirectNode(nil), a legitimately nil parent — threw inside NewHashTrieMap, taking down unique’s
package initializer, net/netip’s, and every dependent (found through encoding/gob’s TestNetIP). A
label is the identifier immediately followed by ONE : — never :: — whose preceding non-space
character opens or continues an argument list; every other colon form the converter emits is excluded by
that (a case X: arm and a goto label are preceded by a keyword or a statement boundary, an interpolated
format specifier {x:F2} by a brace, and a conditional’s cond ? a : b spaces its colon). A parameter
genuinely used elsewhere still matches there, so this discards the label occurrence and never the scan.
Whole-stdlib A/B (two seeded reconverts on the two binaries): every diff hunk the removal of one dead
ref var line and nothing else, and the reconverted corpus built with 0 errors — the removed
aliases are all genuinely unreferenced. (Guarded by the same
NilPointerParamUnsafePointer behavioral test, extended with the composite-literal shape and a
dereferencing positive control whose alias must survive; the exclusion’s own boundaries — ::, a
case X: arm, a spaced conditional colon, an interpolated format specifier, and a label that does not
mask a real use elsewhere — are pinned by the TestBodyReferencesIdentAsValueIgnoresNamedArgumentLabels
converter unit test, since a behavioral program cannot put most of them in that position.)
A pointer RECEIVER compared to nil compares its box, not its deref’d value
A method with a pointer receiver — func (f *File) checkValid() error { if f == nil { … } } — is emitted
as checkValid(this ж<File> Ꮡf, …) with the body alias ref var f = ref Ꮡf.DerefOrNull(). Go’s f == nil is a
pointer comparison (nil-pointer check — and Go legitimately calls methods on nil-pointer receivers), so
it must compare the box Ꮡf == nil, not the deref’d struct value f. Emitted in value form, f == nil
binds the generated File.operator==(File, NilType) — which compares against default(File) and, for a
promoted-embed struct (File embeds *file), dereferences that zero value’s null embed box → a
NullReferenceException (the first crash of a converted fmt.Println, via os.Stdout.Write →
checkValid); even for a plain struct it is the wrong answer (&box{}, a non-nil pointer to a zero struct,
compares equal to default(box) → true where Go gives false). The converter forces the box
form for a deref’d pointer parameter (isDerefdPointerParamIdent, driving the ==/!= operand
context in convBinaryExpr); the receiver is deliberately not a “parameter” in that model
(paramNames excludes the receiver), so it needs its own recognizer, isDerefdPointerReceiverIdent
(object-identity match via isPointerReceiver/identResolvesToReceiver, so a local shadowing the receiver
name keeps its own render). It is scoped to the comparison operands only — unlike a pointer parameter the
receiver is not folded into nilSafePtrParamNames, so its deref-alias form is unchanged (only the
receiver’s ==/!= operand switches to the box); convIdent renders Ꮡf for the receiver through its
existing direct-ж-receiver arm. Like a deref’d pointer parameter, the box form applies to every
==/!= comparison, not just against nil: a pointer receiver can only be ==-compared to another
pointer or to nil, so the box is always the correct (pointer-identity) operand — func (b *Reader)
Reset(r io.Reader) { if b == r … } (bufio) becomes AreEqual(Ꮡb, r), comparing the pointer to the
interface’s held pointer, where AreEqual(b, r) would compare a struct value to the interface
(never equal — a recursion-guard bug). The full-stdlib A/B is small and
mechanical — every changed line adds only the box to a receiver operand (mostly nil-checks, a few
pointer-identity).
(Guarded by the PointerReceiverNilCompare
behavioral test — a plain-struct *box where &box{} must compare != nil, == nil/!= nil receiver
methods, and a promoted-embed *embedder whose pre-fix value comparison NRE’d, output-compared vs Go; and
by the re-baselined RingPointerMethods whose r != nil receiver walk renders Ꮡr != nil.)
A pointer RECEIVER’s deref alias is nil-DEFERRING — the panic moves to the body, it does not vanish
Go legitimately calls methods through a nil pointer: the method RUNS, and the nil-pointer panic
happens only where the body actually dereferences the pointee. An entry alias that dereferences —
ref var b = ref Ꮡb.Value; — would panic every nil-tolerant method before it begins, whichever way
its guard is spelled:
| Guard shape | Go | eager entry alias |
|---|---|---|
INLINE — func (b *Buffer) String() string { if b == nil { return "<nil>" } … } (bytes TestNil) |
returns "<nil>"
|
entry panic |
DELEGATED — func (f *File) Chdir() error { if err := f.checkValid("chdir"); … }, where checkValid is what asks f == nil (os TestNilFileMethods, all fifteen *File methods) |
returns ErrInvalid
|
entry panic |
NONE, but a side effect first — fmt.Println(…) then f.name
|
prints, THEN panics | entry panic, nothing printed |
The nil-SAFE DerefOrNil() is the wrong instrument for the other two rows: it hands back a shared
throwaway default(T) slot, so an unguarded deref reads a silent zero where Go panics — acceptable
only where the converted guard provably excludes the read (the nil-terminated pointer walk it exists
for), never as a corpus-wide default.
The instrument that works is a nil-DEFERRING accessor, DerefOrNull(), which binds
Unsafe.NullRef<T>() for a nil box. A null ref is legal to HOLD and to pass on as ref T; it faults
on USE. So the panic is not discarded and not moved earlier — it lands exactly where Go’s does:
public static (nint n, error err) Read(this ж<File> Ꮡf, slice<byte> b) {
nint n = default!;
ref var f = ref Ꮡf.DerefOrNull(); // binds; never throws
{
var errΔ1 = Ꮡf.checkValid(readˢ); if (errΔ1 != default!) { // through the BOX — Go's guard, reached
return (0, errΔ1);
}
}
(n, var e) = Ꮡf.read(b);
return (n, f.wrapErr(readˢ, e)); // the first real deref — panics here, as Go does
}
The first field read, field write, or whole-struct copy through that ref raises
NullReferenceException, which RuntimeErrorPanic.TryAsPanic already maps to Go’s own
runtime error: invalid memory address or nil pointer dereference — recoverable and printed
verbatim. Measured, not assumed: a synthetic struct whose field sits 200 KB past the null page still
faults as a clean NullReferenceException (the JIT emits an explicit check rather than relying on the
64 KB guard page), and a converted Go struct cannot reach even that offset — Go’s inline [N]T becomes
a golib array<T>, an 8-byte managed reference — so there is no null-page cliff to fall off.
Where it is emitted. The entry alias exists in two places and both take the accessor, or the fix is half-done:
- the converter’s own preamble for a direct-ж receiver (
visitFuncDecl, unconditionally fori == 0 && Recv != nil && directBoxReceiver; plus a repointed receiver’s re-alias invisitAssignStmt, so the two halves of one alias cannot disagree); -
go2cs-gen’s
ReceiverMethodTemplate, the bridge that reaches aref T-receiver method through a box. Left eager, that bridge panicked one call frame earlier than Go — theValAnnouncearm of the guard below caught exactly this and was the reason the generator half was found at all.
Because the accessor is unconditional there is no predicate to get wrong. A non-nil receiver is unaffected: DerefOrNull() routes it to ValueSlot, the identical real
slot — the same slot a reference-pointee receiver would otherwise take through .ValueSlot, with the
genuinely-nil case handled rather than silently read. Pointer PARAMETERS take the same accessor (the next
section).
(Guarded by the NilReceiverMethods behavioral test, output-compared vs go run: a delegated
checkValid-style guard that must return the error, an unconditional deref that must panic with Go’s
message, a side effect that must be observed BEFORE that panic, and a non-nil receiver read through
both emission shapes — each panic case run through both the direct-ж preamble and the generated
ref-receiver bridge. PointerReceiverNilCompare and bytes’ TestNil continue to cover the inline
guard.)
A pointer PARAMETER is nil-deferring for exactly the reason a receiver is
Go’s nil rule does not distinguish the two. Passing a nil *T to a function is as legal as calling a
method through one: the body RUNS, and the nil-pointer panic happens only where the body actually
dereferences the pointee. So the entry alias for a pointer PARAMETER takes the same
DerefOrNull() the receiver does, unconditionally, with no admitting analysis — because the accessor
is faithful whether or not the body guards.
Getting here took three arms of a body ANALYSIS, each one a real fix for a real crash and each one provably incomplete, which is the argument for retiring all of them at once:
| Arm | The shape it admitted | Why it could not be the answer |
|---|---|---|
nil-COMPARED param (collectNilSafePtrParams) |
for p != nil { …; p = p.next }; if p == nil { … }
|
a body that never spells the comparison is not a body that cannot take nil |
nil-ARGUMENT call site (collectNilArgPtrParams) |
digits(ch, 10, nil) — text/scanner’s optional out-param |
SAME-package call sites only; the converter sees one package at a time, so a cross-package nil argument stayed broken |
RE-POINTED before first read (reassignedBeforeDerefParamName) |
l = l.get() — Go’s nil-receiver NORMALIZATION idiom (time.Location.lookup, whose entry fault failed seven of time’s tests) |
narrow by construction: first top-level statement only, and every use inside it non-dereferencing |
The shape none of them can reach is internal/sync’s newIndirectNode(parent *indirect[K, V]), which
STORES its parameter and never dereferences it: every HashTrieMap — the map behind unique, which
net/netip and so net use — creates its root as newIndirectNode(nil), a perfectly ordinary nil
argument with no comparison anywhere in the callee. An eager ref var parent = ref Ꮡparent.Value; would
fault at entry, and every net test would fail before the first one ran.
Nothing analyzable distinguishes that callee from one that derefs immediately. What distinguishes them is what the BODY does, at the point it does it — which is precisely what the nil-deferring accessor defers to:
// internal/sync/hashtriemap.go: HashTrieMap's lazy init
ht.root.Store(newIndirectNode[K, V](nil))
...
func newIndirectNode[K comparable, V any](parent *indirect[K, V]) *indirect[K, V] {
return &indirect[K, V]{node: node[K, V]{isEntry: false}, parent: parent}
}
internal static ж<Δindirect<K, V>> newIndirectNode<K, V>(ж<Δindirect<K, V>> Ꮡparent) {
return Ꮡ(new Δindirect<K, V>(node: new node<K, V>(isEntry: false), parent: Ꮡparent));
}
This body never dereferences parent, so it gets no entry alias at all (A pointer parameter
used only through its box gets no deref VALUE alias, above); a body that does dereference binds
ref var parent = ref Ꮡparent.DerefOrNull();, never the eager ref Ꮡparent.Value.
What the accessor costs. For a NON-nil pointer, nothing observable: DerefOrNull() routes to
ValueSlot, the identical real slot, so every read, every write-through and every re-alias behaves
exactly as .Value did. For a nil pointer it binds Unsafe.NullRef<T>, which is legal to hold and
faults on USE — so an unguarded deref still raises Go’s
runtime error: invalid memory address or nil pointer dereference (via
RuntimeErrorPanic.TryAsPanic), recoverable, at the body’s own deref point, AFTER any side effect the
body performed first. The retired nil-SAFE accessor could not say that: its shared default(T) slot
read a silent zero where Go panics, which is why it was only ever admissible under a proof.
One shape, everywhere a pointer is aliased. The unification also takes the pointer-reassignment
re-alias in visitAssignStmt (single-assign, tuple-deconstruction, and the p = nil repoint alike):
a repoint is not a dereference in Go, so re-aliasing must not fault, and the two halves of one alias
must not disagree about whether nil is legal to hold. And it takes the isInherentlyHeapAllocatedType
→ .ValueSlot arm at THIS site, which is the one place the unification goes beyond swapping an
accessor. .ValueSlot remains type-selected everywhere it belongs — a box-of-pointer LOCAL, a
named-result box, heap(out …), the reflection bridge’s field paths — but at an entry alias it was
selected AFTER the nil-safe analysis, i.e. the old code already ranked nil-ability above pointee-kind.
Keeping it ranked first under an unconditional nil policy would have inverted that order and handed a
handful of corpus aliases (internal/weak’s ptr, runtime mbitmap’s header, dwarf’s
fixups, … ) a NEW entry-time fault on exactly the nil arguments the previous
converter tolerated —
measured, and the reason the arm is not selected here.
Measured footprint (seeded whole-stdlib A/B reconvert when the accessor was unified): a single shape,
the accessor token at a = ref <box>.<accessor>; position — .Value, .DerefOrNil() or .ValueSlot
becoming .DerefOrNull() — with zero unclassified lines and no file changing its line count. The
remaining entry-position .Value sites are genuine USE sites, where Go does panic, and no converted
file emits DerefOrNil().
(Guarded by the NilPointerParamMethods behavioral test, output-compared vs go run: a nil argument
through a DELEGATED guard that must return the error, a nil argument STORED and never dereferenced —
the newIndirectNode shape — an unconditional deref that must panic with Go’s message, a side effect
that must be observed BEFORE that panic, a nil-terminated walk, a *error parameter that legally
HOLDS nil, the normalization idiom, and every non-nil argument unchanged. Neuter-proven: restoring the
eager param arm diverges from Go on 42 output lines. GuardedNilPointerParamDeref,
PointerParamNilWalk, NilReceiverNormalization and PointerToInterfaceParamDeref continue to cover
the shapes the retired arms were built for.)
A receiver or parameter RE-POINTED before first use — the normalization idiom
Go’s other nil idiom does not test the pointer at all: it normalizes it, by re-pointing through a
helper that does the testing. time’s Location.lookup is the canonical case — Time{} carries a nil
*Location meaning UTC, and get maps nil to &utcLoc:
func (l *Location) lookup(sec int64) (name string, offset int, start, end int64, isDST bool) {
l = l.get()
if len(l.zone) == 0 { … }
No comparison predicate can see this — lookup never writes l == nil — so under an eager alias the
receiver would fault before its first statement, where Go returns UTC. That one site would fail
seven of time’s tests (TestDefaultLoc, TestSecondsToUTC, TestNanosecondsToUTC, TestParse,
TestTimeGob, TestTimeIsDST, TestZoneBounds), every one of them dying at entry.
It was closed first by a dedicated predicate (reassignedBeforeDerefParamName: the FIRST top-level
body statement that mentions the pointer must be an = that re-points it, with every use of it inside
that statement leaving the pointee untouched — a pointer-receiver method call like l.get(), a nil
comparison, or the bare pointer as an argument; a field selection, a value-receiver method, or a *p
disqualified it). That predicate is gone: the unconditional nil-deferring alias covers the idiom
without needing to recognize it, and covers the variants it could not admit (a normalization on the
SECOND statement, or one nested in an if). Both halves read the same way:
internal static (@string name, nint offset, int64 start, int64 end, bool isDST) lookup(this ж<ΔLocation> Ꮡl, int64 sec) {
…
ref var l = ref Ꮡl.DerefOrNull(); // binds for Time{}'s nil loc; no fault at entry
Ꮡl = Ꮡl.get(); l = ref Ꮡl.DerefOrNull(); // the repoint is not a deref either
One subtlety the predicate era got right and this must keep: a normalizer that CAN return nil
(GenericFuncCall’s renew — p = escape(p); *p += 10, where escape is identity) must still panic
at the *p, and it does — the re-alias binds a null ref and the dereference through it faults with
Go’s message, exactly where Go’s does. (Guarded by the NilReceiverNormalization behavioral test —
normalize-then-read, normalize-then-write through the re-aliased receiver with a read-back proving
the real slot was addressed, the same shape on a nil-legal pointer parameter, and a control that
reads BEFORE normalizing and still panics through a nil receiver exactly as Go does.)
A reinterpreted raw address ALIASES native memory instead of boxing a copy
(ж<T>)(uintptr) is the reinterpret seam: it turns a raw address back into a pointer. It must not
box a copy of the pointed-at value, as an operator of this shape would —
public static unsafe explicit operator ж<T>(uintptr value) => new ж<T>(*(T*)value.Value);
— which silently discards the address. That is fine only for an immediate single read. It makes three things impossible: pointer arithmetic (there is no address left to advance), observing writes that native code makes afterward, and — worst — handing the pointer back to the OS, which then operates on the address of a managed box field instead of the native block.
syscall.Environ does all three. It walks the GetEnvironmentStringsW block and frees it:
envp, e := GetEnvironmentStrings()
defer FreeEnvironmentStrings(envp)
for *envp != 0 { … end = unsafe.Add(end, size) … }
Against a copy, the walk would scan the GC heap and the deferred FreeEnvironmentStringsW would ask
Windows to free GC memory — an outright STATUS_HEAP_CORRUPTION (0xC0000374) process kill on a bare
os.Environ().
So ж<T> has a native reference kind (NativeBox<T>) alongside the standard value, struct-field and array-element
refs: a box that aliases a native address. Value/ValueSlot read that memory through
Unsafe.AsRef, IsNull is address-based (address 0 is the nil pointer, matching Go’s
(*T)(unsafe.Pointer(uintptr(0))) == nil), and the uintptr/void* operators round-trip the
address exactly — which is what Go’s uintptr(unsafe.Pointer(p)) guarantees. unsafe.Add,
unsafe.Slice and unsafe.String honor the kind.
unsafe.Add has an unsafe.Pointer overload too. Go’s unsafe.Add is byte arithmetic and
its argument is always an unsafe.Pointer, but golib models unsafe.Pointer as ж<uintptr> whose
value is the address — so the generic ж<T> overload, which resolves a managed array-element
reference, would find none and return a nil pointer, and stepping through a native block would
dereference address 0 on the very first step.
unsafe.Slice over a genuinely native address yields a native-backed slice over that memory
(slice<T>.OverNativeMemory), so writes reach the memory as in Go.
unsafe.String copies the
bytes into a managed string, which is faithful because a Go string is immutable.
An address of MANAGED storage that outlives its statement must carry a PIN
The native-address box above is exactly right when the address IS native memory — there is nothing
behind it the collector could move. It is a dangling pointer when the address points into managed
storage, and the reinterpret fallback produces precisely that: where (*U)(unsafe.Pointer(p)) cannot
alias p’s storage in the managed model, golib names it by address instead
(PointerExtensions.Reinterpret → (ж<U>)(uintptr)box), and the uintptr operator’s
fixed (void* ptr = &value.Value)
return (uintptr)ptr;
pins for that statement and no longer. The derived pointer outlives it. Once a collection moves
the storage, reads through the pointer return whatever now occupies the old address — and writes
land in whatever now owns it. The second is the one that matters: it is not a wrong value, it is
silent heap corruption, and the crash surfaces later, somewhere unrelated. The corpus’s clearest
instance of the shape is os_windows_test.go’s createMountPoint, whose four uint16 field stores
go through a []byte scratch buffer addressed as a reparse record:
byteblob := make([]byte, buflen)
buf = (*windows.MountPointReparseBuffer)(unsafe.Pointer(&byteblob[0]))
buf.SubstituteNameOffset = target.substituteName.offset // … and three more
var byteblob = new slice<byte>(buflen);
buf = Ꮡ(byteblob, 0).Reinterpret<byte, windows.MountPointReparseBuffer>();
buf.SubstituteNameOffset = target.substituteName.offset;
The rule: the pin’s lifetime is the DERIVED POINTER’s, not the address-taking statement’s. The
fallback asks the source box for a pinned address (ж<T>.TryPinnedReinterpret), and the derived
box OWNS that pin — a PinnedBuffer.PinOnly handle held in the box’s m_pin field and released by
its finalizer when the box is collected. This is the same field and the same idiom as the fixed-array
syscall-buffer pin (pinnedArrayData, above); the two uses are disjoint, since a native box never
takes the lazy one.
Only an array/slice-element reference can be pinned, and that is not a shortcut — it is the only
reference kind whose storage is an object the runtime can be asked to hold still. A native alias has
nothing managed behind it; a nil box has no storage; and the storage of a standard heap box and of a
struct-field reference alike was then a field of a ж<T> object, which GCHandle refuses to pin.
Those kinds keep the address route, so
the pin is strictly additive: where a pin cannot be taken, nothing is newly wrong. That is also what keeps
reflect’s prefix-downcast idiom
((*structType)(unsafe.Pointer(t)) over a *abi.Type, structurally unrepresentable and deliberately
on the address route) working — a blanket “fail loudly” is not available. (The address
route has a pin of its own: the uintptr operator calls EnsureStableAddress before it
takes a managed address, and a standard heap box over a reference-free T keeps its value in a
pinnable one-element slot — see EVERY managed address handed to native code is pinned for the
pointer’s lifetime, above. The reinterpret arm’s own TryPinnedReinterpret is still element-storage
only.)
The pin is cross-checked before it is trusted. It is taken on the backing store
the element box’s canonical (storage, absolute index) pair names, which proves something only if the referent
really lives inside that object,
so the address reached through the box’s own value slot must be the same byte as the address of that
backing’s element; a view whose Source is a detached copy fails the test and gets no pin.
Guarded by tests/Behavioral/ReinterpretPinLifetime, which is deterministic in both directions — it
writes through the derived pointer before and after enough allocation churn to move and recycle the
buffer, and reads back through both the derived pointer and the original slice; without the pin the C#
prints read: false true true / write: false false false false false false where Go prints all
true. Its sibling
ReinterpretPointerLifetime guards the
other half of the same contract — the ALIASING route, for reinterprets the managed model can
represent.
Not fixed by the pin, and a different class: a destination struct holding a managed reference where
Go has an inline array (PathBuffer [1]uint16 → array<uint16>) fabricates an object reference
out of whatever bytes sit at that offset when the field is read. Pinning makes those bytes the real
buffer’s rather than recycled memory, but a fabricated reference is a CLR type-safety break either
way. That is the raw-metal-on-non-native-types fork (os.readReparseLink’s remedy is a hand-owned
decode; os_windows_test.go’s createMountPoint is test code that cannot be hand-owned).
The scalar fields of such a struct read and write at their Go offset through the field property, and
an element or slice of its array field through the field view, so createMountPoint’s stores land where Go
puts them: A field reached through a pointer REINTERPRETED from unsafe.Pointer.
A field reached through a pointer REINTERPRETED from unsafe.Pointer reads at its Go offset — the field property
A local whose every assignment in its function is (*T)(unsafe.Pointer(e)), where T is a struct that holds a
managed reference (an array, string, slice, map, interface or pointer field), reads and writes its fields as Go
spells them: rec.Kind = x, rec.Off += 1, rec.In.X. Such a pointer may alias native or pinned memory, where
the struct’s CLR layout is not Go’s (the CLR places the reference first), so neither the whole-value read
rec.Value nor the field’s CLR slot can serve the site.
type record struct {
Kind uint32
Length uint16
Off uint16
Path [4]uint16
tail
}
…
blob := make([]byte, 24)
rec := (*record)(unsafe.Pointer(&blob[0]))
rec.Kind = 0x01020304
rec.Length = 0x0506
rec.Off = 7
rec.Off += 1
rec.Path[1] = 0x0a0b
n := copy(rec.Path[2:], []uint16{0x0c0d})
rec.B = 0x1112
[GoFieldAccess(typeof(record), typeof(syscall.RawSockaddrInet4), typeof(reparse))]
partial class main_package {
…
var blob = new slice<byte>(24);
var rec = Ꮡ(blob, 0).Reinterpret<byte, record>();
rec.Kind = 0x01020304;
rec.Length = 0x0506;
rec.Off = 7;
rec.Off += 1;
rec.of(record.ᏑPath).ElementRef(1) = 0x0a0b;
nint n = copy(rec.of(record.ᏑPath).slice(2), new uint16[]{0x0c0d}.slice());
rec.tail.B = 0x1112;
rec.Kind is a C# 14 extension PROPERTY on ж<record> that go2cs-gen emits, a ref over golib’s
ж<T>.field:
extension(global::go.ж<global::go.main_package.@record> ʗp)
{
public ref uint Kind => ref ʗp.field(global::go.main_package.@record.ᏑKind);
…
}
field answers the field at its Go offset when the box aliases native memory (the offset resolved once per
accessor from the struct’s Go layout) and ref the struct’s own slot otherwise. Being a ref, one property
serves a read, a write, a compound assignment, a nested field and a ref argument; it allocates nothing.
-
The record. Each file with a site names its struct types in a
[GoFieldAccess(…)]record on its package class, and go2cs-gen emits one extension block per type into aninternal static class GoFieldAccessof the USING compilation. The record is per FILE because a test assembly recompiles the production files beside its own;internalkeeps two packages that extend one foreign type from meeting. -
A foreign type gets a property for each field whose
Ꮡaccessor it can see; a type of the same package gets one for every declared field, since the type generator emits the accessors in the same run. A generic struct getsextension<T>(ж<G<T>> p)with its constraints. -
A field named like a member of
ж<T>(Value,of,at,IsNull, …) gets no property, because an instance member binds first and the property would silently lose to it; the site emitsp.of(T.ᏑValue).Value. The list issrc/go2cs/fieldAccessCollisions.txt, read by the converter, go2cs-gen and the guard that fails whenж<T>gains a member the list lacks. -
A field that shadows a method an embedded type promotes (
abi.ArrayType’s fieldLenagainst theLen()its embeddedabi.Typepromotes) takes the same explicit form: Go selects the shallower field, but the box carries the promoted method as an extension method, and C# bindsa.Lento that method group. -
A field of a struct a hand-owned file declares (runtime’s
iface,efaceandfuncinl, in the hand-ownedruntime2.cs) takes the same explicit form. A hand-owned file’s structs have nopackage_inforecord to make theminternal, so each is a private type nested in the package class, which the generated extension class cannot name. A field promoted into such a struct keeps the dereference. -
An element of an array field,
p.arr[i], emitsp.of(T.Ꮡarr).ElementRef(i), and a slice of one,p.arr[lo:hi], emitsp.of(T.Ꮡarr).slice(lo, hi): the field view is a native window over native memory (An element pointer reinterpreted as an array pointer). -
A field promoted through value embeds walks them by name,
rec.tail.B(Go’s own explicit spelling ofrec.B); the embed’s property is itsrefaccessor’s. A field promoted through a POINTER embed lives in another allocation and keeps the dereference. -
&p.fkeepsp.of(T.Ꮡf), which already answers a view at the Go offset. -
A header-shaped struct gets GET/SET properties instead of
refones. Such a struct is anunsafe.Pointerfield followed by one or twointfields: internal/unsafeheader’sSliceandString, and runtime’ssliceandstringStruct. The setter isset { ʗp.field(T.ᏑLen) = value; ʗp.OnHeaderStore(); }. golib views a slice or string variable as such a header ((*unsafeheader.Slice)(unsafe.Pointer(&s))), and the setter is the only point at which golib can commit each store to the variable. Arefcannot be intercepted, and the last store of a field-by-field build is followed by no access of the box. Reads, stores and compound assignments bind the property as before; arefto such a field does not compile. The commit and its three limits belong to golib: the header boxes inж.SliceHeaderBox.csandж.StringHeaderBox.cs. Guarded by GenTests’ two header arms and GolibTests’HeaderWriteThroughTests; internal/unsafeheader’sTestWriteThroughHeadervalidates against Go.
Limits. A field whose own CLR form is a managed reference (an array, a string) still cannot be read WHOLE
over native memory: field refuses it by name, runtime error: cannot read native memory as main.record: its
CLR layout is not its Go layout (field Path is a managed reference where Go stores the value inline), as the
whole-value read did. Over managed memory every field reads as before. A local assigned anything else (a
new(T), a parameter, a field), a range variable, a multi-value assignment other than the call below, or a
local whose address is taken keeps the p.Value.f / (~p).f emission.
One step across a return, for native bytes only. A local assigned from a CALL takes the same property form,
single- or multi-value (the local at position i takes result i), when the callee lays the struct over BYTE
STORAGE: a static, non-generic function or concrete method of the same package with unnamed results, every
return of which gives that position nil, (*T)(unsafe.Pointer(&b[i])) with b a byte array or byte slice,
or a callee local whose every assignment is that form. This is os’s TestRootSymlinkRelativity: its helper
readSymlinkReparseData returns a *windows.SymbolicLinkReparseBuffer over the [N]byte that
DeviceIoControl fills, and the caller reads osRDB.Flags.
func readReparse(flags uint32, fail bool) (*reparse, error) {
var buf [64]byte
if fail {
return nil, errors.New("no record")
}
buf[8], buf[9] = byte(flags), byte(flags>>8)
buf[12] = 'x'
rec := (*reparse)(unsafe.Pointer(&buf[0]))
return rec, nil
}
…
got, err := readReparse(0x0201, false)
for i := 0; i < 1000; i++ {
sink = make([]byte, 1024)
}
runtime.GC()
fmt.Println("returned:", got.Flags, got.Path[0], err)
var (got, err) = readReparse(0x0201, false);
for (nint i = 0; i < 1000; i++) {
sink = new slice<byte>(1024);
}
runtime.GC();
fmt.Println("returned:", got.Flags, got.of(reparse.ᏑPath).ElementRef(0), err);
The box owns its pin: the reinterpret of a managed element pins the backing and hands the pin and its source to
the native box it returns, so the returned record keeps the callee’s array still and alive across the
collection. A nil result reads as Go’s nil dereference panic. The step is ONE: a callee that returns another
call, a named-result callee (a bare return or a deferred write reaches its result), a generic or cross-package
callee, a call through a func value and a callee over any storage that is not bytes (an unsafe.Pointer
parameter, a uintptr, a []uint64 element) all keep (~p).f. Those reinterpret managed storage of another
Go type, which (~p).f already reads correctly, and only a struct laid over raw bytes reaches the refusal.
Guarded by: tests/Behavioral/FieldAccessReinterpret (byte-level, against Go; with the route off it exits
with the refusal above), the converter’s TestFieldAccessReadsThroughTheFieldProperty and
TestFieldAccessCrossesOneReturn (the return step and its negatives), GenTests’
FieldAccessGeneratorTests (the collision guard among them), and GolibTests’ FieldAccessTests (Go offsets,
the refusal, zero bytes allocated per call).
unsafe.Slice over MANAGED element storage ALIASES it
unsafe.Slice’s last-resort arm snapshots the pointee — a fixed read copied into a fresh slice
— and that is the wrong answer for the far commoner shape: unsafe.Slice(&s[i], n), where the pointer
addresses an element of a managed slice or array.
Go’s result shares that storage, so writes through the rebuilt slice must land in the original
backing — a snapshot would silently swallow every one of them. crypto/subtle is the case: XORBytes
hands xorBytes bare pointers, which rebuilds its three slices and writes the whole result through
dst, so against a snapshot XORBytes would write nothing at all (its test matrix compares dst
against its untouched 0xdd fill).
ж<T> answers with the window when it has real managed element storage
(golib’s ElementWindow.TryGet): the referent is reduced to the same canonical (storage, absolute
index) pair pointer equality uses — so a pointer taken through a re-sliced view addresses the same absolute element Go’s
would — and the result is a slice<T> over that backing with len == cap == n, exactly Go’s shape.
A heap box, a struct-field ref, or a REINTERPRETING pointer (a (*U)(unsafe.Pointer(&b[0])) over a
differently-typed array) has no such storage and keeps the snapshot; a T[] view over another
element type does not exist in the managed model.
That last exclusion is why crypto/internal/fips140/subtle/xor_generic.cs is hand-owned
([module: GoManualConversion]). Its word-at-a-time loop reinterprets the byte slices as
[]uintptr —
func words(x []byte) []uintptr {
return unsafe.Slice((*uintptr)(unsafe.Pointer(&x[0])), uintptr(len(x))/wordSize)
}
— which the converted form can only snapshot, so for every length that is a multiple of 8 the XOR
went to a detached buffer and dst stayed untouched, while other lengths landed only their trailing
n % 8 bytes. The hand-owned file does the same reinterpret the managed way,
MemoryMarshal.Cast<byte, ulong> over the slices’ own spans — a genuine aliasing view, so the word
writes land in place. It keeps Go’s word-at-a-time behavior (and with it the performance contract
crypto/cipher’s CTR and GCM modes depend on) and drops only Go’s supportsUnaligned/aligned gate,
which exists for architectures whose unaligned word loads fault. (crypto/subtle’s own suite covers it,
over the full 1..1024 × 8 × 8 × 8 alignment matrix.)
A reinterpret of a MANAGED pointer aliases the box — it never round-trips through the address
The section above is about a pointer whose source genuinely is an address. The mirror case is
(*U)(unsafe.Pointer(p)) where p is an ordinary Go pointer *T — the shape reflect uses to view
one struct as another (toRType: (*rtype)(unsafe.Pointer(t))). Both pointee types are managed, so
there is no native memory anywhere in the expression, so it must not route through the raw-address
seam:
return (ж<view>)(uintptr)(new @unsafe.Pointer(Ꮡh)); // through the address: unsound
return Ꮡh.Reinterpret<header, view>(); // the emission: aliases the box
The address form is not merely indirect, it is unsound. golib’s implicit operator uintptr(ж<T>)
ends in fixed (void* ptr = &value.Value) return (uintptr)ptr; — fixed pins only for the duration
of its own statement. The address escapes it, and (ж<U>)(uintptr) then builds a native-backed box
holding no reference to the source. The derived pointer therefore neither keeps its pointee alive nor
survives the collector moving it. Consumed immediately it happens to work; retained, it dangles,
and the read comes back silently wrong once another allocation reuses the address.
That is exactly what reflect does — canonType caches the reinterpreted rtype for process
lifetime — so after enough heap churn TypeOf(x).Kind() would report Invalid mid-process, which
inverts fmt.Sprint’s “space only between two non-strings” rule.
The golib extension Reinterpret<T, TDst>() decides by provenance first:
| Source pointer | Result |
|---|---|
A nil box, or a plain null reference (both are Go’s nil pointer) |
ж<TDst>.NilBox — Go’s (*U)(unsafe.Pointer((*T)(nil))) == nil
|
Aliases a NATIVE address (m_nativeAddr — a Win32 API’s returned block) |
the same address; the interop contract above is untouched |
| Owns MANAGED storage, and the reinterpret is representable (below) | a box aliasing that storage, through ж’s existing struct-field-ref kind |
| Anything else | the pre-existing address route |
The managed arm recomputes its ref from a live object reference on every access
(Unsafe.As<T, TDst>(ref …ValueSlot)), so it is GC-safe and needs no pin. It composes through the
field-ref and array-element reference kinds — a reinterpret of &s.f or &a[i] aliases the real
storage rather than a copy — and two reinterprets of one box compare equal, as Go requires (ж
equality compares the source object plus the accessor, and the accessor is a static method).
It is an extension method on ж<T>?, which is what lets a null source be tolerated at all: a
zero-valued pointer field is a plain null, and an instance call on it throws where Go yields nil.
That is why the emission carries both type arguments.
Why the managed arm is gated. Go’s rule for (*TDst)(unsafe.Pointer(p)) is that TDst is no
larger than T and the two share an equivalent layout — but that rule cannot be inherited, because a
go2cs surrogate’s C# layout is not its Go layout: a Go [2]byte is 2 bytes while array<byte> is a
single reference to a backing store, a Go string is 16 bytes while @string is 8, a Go []byte is
24 while slice<byte> is 32. A valid Go reinterpret can therefore become an oversized
Unsafe.As that reads past the value slot into the box’s own private fields and materializes a
fabricated managed reference — a CLR type-safety break, strictly worse than the contained
wrong-read the address route gives. So the alias is taken only where it is demonstrably safe: both
pointees value types; the destination fits inside the source; and either neither type contains
managed references, or the two are layout-compatible in the senses the converter generates (the same
type, a single-field wrapper over the other — Go’s struct-embedding idiom and the generated
named-type wrappers — or an identical recursive field-type sequence). Everything else falls back to
the address route, so the change is additive: where it does not apply, behavior is exactly what it
was.
This mirrors Go’s own rule on the other axis too: a pointer obtained through unsafe.Pointer is a
real reference the collector tracks, while a uintptr is a number that does not keep its referent
alive — so an arithmetic-derived source ((*U)(unsafe.Pointer(uintptr(p) + off))) keeps the address
route.
What this deliberately does not cover: Go’s prefix-downcast idiom, where the runtime allocates a
larger struct, hands out a pointer to its embedded header, and casts back
((*structType)(unsafe.Pointer(t)) with t a *abi.Type). In Go the larger allocation is really
there; in the managed model a ж<abi.Type> holds only an abi.Type, so there is nothing behind it
to downcast to. Those sites keep the address route and remain the raw-metal class — which is why the
two of them that converted code actually reaches, abi.Type.StructType() and ArrayType(), are
hand-owned and SYNTHESIZED instead (see
abi.Type’s SPECIALIZATIONS are synthesized, not downcast).
Emission detail: the peeling is shared with the identity-reinterpret elision
(pointerConversionSource — it unwraps an optional abi.NoEscape/noescape wrapper and an
unsafe.Pointer(p) conversion, but never a function that merely returns unsafe.Pointer, e.g.
mallocgc). Identical element types stay the identity elision described above; differing element
types are the genuine reinterpret. The interception sits at the two points that emit the address
route — the conversion path and the regular-call path, since (*U)(unsafe.Pointer(…))
mis-classifies as a non-conversion and reaches only the latter — deliberately not upstream with the
identity elision: the re-box routes above render their own conversions correctly, and diverting them
breaks named ARRAY wrappers, whose lazily-materialized backing store a storage reinterpret bypasses.
A pointer-to-ARRAY target is excluded at the CONVERTER, not at the golib gate. For
(*[N]T)(unsafe.Pointer(p)) the interception can only ever lose. golib never takes the managed arm
for it — array<U> is an 8-byte struct holding a backing-store reference, so it fails the size
gate against any smaller pointee and the reference gate against any numeric one, and Reinterpret
falls straight through to the address route it was meant to replace. But the address route’s text
is not inert: the slice-of-pointer-cast fusion in convSliceExpr keys on a leading (ж<…>
(isPointerCast) to lower (*[N]T)(ptr)[:n] into a slice<T> over a ReadOnlySpan<T> of the
pointed-to memory — the only correct lowering of that idiom, since an array<T> can neither view
native memory nor be punned out of a scalar’s bytes. Emitting Reinterpret defeats the match and
leaves (~box).slice(…) over an array<T> whose backing reference was read out of the pointee’s
data: a fabricated managed reference, i.e. an AccessViolationException that kills the process
rather than the contained wrong read the address route gives. Measured end to end on
internal/syscall/windows/registry.GetStringValue — the read behind time.initLocalFromTZI and
mime.initMimeWindows, so essentially every Windows program that formats a local time or looks up a
MIME type: same probe, Windows 10 Pro before and a hard fault after. pointerReinterpretManagedSource
therefore returns nil for an array-underlying target, keeping the address-route emission, fused
or not. (reparse_windows.path() → os.Readlink, registry Get/SetStringValue, os/user, and
reflect.gcSlice were on that route; further corpus sites were already on the address route and
were unchanged. The same-element-type members of this
shape take the element-aliasing AliasPointer route — An element pointer reinterpreted as an
array pointer ALIASES the element’s storage, above.)
(Guarded by the ReinterpretPointerLifetime behavioral output test — an aliasing case, plus a
lifetime case whose reinterpreted pointer is the only surviving reference across heavy allocation
churn; before the fix it printed lifetime: true false false against Go’s true true true. The
gate’s fallback is guarded by FixedArrayBufferPointer, whose fixed-array pin must survive, and the
array-target exclusion by PointerCastSliceReinterpret, an output test over the NON-IDENTITY
pointer-cast slice that PointerCastSliceRange explicitly defers to “the stdlib exercises that
shape” — which is exactly how the fabrication reached the corpus untested.)
A pointer-cast slice with a LOW bound offsets the span
(*[N]T)(ptr)[lo:hi] lowers to a slice<T> over a ReadOnlySpan<T> of the pointed-to memory (see
the fusion above). Go’s expression is the elements lo..hi, so the span starts at element lo and
runs hi - lo; a span built from element 0 with length hi would drop the low bound and hold the
wrong elements whenever lo is not 0. A byte view of wider elements shows it:
tail := (*[8]byte)(unsafe.Pointer(&words[0]))[3:7]
var tail = new slice<byte>(new ReadOnlySpan<byte>((byte*)(uintptr)(@unsafe.Pointer.FromPinnedBox(Ꮡ(words, 0))) + 3, 7 - 3));
A same-element-type view such as the reparse buffer’s (*[0xffff]uint16)(unsafe.Pointer(&rb.PathBuffer[0]))[n1:n2:n2]
(*[0xffff]uint16 over a *uint16) takes the element-aliasing route instead —
array<uint16>.AliasPointer(Ꮡ(rb.PathBuffer, 0), 65535).slice(n1, n2, n2) (above) — and the span
lowering, with this low-bound offset, serves the pointer-cast slices that route does not.
A pointer cast binds tighter than +, so the offset lands on the typed pointer with no extra
parentheses. Two consequences a dropped low bound would have: internal/syscall/windows’s
(*symbolicLinkReparseBuffer).path() slices [n1:n2:n2] to skip the print name, so os.Readlink
would return the reparse buffer from offset 0 instead of the substitute name; and internal/abi’s
FuncType.OutSlice() slices [InCount : InCount+outCount], so it would return the in-parameters
followed by the out-parameters and reflect.Type.Out(i) would index the wrong half (reflect.gcSlice
[begin:end:end] likewise would read the GC bitmap from the wrong start). (Guarded by the non-zero-low
arms of PointerCastSliceReinterpret — the reparse [n1:n2:n2] shape and a byte reinterpret of a
wider element sliced [3:7], both matching Go. StdLibInternalAbi’s golden
pins the corrected OutSlice; its own stdout does not depend on the offset.)
unsafe.Pointer(p) on a pointer PARAMETER renders the box, never a deref
A pointer parameter is emitted as the box ж<T> Ꮡp plus a deref’d VALUE alias
(ref var p = ref Ꮡp.DerefOrNull(); the strict ref Ꮡp.Value when this rule was written). Taking its address through
that alias —
@unsafe.Pointer.FromRef(ref p) — forces the alias to be materialized, so the entry-time deref
raises a nil-pointer panic for a nil argument, even though Go never touches the pointee and
uintptr(unsafe.Pointer(nil)) is defined to be 0.
Nil out-pointers are idiomatic in the syscall wrappers. DuplicateHandle takes lpTargetHandle
*Handle as nil together with DUPLICATE_CLOSE_SOURCE to close a handle without receiving a
duplicate, and syscall.StartProcess does exactly that in a deferred call — so spawning any child
process panicked there.
The address comes from the box: @unsafe.Pointer.FromPinnedBox(Ꮡp) ((uintptr)Ꮡp under an enclosing
uintptr(…), per the dead-wrapper peephole above). golib’s
implicit operator uintptr(ж<T>) already yields precisely the address Go wants — 0 for a nil box,
the aliased address for a native pointer (above), the pinned storage otherwise. Rendering the box
also removes the bare value reference from the body, so an otherwise-unused alias is dropped as dead
by the scan described under A pointer parameter used only through its box gets no deref VALUE alias
and the entry deref disappears with it. This holds for every pointee shape, including a named-numeric
pointee (*Handle) and a pointer-to-pointer (**uint16).
A pointer receiver keeps the FromRef form — a this ref T receiver has no box to address.
Guarded by NilPointerParamUnsafePointer (nil and non-nil arguments across all three pointee shapes,
plus a case where the value alias stays genuinely live so the box rendering must not drop it).
A reference-type-pointee pointer parameter uses the nil-check-free .ValueSlot deref alias
(This arm is superseded: every pointer parameter’s entry alias is now the nil-deferring
DerefOrNull(), whose non-nil path IS ValueSlot — see A pointer PARAMETER is nil-deferring for
exactly the reason a receiver is, above. The record of why the arm existed follows.) The entry
deref-alias for a pointer parameter was ref var p = ref Ꮡp.Value. The .Value getter
throws NilPointerDereference when the box reports IsNull — for a MANAGED box, m_val is null.
That is correct when the pointee is a VALUE type (a null m_val means a genuinely nil pointer). But
when the POINTEE is itself a reference type — *error, *[]T, *map[K]V, **T, *func(…),
*chan T — the box holds the reference VALUE directly, and that value is legitimately null when it
is the zero value (a nil interface/slice/map). The pointer is still a valid, non-nil box (Ꮡ(err)),
so establishing the entry ALIAS is a read of the held value, not a dereference of the box: in Go,
*(&err) of a nil error yields nil, no panic. .Value’s IsNull check misfires on m_val is
null and panics spuriously at function entry — text/scanner’s digits(…, invalid *bool) and, for a
reference pointee, text/tabwriter’s handlePanic(err *error) (deferred from Write, whose err is
a nil named-return interface) crashed with a nil-pointer panic before recover() even ran. The fix:
when the pointee isInherentlyHeapAllocatedType, emit the nil-check-free .ValueSlot accessor
(ref var err = ref Ꮡerr.ValueSlot), mirroring namedResultBoxAccessor — a named result of the same
type already reads this way. .ValueSlot returns the same real m_val slot as .Value in every
non-throwing case, so write-through and non-null reads are byte-behaviorally identical; only the
spurious-panic case changes. The swap was value-preserving corpus-wide (full behavioral suite Output
0-fail). (Guarded by the PointerToInterfaceParamDeref
behavioral test
— a *error parameter read through inside a deferred recover/re-panic where the pointee is nil at
address-of time; before the fix the entry alias NREs, after it prints the re-panic message,
output-compared vs go run.) ⚠ That fixed only the spurious CRASH. The SEPARATE latent defect it left — a
non-heap-promoted
address-taken named return, where Ꮡ(err) boxed a COPY so *err = … in the deferred handler wrote
the copy while return err read the original — is closed too: an address-taken named result is
heap-boxed (heap<error>(out var Ꮡerr); in text/tabwriter’s flush; see A NAMED RESULT routed to
shared storage declares its box too, above).
The value-type nilable case — a genuinely nil *rune/*bool/*int optional-out-param, deref’d only
under a body VALUE guard — was once handled by a companion call-site nil-argument
detection, and is now subsumed by the unconditional nil-deferring entry alias (see A pointer
PARAMETER is nil-deferring for exactly the reason a receiver is). The problem it solved is worth
keeping on the record, because it is the cleanest demonstration of why an entry-alias policy cannot be
an analysis: collectNilSafePtrParams scanned only the body for param == nil/!= nil, so
text/scanner’s digits(ch0 rune, base int, invalid *rune) — whose sole deref *invalid == 0 sits
behind ch >= max (never invalid != nil) and which is called digits(ch, 10, nil) — kept the
strict .Value entry hoist and NRE’d at entry, where Go never dereferences (ch >= max is false on
the nil-call path). The remedy was a package-wide pre-pass (collectNilArgPtrParams) recording, per
*types.Func, the pointer-parameter positions ever passed the untyped nil at a call site — which
worked, but only for SAME-package call sites, because the converter processes one package at a time.
A parameter passed nil solely from another package stayed strict and stayed broken; that residual is
what .DerefOrNull() closes structurally, and the pre-pass was deleted with the rest of the analysis.
(Still guarded by the GuardedNilPointerParamDeref behavioral test — a *int out-param deref’d under
an i >= base guard, called once with a real pointer and once with nil; NREs at the entry hoist under
either predecessor, matches go run now without one.)
A pointer-element composite literal takes the box for a deref-aliased ident
A bare identifier element of a pointer-element composite literal ([]*CommentGroup{c}) renders
the pointer VALUE — the box Ꮡc — not the deref’d receiver ref-local c. Every named pointer
parameter is deref-aliased in C# (ref var c = ref Ꮡc.DerefOrNull()), and the bare name is the value
alias; the array element type is ж<CommentGroup>, so the alias form was CS0029 (go/ast’s
CommentMap.addComment — the sibling append(list, Ꮡc) already took the box through the
call-argument pointer arm). The routing mirrors the struct-field pointer arm: the element index
is marked argTypeIsPtr, which convExprList turns into the pointer ident context:
list = new ж<CommentGroup>[]{Ꮡc}.slice();
Gated to bare idents of pointer type — keyed elements (maps) and address-of/composite elements
manage their own pointer rendering. Guarded by the PointerParamWalk extension collect (the
literal arm and the append arm, aliasing proven by a post-collect write through the original).
A pointer value passed to an any argument takes the box
A deref-aliased pointer passed WHOLE (as an argument, not p.field) to an EMPTY-interface (any)
parameter renders the pointer VALUE — the box Ꮡp — not the deref’d value alias p. Go boxes the
pointer into the interface, so dropping the box stores the pointed-to VALUE and loses pointer
identity: a later x.(*T) assertion (rendered ._<ж<T>>()) then finds a bare T and panics
(“interface conversion: … is T, not *T”). This is fmt’s own sync.Pool round-trip —
func (p *pp) free() { … ppFree.Put(p) } (Put’s parameter is any) feeding newPrinter’s
ppFree.Get().(*pp) — which crashed the SECOND time through the pool, blocking every multi-call fmt
program. Both a pointer RECEIVER and a plain *T PARAMETER take the box:
func (p *pp) free() { poolPut(p) } // p is *pp (pointer receiver); poolPut(x any)
func keep(q *pp) { poolPut(q) } // a plain *T parameter, same shape
internal static void free(this ж<pp> Ꮡp) {
ref var p = ref Ꮡp.DerefOrNull();
…
poolPut(Ꮡp.OrTypedNil()); // NOT poolPut(p) — a pp VALUE loses pointer identity
}
internal static void keep(ж<pp> Ꮡq) {
poolPut(Ꮡq.OrTypedNil());
}
(The OrTypedNil() suffix is the other half of the same boundary — see
A pointer crossing into an interface carries its static type,
which generalized this arm from the call-argument slot to every empty-interface slot.)
This mirrors the composite-literal element arm above: the argument index is marked argTypeIsPtr,
which convExprList turns into the pointer ident context, so convIdent emits the parameter box
(Ꮡp) or the current method’s direct-ж receiver box. It fires ONLY for the empty interface — a
NON-empty interface already routes the pointer through its *T→interface adapter (interfaceTypes),
and the two arms are mutually exclusive. A pointer LOCAL is excluded (it already holds its box
directly — the bare name IS the box), an unsafe.Pointer argument is excluded (not a *types.Pointer),
and the treatment fans out across a variadic ...any. The receiver form reaches through a closure
too — Ꮡs.Value.d.note(Ꮡs.OrTypedNil()) for s.d.note(s) inside a nested lambda (the database/sql (*Stmt)
shape). Guarded by PointerValueToInterfaceArg (a minimal sync.Pool-shaped free list round-tripping
a *pp via both a pointer receiver and a pointer param, each .(*pp)-asserted after the any hop —
the 2nd pool Get panicked before the fix) and the NestedLambdaReceiverField receiver-in-closure case.