The struct module
import struct reaches no unit and no shim.
Why it is next
mimic_array landed 2026-09-09 and cleared the array wall in world.py; the
module's next error is this one. That is the honest shape of the lekkerzeilen
blocker census — see the umbrella — and it is why this ticket exists rather than
the array one simply being reopened.
CORRECTION 2026-09-10 — the population, re-measured at HEAD
The call sites below are labelled rd.py and that is wrong; they are
capture.py's. rd.py imports math and not struct, and compiles clean at
786a00f9a. Re-measured first wall per module, compiler 61f8a78f8aae:
| module | first wall |
|---|---|
world |
struct |
capture |
ctypes (then zlib, then struct) |
rd |
none — compiles clean |
gauges, app |
queue |
grep -l '^import struct' over the runtime package returns exactly
capture.py world.py. So the first-wall count for this ticket is 1, not 3,
and clearing it is not measured as sufficient for capture — which needs two
other things first, one of them deliberately out of scope.
Recorded rather than quietly edited: the wrong number was in the summary, which is the part everyone reads, and it would have ranked this above the queue ticket on a population that does not exist.
Measured surface (lekkerzeilen, 2026-09-09; file labels corrected above)
struct.pack(">I", len(payload)) # rd.py
struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0) # rd.py
struct.pack("<h", 1) != struct.pack("=h", 1) # world.py — endianness probe
struct.pack("<f", 1.0) != struct.pack("=f", 1.0) # world.py
struct.unpack("<%df" % (len(verts) // 4), verts) # scenery-side, RUN-TIME format
struct.unpack("<%dI" % (len(indices) // 4), indices)
So: byte-order prefixes < > =, formats I i h H B f d, and a repeat count
that is computed at run time — the format string is not a literal, so a
frontend-side compile-time format parser is not an option; the shim parses the
format at run time, as CPython does.
unpack returns a TUPLE, and both call sites wrap it in list(...).
Shape
A .pas shim (lib/rtl/mimic_struct.pas), for the reason mimic_array states
in its own header: the job is byte reinterpretation, which Pascal does with a
pointer cast and pure Python can only do by hand-rolling IEEE-754. The typecode
size/signedness table is the one mimic_array.SetTc already carries; the two
should share it rather than growing a second copy.
The = vs < distinction is load-bearing and is not cosmetic: world.py's
only use of struct is to ask whether the host is little-endian, by comparing the
two encodings. A shim that treated = as < would answer "never swap" on a
big-endian host and decode every terrain file backwards — silently, and only
there. Whatever else is subset, that comparison has to be real.
RESOLVED 2026-09-10 (frankB) — lib/rtl/mimic_struct.pas
Byte-identical to CPython across all 33 rows of
test/test_nilpy_the_struct_module.npy, whose .expected is generated by
CPython and not by us — including the float32 precision loss
(0.10000000149011612), the run-time repeat count, and the tuple repr.
The typecode table is SHARED, not copied. mimic_array's twelve rows moved
out of array_.SetTc into a unit-level PyTypecode exported from that unit,
and this shim uses it. Our fixed widths ARE struct's standard sizes — l is 4
bytes here and 4 under any byte-order prefix in CPython — so one table really
does serve both. Copying it would have put mimic_array's divergence note into
two files that must agree with no way to notice when they stop. One shared
table, two vocabularies: u is array's Py_UCS4 and is refused here.
= is resolved against the actual host order, probed at run time. Not a
{$IFDEF ENDIAN_LITTLE}, which is decided by whichever compiler builds lib/rtl
and is wrong the moment it is built once for a cross-target. world.py's only use
of struct is the probe, so this is the one thing that could not be subset.
The tuple tag was free and worth taking. unpack returns a TPyList tagged
PYSEQ_TUPLE, so print(struct.unpack(...)) gives (1234567,) and not
[1234567]. Both corpus call sites wrap the result in list(...) and would
never have noticed — which is exactly why it was worth one assignment: the next
caller prints it, and a repr that quietly disagrees with CPython is found in
someone else's output diff.
Stated divergences, neither tested against the oracle because a row asserting
a known divergence is red by construction: bare l/L (ours 4 bytes, CPython
native 8 on LP64) and @/no-prefix alignment (CPython pads, we do not). Both in
the shim header.
pack is capped at 7 arguments — capture.py's ">IIBBBBB" is the widest
call in the corpus, and a wider one is a clean compile error rather than a wrong
answer. unpack has no such limit, and that is where world.py's run-time count
lands. pack_list is public for a genuinely run-time-sized argument list.
Result on the target: cleared, and NOT sufficient
world.py's first wall moved from struct to math.atan2
(feature-nilpy-math-module-twelve-absent-names-measured, prio 75, already
filed). capture.py is unchanged — its first wall is ctypes, settled as
bind-natively. So this clears one module's first wall and compiles no module.
Log
- 2026-09-10 — resolved; this names the commit that carried the resolve, which is not always the one that carried the change — commit 1eb448030.