D468 - The ctype tables are measured off hardware, because the documented layout is the wrong one
measured - 2026-09-02 (user-directed /loop: an obSCEne hardware capture arrived mid-session)
_Getpctype answers a pointer to the table
isalpha, isdigit and the rest index, and the
guest dereferences what it gets back - so the
Unimplemented placeholder is not a wrong answer that
propagates, it is an address read through (D459). Filling it in needs
the table's actual contents.
D448 recorded the plan: FreeBSD documents the ctype table, that is the lawful oracle, transcribe it. That would have produced a wrong table, and this is the entry that says so, because an obSCEne capture of the running console now exists to compare against.
The measured layout disagrees with the documented one
Derived from the capture by asking which characters carry which bit - not read out of anyone's source:
| bit | members | meaning |
|---|---|---|
0x01 |
0-9 A-F a-f |
hexadecimal digit |
0x02 |
A-Z |
upper |
0x04 |
space | space |
0x08 |
!"#$%&'()*+,-./:;<=>?@[\]^_{|}~` |
punctuation |
0x10 |
a-z |
lower |
0x20 |
0-9 |
digit |
0x40 |
HT LF VT FF CR | the whitespace controls |
0x80 |
\x00-\x1f, \x7f |
control |
0x400 |
HT | tab alone |
FreeBSD's ctype.h numbers these entirely differently -
_CTYPE_A is 0x100, _CTYPE_C
0x200, _CTYPE_D 0x400.
0x400 is FreeBSD's digit bit and is the measured
table's tab bit. A transcribed table would have made
isdigit answer true for the tab and false for every digit,
and it would have done so silently, in a shader-free part of the
emulator where nothing renders wrong enough to notice.
crates/orbistoun-libc/src/ctype.rs pins this in a test that
names the trap.
The platform's C runtime is evidently not FreeBSD's libc here - the
small-bit layout, and the
_Getpctype/_Getptolower/_Getptoupper/_Mtx_init/_Xtime_get_ticks
naming, are a different lineage. That is worth knowing beyond this
table: "FreeBSD-derived kernel" does not license assuming a
FreeBSD C library, and the four-oracle list in
CLAUDE.md should be read with that split in mind.
The capture validates its own parse, and caught the first attempt
The probe records the table twice over: 34 rows of raw bytes,
and a dozen spot-checks taken through the running library
(mask_a_97 = 0x11, mask_eof_neg1 = 0x0, ...).
The generator refuses to emit a table that disagrees with them, and
three negative tests hold that guard down - a guard nobody has watched
reject something is a guard nobody knows anything about.
It rejected the first run. The record is named
table_raw_neg16 and that sixteen is bytes, not
entries: the margin below index zero is eight u16
entries, not sixteen. Read as entries it put index 0 half a table away,
and every classification would have been wrong while the file still
looked entirely reasonable. The alignment was then solved rather than
guessed - one offset satisfies all twelve spot-checks at once - and the
constant now says which unit it is in.
Shape of the change
Extraction is a generator subcommand
(orbistoun-gen ctype), beside constants, which
reads an external source and writes a crate's data/ file;
not a stand-alone script. Like constants it is deliberately
not in the tables verb, because that verb
re-derives from in-repo recordings and this reads a capture that lives
in a sibling repository. The tables land in
crates/orbistoun-libc/data/ctype.toml carrying
known_by = "measured", and the three functions allocate
them into guest memory once and answer the address of entry zero - the
middle of the allocation, since table[-1] must be
readable.
What it did not do
The wall did not move. PPSA02664 still faults at
image+0xb14be3, verdict same, six runs in six.
See worklog 297: the implementation binds (import slot 54) and is
demonstrably called - 415 times in a run, answering a real pointer - and
yet the call at the faulting site still receives
0x7fff0001. Two routes to one symbol is the leading
hypothesis and it is not yet diagnosed. This entry
claims a correct table and nothing about progress; D450 predicted the
wall would move to the tlsf allocator and it did not move at all, which
is a third thing neither entry predicted.