C#: byte storage
This bounded Phase 4 change specializes C# byte storage. Host operations, library exports, generated SDK interoperability and remaining target byte storage remain separate assignments.
The emitter recognizes underlying uint8, including byte, aliases, named
elements, named slices and named arrays. Byte arrays and slice backing use
native byte[]; generic slices and arrays retain object[]. Scalars keep the
existing Go long representation. C# bytes are unsigned, so primitive reads
widen directly to long; stores narrow with an explicit unchecked cast.
Generic slice accessors box bytes as long, preserving interface and contract
encoding behavior rather than exposing boxed host byte values.
Immutable headers retain backing identity, offset, length and capacity. Their
Array backing holds byte[] or object[]; the BYTE_NIL hint preserves byte
storage through nil views, empty spread append and zero-argument append. Nil
remains distinct from allocated zero-length storage. Byte make and growth
allocate the complete, physically zeroed capacity. The capacity rule remains
max(needed, max(1, 2*cap)). Size addition widens before casts; host-limit
checks precede allocation and mutation. Existing make validation and the
int.MaxValue - 64 host element limit remain; available host memory also
limits allocation. Host-limit rejection is an implementation fault, while
source bounds and invalid make sizes remain source panics.
Native append and copy use overlap-safe Array.Copy, including both overlap
directions. Spread append has no full-payload boxed temporary; individual
arguments use primitive byte-array literals. String append writes directly
to the selected native destination. Clear zeros only the selected view. Array
assignment copies into existing native backing, preserving array/view aliases;
array value copies and slice-to-array value conversion allocate independent
native arrays. Array equality compares byte values and map-key helpers create
immutable Latin-1 string snapshots. Named array and interface identity still
comes from the existing type descriptors. Go strings retain one host character
per source byte, and conversions copy NUL, high bytes and invalid UTF-8 without
a text decoder.
Generated byte loads/stores use primitive bget/bset paths, including
unsigned-index checks. Generic function-slice consumers, rune conversion and
contract codecs use scalar accessors. Arrays, headers, struct fields, pointers,
interfaces, closures and suspended frame fields remain ordinarily reachable
through .NET GC. Element addresses, slice-to-array pointer conversions and
source generics remain outside the supported language subset. No host-I/O
ownership or retained-key lifecycle is introduced here.
Verification
Section titled “Verification”Run these from the Goalchemy root. GOTOOLCHAIN selects the compiler’s Go
toolchain; the loader and real native-Go oracle retain their pinned defaults.
GOTOOLCHAIN=go1.25.14 go run ./cmd/goalchemy spec validateGOTOOLCHAIN=go1.25.14 go run ./cmd/goalchemy spec generate -checkGOTOOLCHAIN=go1.25.14 go test ./tests/contracts -count=1 -vGOTOOLCHAIN=go1.25.14 go test ./internal/emit/... ./internal/driver ./internal/subset ./internal/specgen -count=1GOTOOLCHAIN=go1.25.14 GOALCHEMY_TEST_TARGETS=csharp go test ./tests/language -count=1 -vGOTOOLCHAIN=go1.25.14 GOALCHEMY_TEST_TARGETS=go,typescript,python,java,csharp,rust,c FIXTURE=byte_storage go test ./tests/language -count=1 -vGOTOOLCHAIN=go1.25.14 go test ./tests/contracts -run TestCSharpByte -count=1 -vAll these checks passed without skips. The catalog remains 13 type contracts, 94 function contracts, seven targets and 502 generated files. The full C# language suite passed in 50.309 s, the contracts suite in 52.786 s, and the shared byte fixture across all seven targets in 32.854 s. Driver and subset checks passed in 18.627 s and 53.976 s respectively. These are observations, not timing requirements.
The default contracts include TestCSharpByteStorage, which compiles the
common fixture and runs targets/csharp/tests/ByteStorageTest.cs against the
emitted program/runtime. It asserts actual byte[] backing, full physical
zeroing, native nil/empty behavior, offset/capacity identity, overlap, string
copies, scalar long boxing, host-limit checks before mutation and retained
generic storage. Reflection exercises emitted primitive zero/clone/set/equality
and immutable key helpers, in addition to emitted-path checks; boxed storage
cannot pass merely by printing equivalent observations.
TestCSharpByteGrowthNativeOracle always compares generated growth behavior
with native Go. Before this change, C# raised NullReferenceException when
reading grown spare capacity. The fixed observation is 4 255 128 3 0, followed
by two zero lines. The earlier TypeScript assignment’s opt-in reproducer also
passes for C# now:
GOTOOLCHAIN=go1.25.14 GOALCHEMY_BYTE_TARGETS=csharp go test ./tests/contracts -run TestTypeScriptByteGrowthNativeOracle -count=1 -vThe historical failing observation remains in out/csharp-byte-baseline.log
and the original remaining-target baseline. The common native-Go differential
byte_storage fixture covers named/alias types, arrays and struct copies,
array assignment aliases, escaped pointers, interface aliases, closures,
full slices, clear, detachment and arbitrary string bytes. The default
TestCSharpByteValuesNativeOracle reuses the target-neutral
java_byte_values fixture: array/interface map keys and immutable snapshots,
valid and huge uint64 indexes, invalid make sizes, closures and cooperative
suspension/frame storage. Full language regressions include non-byte slices,
runes, maps, function slices, interfaces and cooperative scheduling.
Memory and throughput
Section titled “Memory and throughput”Exact reproduction with the pinned .NET SDK 8.0.425:
sh targets/csharp/tests/byte_storage_bench.shThe script compiles against the local .NET 8 reference assemblies and runs
four fresh processes: native/boxed storage, each with 2/8 MiB input. The
program warms the selected runtime path, measures managed heap after requested
GC, fills source, makes destination, performs 16 copies and appends source to
itself into a doubled-capacity buffer. Three buffers remain reachable through
payload checks and GC.KeepAlive; exact native backing is four times input
size. The boxed baseline uses the unchanged generic object[]/boxed long
representation, including generic overlap temporaries.
Measured on Linux x86_64, kernel 6.8.0-87-generic, AMD Ryzen 7 6800H, 16 logical CPUs, .NET SDK 8.0.425/runtime 8.0.31:
| Input | Storage | Exact native backing | Managed heap delta | Copy 16 times | Append once |
|---|---|---|---|---|---|
| 2 MiB | byte[] | 8,388,608 B | 8,388,632 B | 7.744 ms | 3.969 ms |
| 2 MiB | Generic object[]/long | — | 117,432,624 B | 342.528 ms | 54.794 ms |
| 8 MiB | byte[] | 33,554,432 B | 33,554,456 B | 30.643 ms | 8.964 ms |
| 8 MiB | Generic object[]/long | — | 469,754,160 B | 1,378.838 ms | 242.191 ms |
Array lengths establish one native byte per retained element. Managed heap
results include runtime overhead and timings vary with GC, JIT, CPU and other
processes; neither heap nor RSS thresholds are mandatory tests. The recorded
measurement output is out/csharp-byte-memory.log. String conversion still
allocates immutable host strings and copies; this is an in-memory runtime,
not a streaming interface.
Remaining SDK requirements
Section titled “Remaining SDK requirements”Python subsequently gained native byte-storage checks. Rust subsequently gained native byte-storage evidence. C subsequently gained native byte-storage evidence. Existing generic non-byte append growth can leave unset spare capacity; this change retains that separate gap. Pending host operations, completion retention, key/buffer lifetimes, cancellation, shutdown, library exports and generated SDK packages remain Phase 4/5/6 work. GCE002 for unavailable crypto/HTTP capabilities and GCE006 for unsupported library targets remain in place.