Skip to content

Java: byte storage

This bounded Phase 4 change specializes Java byte storage. Host operations, library exports, SDK Java interoperability and remaining target byte storage are still separate assignments.

The emitter recognizes underlying uint8, including byte, non-generic aliases, named elements, named slices and arrays. Byte arrays and slice backing use native byte[]. Go integer values remain Java long: primitive slice readers and array loads mask signed Java bytes with 255, and stores narrow to byte after the existing Go integer operations. Generic slices and arrays retain Object[] storage.

Immutable slice headers hold backing identity, offset, length and capacity. A byte flag on BYTE_NIL preserves the representation through nil reslices, empty spread append and zero-argument append. Allocated empty byte slices have non-null zero-length arrays. make allocates and zeroes the complete capacity, and native append growth allocates zeroed spare capacity. The capacity rule remains max(needed, max(1, 2*cap)). Additions widen before integer casts; size checks precede allocation and mutation. JVM allocations retain the existing Integer.MAX_VALUE - 8 element limit and available-memory constraint; exceeding host limits is an implementation fault.

Native append and copy use System.arraycopy, including overlapping views in both directions. Spread append avoids a full-payload boxed temporary; individual append arguments use primitive byte[] literals. Byte clear fills only the selected view. Array assignment copies into existing backing so array slices retain their alias; array value copies and slice-to-array value conversion allocate independent native arrays. Array equality compares byte values, and array map keys snapshot bytes in an immutable Latin-1 Java string. Named array/interface type identity still comes from the existing type descriptors. Go strings retain one Java character per source byte; byte conversions copy and preserve NUL, high bytes and invalid UTF-8.

Slice.get/set provide scalar boxing only at generic boundaries, including contract encoding. Generated byte index/store operations use primitive bget/bset variants, including unsigned-index checking. Generic function slice consumers and rune conversions use the same scalar accessor without scheduler changes. Headers, arrays, struct fields, interfaces, closures and suspended frames remain ordinary Java GC-reachable values. Element addresses, slice-to-array pointer conversions and source generics remain outside the accepted language subset.

Run from the Goalchemy root. GOTOOLCHAIN selects the compiler’s Go toolchain; the loader and actual native-Go oracle retain their pinned defaults.

Terminal window
GOTOOLCHAIN=go1.25.14 go run ./cmd/goalchemy spec validate
GOTOOLCHAIN=go1.25.14 go run ./cmd/goalchemy spec generate -check
GOTOOLCHAIN=go1.25.14 go test ./tests/contracts -count=1 -v
GOTOOLCHAIN=go1.25.14 go test ./internal/emit/... ./internal/driver ./internal/subset ./internal/specgen -count=1
GOTOOLCHAIN=go1.25.14 GOALCHEMY_TEST_TARGETS=java go test ./tests/language -count=1 -v
GOTOOLCHAIN=go1.25.14 GOALCHEMY_TEST_TARGETS=go,typescript,python,java,csharp,rust,c FIXTURE=byte_storage go test ./tests/language -count=1 -v

The default contract suite includes TestJavaByteStorage, which compiles the common language fixture and then runs targets/java/tests/ByteStorageTest.java against its emitted program and runtime. It asserts actual byte[] backing, physical zeroed capacity, primitive unsigned reads, nil/empty behavior, copy/append overlap, offsets, string copies, array-view aliasing, size errors, and generic non-byte behavior. Reflection invokes emitted primitive array zero/clone/set/equality/key helpers, so boxed arrays cannot pass solely by matching program output. The suite checks emitted primitive paths too.

TestJavaByteGrowthNativeOracle always runs the independent native-Go growth oracle; no environment opt-in is needed. Before this change, Java threw a host NullPointerException when reading grown spare capacity. The fixed observation is 4 255 128 3 0, followed by two zero lines. The historical TypeScript assignment’s opt-in reproducer also now passes for Java:

Terminal window
GOTOOLCHAIN=go1.25.14 GOALCHEMY_BYTE_TARGETS=java go test ./tests/contracts -run TestTypeScriptByteGrowthNativeOracle -count=1 -v

The common byte_storage fixture compares real native-Go observations for aliases/named types, full slices, growth detachment, arrays and struct copies, escaped pointers, interface aliases, closures and arbitrary byte strings. The separate native-Go differential java_byte_values fixture adds array and interface map keys, high-byte reads/stores with uint64 indexes, huge unsigned index rejection and byte arrays/slices across cooperative frame and closure suspension. Generic slice, rune and function-slice behavior is also exercised by the full Java language and all-seven contract suites. Emitter/specgen packages currently contain no package-local tests; their behavior is tested by these contract/language runs.

Compile once, then start a fresh JVM for each observation:

Terminal window
mkdir -p out/java-byte-bench
.toolchains/jdk-21.0.12.1+1/bin/javac -d out/java-byte-bench targets/java/types/*.java targets/java/runtime/*.java targets/java/tests/ByteStorageBench.java
.toolchains/jdk-21.0.12.1+1/bin/java -Xms256m -Xmx1g -cp out/java-byte-bench ByteStorageBench native 2
.toolchains/jdk-21.0.12.1+1/bin/java -Xms256m -Xmx1g -cp out/java-byte-bench ByteStorageBench boxed 2
.toolchains/jdk-21.0.12.1+1/bin/java -Xms256m -Xmx1g -cp out/java-byte-bench ByteStorageBench native 8
.toolchains/jdk-21.0.12.1+1/bin/java -Xms256m -Xmx1g -cp out/java-byte-bench ByteStorageBench boxed 8

Measured on Linux x86_64, kernel 6.8.0-87-generic, AMD Ryzen 7 6800H, 16 logical CPUs, pinned Temurin 21.0.12.1+1. The script warms the selected runtime path, measures heap after requested GC, fills source and destination buffers, copies 16 times and grows source by appending it to itself. All three resulting buffers remain reachable and are checked after GC; total retained native byte backing is exactly four times the input size. The boxed baseline calls the retained generic Object[]/Long runtime with the same byte values and operations, including its overlap temporary.

Input Storage Exact native backing Heap delta after GC Copy 16 times Append once
2 MiB byte[] 8,388,608 B 11,052,888 B 0.96 ms 2.39 ms
2 MiB Object[]/Long — 61,436,056 B 234.71 ms 49.93 ms
8 MiB byte[] 33,554,432 B 36,218,712 B 17.88 ms 11.72 ms
8 MiB Object[]/Long — 237,637,872 B 1,536.15 ms 228.91 ms

Array lengths establish exact one-byte element backing; heap deltas include JVM array/object overhead, allocator/GC accounting and measurement noise. Timing and heap observations are illustrative, with no mandatory RSS or heap threshold. Byte-string conversion still copies into immutable Java strings; this is an in-memory runtime, not a streaming API.

C# subsequently passed its bounded native byte-storage checks. 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 assignment preserves the generic representation and does not repair that separate gap. Pending host I/O, key/buffer completion lifetimes, cancellation, shutdown, library exports and generated SDK packages remain Phase 4/5/6 work. Crypto/HTTP unavailable-feature diagnostics GCE002 and the unsupported library-target diagnostic GCE006 remain in place.