Nineteen Matching Fingerprints Missed a Compiler Bug on MVS
A cross-platform determinism check can pass on a build whose internal state is wrong. FlyBatch's 19 golden fingerprints matched on MVS 3.8j while its kernel's zero constant was a subnormal near 1e-318. Here is what the fingerprint hashes, what caught the fault, and the eight-byte move behind it.
What FlyBatch's CRC-32 fingerprint hashes
FlyBatch, called ONFLY until 2026-09-27, runs a 501-neuron fruit fly feeding circuit from one C89 source on x86-64, on Linux s390x under QEMU and on MVS 3.8j under Hercules. What it has to prove is that the same request gives the same answer on all three. The evidence is a fingerprint, a CRC-32 of the response, defined as requirement IR-COM-05 in the project specification.
The hashed bytes are the network payload CRC, the request's numeric fields, the return code, the output and step counts, and then each readout neuron's ID, first-spike latency and spike count, every field a big-endian 32-bit integer. Text is excluded so an EBCDIC host and an ASCII host can agree.
Each of those fields is an integer, so they compare exactly. Membrane and synaptic values are binary64, and the fingerprint never sees one.
200 lines out of 19,909, all in the silent request
The fault surfaced when the project added a chunk stream: the MVS job punches its progress to the host while it runs, including each readout neuron's state as a binary64 bit pattern, and that stream can be diffed against the x86 one. After mapping CRLF to LF, 200 of 19,909 lines differed. Every one was a state line of G-15, the rate 0 request in which nothing should happen (VL-118).
x86 wrote exactly +0.0 for both readout neurons at every chunk, matching the Python oracle. MVS wrote subnormals of the order of 1e-318 that rose over the first 34 chunks and then held bit-identical for the remaining 166 (VL-119). With no stimulus and no synaptic input, the specified timestep gives zero at every step, so x86 was right.
One helper, four ways into the kernel
A probe linking the engine's own float units printed FPP ZERO 00000000000AE782 as its first line. onffzer(), the function that returns +0.0, did not, although its C source was correct (VL-122). The engine initialised every membrane value, synaptic value and ring-buffer entry from it, cleared each ring slot with it after use, and reset a neuron's synaptic value with it after each spike.
It also fed the subnormal clamp. Requirement NR-08 sets any synaptic value whose magnitude is below G_EPS, fixed at 2^-1022 (the smallest normal binary64), to exactly +0.0, so no platform does subnormal arithmetic and hosts that handle subnormals differently cannot disagree. On MVS, every time the clamp fired it wrote the subnormal back in.
No fingerprint moved. All 19 golden requests matched, and the response records equalled the x86 recordings byte for byte apart from one text field that MVS stores in EBCDIC (VL-125, release notes). A value near 1e-318 is swamped by every normal-magnitude number it meets and never approaches the 7.0 mV spike threshold; the specification calls that luck, since nothing in the engine protected against it.
The eight-byte move from a four-byte literal
The minimal reproducer, committed on 2026-09-27, is 26 lines of C89 with nothing of FlyBatch in it: a two-word structure, a function zero0(void) that sets both words to 0UL and returns it, and make2(h, l), which returns its arguments. Under GCCMVS 3.2.3 at -O1, zero0 printed lo=000A5B8A. At -O0, and under the JCC 1.50.00 compiler, both functions printed zeros.
The -O1 assembler listing shows why. The code block for zero0 is LR 2,0, MVC 0(8,2),=F'0' and LR 15,0, and the MVC is an eight-byte move from a fullword literal that is four bytes long. The four bytes after that literal are the function's page table, whose one entry is the address constant DC A(PG0), so the low half of the zero receives an address instead (VL-145).
The engine's wrong zero changed between two probe builds, 0AE782 in one and 0AE612 in the next, so it depended on code layout (VL-124). VL-145 records that it matches the reproducer's address-like word in kind. In the reproducer, the load address behind 000A5B8A is itself inferred, since none was printed.
Fixing it without exposing the representation
Decision D-420 removed onffzer() from the float API. Its three call sites now use onffbit(0UL, 0UL), which builds the value from two explicit words and was measured returning 0000000000000000 on the same failing build.
D-420 rejected three alternatives, one of them leaving the engine as it was. A dummy argument rested on the untested theory that the empty argument list was the trigger. Writing z.hi = 0; z.lo = 0 at each call site also measured correct, but it puts the binary64 layout into the kernel, which the float layer exists to keep it out of.
After the fix the MVS and x86 streams are byte-identical. A JCC rebuild returned the same five fingerprints its row already carried, and the specification is explicit that this cleared nothing: a check the bug never moved cannot acquit a compiler of it (VL-128). The reproducer later answered the JCC question, for this one program.
The limits are the project's own. One program, run on an emulator, with GCCMVS's code generator source unread and other structure sizes and constants untried. FlyBatch has not run on IBM Z hardware, and every MVS figure here comes from MVS 3.8j under Hercules on one laptop (release notes).
Diff the state before trusting the fingerprint
If you build a cross-platform determinism check, list the internal state it cannot see, and add one comparison that does cover it. Hashing the answer is correct for a matrix that certifies answers. In FlyBatch, the stream's readout membrane lines were the first MVS check able to see a wrong zero that changed no spike (VL-121), and the float self-test now asserts the zero's bit pattern on MVS (D-421).
The same port first had to fit every SoftFloat symbol into eight characters, covered in porting Berkeley SoftFloat to MVS 3.8j. Run the structret probe in tools/mvsgcc.py from the FlyBatch repository against a TK5 system to reproduce the move yourself.