Skip to content

[TRACK_B] Route the last pure builtins — three defects, a false doc promise, and the first conditional prelude - #75

Draft
ib823 wants to merge 4 commits into
mainfrom
claude/continue-solution-4gn31y
Draft

[TRACK_B] Route the last pure builtins — three defects, a false doc promise, and the first conditional prelude#75
ib823 wants to merge 4 commits into
mainfrom
claude/continue-solution-4gn31y

Conversation

@ib823

@ib823 ib823 commented Aug 21, 2026

Copy link
Copy Markdown
Owner

REQ-70. Every builtin family is now routed: 341 of 373 compile, up from 323.

I described these 26 as unexamined backlog. That framing was wrong, and so was my earlier claim that the remaining interpreter-only set each had a stated reason — only 24 of 50 did. Examining the other 26 found three defects rather than missing implementations.

Three defects, not a backlog

A language feature did not compile. adalah_kiri/adalah_kanan/nilai_kiri/nilai_kanan are not stdlib functions a user calls — they are compiler internals. The parser's if-chain pattern compiler emits them for a constructor pattern nested where a Case cannot go, such as (Ada(a), Tiada) inside a tuple pattern. Unrouted, that entire class of pattern ran under riinac run and failed riinac build with Codegen Error: unbound variable: nilai_kiri. Four obscure interp-only rows were in fact one missing feature in the compiled backend.

baki/rem were uncallable from any well-typed program. They are binary and take a pair — the interpreter's extract_pair_ints and the emitted C's RIINA_TAG_PAIR check both say so — but the typechecker declared them Nombor -> Nombor, because they shared a registration loop with the genuinely unary log2. So baki(10, 3) failed with "Expected function type, found Int" and baki((10, 3)) failed with "expected Int, found Prod(Int, Int)". Two of three components agreed; the type was the outlier, so the type is what changed.

cetak_baris was a pure aliasing gap. The interpreter binds cetakln, println and cetak_baris to one Value::Builtin("cetakln"). Only builtin_canonical knew two of the three.

baki, log2 and rawak already had C sitting unreferenced in emit.rs, exactly like the json helpers before them.

The Backend column was making a false promise

interp-only reads "riinac run only". For the eight crypto-agility builtins (guna_kripto/use_crypto, pilih_algo/select_algorithm, cipher/sifer, hash_dengan/hash_with) that was false: they sit in the typechecker registry with Fn(Teks, Any, Kripto) and carry the REQ-48 deprecation check at their call sites, but no runtime binds them. Verified by command for all eight — riinac run fails with unbound variable exactly as riinac build does.

A three-state column had no way to say "typed but unimplemented", so it said the nearest thing. Added a fourth state typed-only, derived from a new riina_codegen::interpreter_supports_builtin that builds the real interpreter environment and looks the name up — not from a list, which would drift the moment a runtime is added.

The Unicode three needed the emitter's first conditional prelude block

nfc, skeleton and adalah_keliru need ~250 KB of vendored UCD tables. The prelude is already ~228 KB for a hello-world and is otherwise entirely unconditional, so emitting the tables always would more than double every compiled binary to serve three builtins most programs never call. emit() now scans the IR and emits the block only on a call.

Measured: hello-world stays 229,600 bytes with zero tables; a program calling nfc gets 492,515.

The C tables are generated from the same Rust statics the interpreter reads, not transcribed. A hand-copied 250 KB table is a drift source no differential could realistically cover; generating it means the backends cannot disagree about the data at all — only about the ~150-line algorithm, which is what the tests exercise.

A negative control caught a test passing for the wrong reason

The composition-blocking case was first written as a + dot-below + acute. Deleting the blocking condition from the emitted C did not make it fail — there both marks compose into the starter in turn, so blocking never applies.

Rewritten as a + U+0305 overline + U+0301 acute: equal combining classes, and the overline does not compose, so removing the check composes a+acute across the overline and shortens the result from five bytes to four. Re-ran the control; the test then failed as intended. The reasoning is recorded in the test's own comment so the input is not "simplified" back later.

keselamatan_nama.rii now compiles

One of the four Gate C sample apps, and the only one that would not build. Verified by command, with output byte-identical to riinac run.

What remains, and why

Every remaining interp-only builtin has a stated reason:

Group Count Reason
jaring_tls_* / net_tls_* 16 Awaiting the Law 8 owner decision
VirtualFs trio 6 Needs an in-memory FS + quota accounting in C
csrf_generate 2 Non-deterministic; mirroring a known-substandard generator doubles the eventual fix

The 8 typed-only crypto-agility builtins need a runtime, not routing — separate work, and not REQ-70's to close.

Tests

  • pure_builtin_differential.rs — 6 cases. The load-bearing one is the nested constructor pattern, ordered so a failing tag test precedes a succeeding one, plus a separate case asserting the bound payload matches (a nilai_kiri returning the sum rather than its contents would still pick the right arm and print the wrong number).
  • unicode_differential.rs — 7 cases, chosen where the algorithm decides rather than the data: Hangul (arithmetic, in no table), stable canonical ordering, the blocking rule above, and that skeleton ends in NFD rather than NFC. Plus hello_world_carries_no_ucd_tables, which keeps the conditional-emission promise honest — without it the block would silently become unconditional again and nothing else would notice.
  • rawak/random is routed despite the backends being uncomparable by output (both time-seeded). The differential pins the range invariant and says plainly that equality is not checked. Routing is still right: without it a compiled RIINA program has no source of randomness at all. Neither implementation is a CSPRNG and neither claims to be.

Verification

  • 03_PROTO: 3373 passing, 0 failed (+13)
  • 05_TOOLING: 323 passing, 0 failed
  • cargo clippy -- -D warnings: clean on both workspaces
  • scripts/audit-docs.sh: 0 discrepancies (2 pre-existing warnings, unrelated)
  • docs/api/STDLIB.md regenerated from the compiler — counts re-derived, not carried forward

🤖 Generated with Claude Code

https://claude.ai/code/session_01Uu28z8CdRQ1SLzTv8yszth


Generated by Claude Code

claude added 4 commits August 21, 2026 02:03
…cts they hid

REQ-70. I described the remaining interpreter-only set as a backlog of builtins
nobody had examined. Examining them found that most were not missing
implementations at all -- they were defects that the Backend column happened to
render as "interp-only".

1. A LANGUAGE FEATURE DID NOT COMPILE. adalah_kiri/adalah_kanan/nilai_kiri/
   nilai_kanan are not stdlib functions a user calls; they are COMPILER
   INTERNALS. The parser's if-chain pattern compiler emits them for a
   constructor pattern nested where a `Case` cannot go -- `(Ada(a), Tiada)`
   inside a tuple pattern. Unrouted, that entire class of pattern ran under
   `riinac run` and failed `riinac build` with
   `Codegen Error: unbound variable: nilai_kiri`. Four obscure interp-only rows
   were in fact one missing feature in the compiled backend.

2. baki/rem WERE UNCALLABLE FROM ANY WELL-TYPED PROGRAM. They are binary and
   take a pair -- the interpreter's extract_pair_ints and the emitted C's
   RIINA_TAG_PAIR check both say so -- but the typechecker declared them
   `Nombor -> Nombor`, because they shared a registration loop with the
   genuinely unary log2. So `baki(10, 3)` failed with "Expected function type,
   found Int" and `baki((10, 3))` failed with "expected Int, found
   Prod(Int, Int)". Two of three components agreed; the type was the outlier,
   so the type is what changed.

3. cetak_baris WAS A PURE ALIASING GAP. The interpreter binds cetakln, println
   AND cetak_baris to one Value::Builtin("cetakln"). Only builtin_canonical knew
   two of the three, so a program using the third ran and then failed to build.

baki, log2 and rawak already HAD C implementations sitting unreferenced in
emit.rs, exactly like the json helpers before them. Only the gate was missing.

ROUTED (12 names): the four sum helpers, baki/rem, log2, julat/julat_inklusif,
rawak/random, cetak_baris. New C for the ranges and the sum helpers; the ranges
mirror the interpreter's SATURATING inclusive increment, and the sum payload
projections are deliberately lenient on a non-sum argument as the interpreter is.

rawak/random is routed despite the two backends being uncomparable by output --
both are time-seeded. The differential pins the range invariant instead and says
plainly that equality is not being checked. Routing is still right: without it a
compiled RIINA program has no source of randomness at all. Neither
implementation is a CSPRNG and neither claims to be.

THE BACKEND COLUMN WAS MAKING A FALSE PROMISE, now fixed. Its `interp-only` cell
reads "`riinac run` only". For the eight crypto-agility builtins
(guna_kripto/use_crypto, pilih_algo/select_algorithm, cipher/sifer,
hash_dengan/hash_with) that was FALSE: they are in the typechecker registry with
`Fn(Teks, Any, Kripto)` and carry the REQ-48 deprecation check at their call
sites, but NO runtime binds them, so `riinac run` fails with `unbound variable`
exactly as `riinac build` does. Verified by command for all eight. A three-state
column had no way to say "typed but unimplemented", so it said the nearest thing,
which was wrong. Added a fourth state `typed-only`, derived from a new
`riina_codegen::interpreter_supports_builtin` that builds the real interpreter
environment and looks the name up -- not from a list, which would drift the
moment a runtime is added.

Counts re-derived from the regenerated doc: 373 registered -- compiled 21 /
native-only 314 / interp-only 30 / typed-only 8. The remaining 30 are the TLS
half (16), the VirtualFs trio (6), csrf_generate (2) and the Unicode NFC and
confusables builtins (6); the first three are excluded for stated reasons, and
the Unicode six are the next increment.

Tests: pure_builtin_differential.rs (6 cases). The load-bearing one is the
nested constructor pattern, ordered so a failing tag test precedes a succeeding
one, plus a separate case asserting the BOUND PAYLOAD matches -- a nilai_kiri
returning the sum rather than its contents would still pick the right arm and
print the wrong number.

Verified: 03_PROTO 3366/0 (+6), clippy clean on both workspaces, audit-docs.sh
0 discrepancies.
… conditional prelude

REQ-70. Routes nfc/ke_nfc (UAX #15), skeleton/rangka and
adalah_keliru/is_confusable (UTS #39) -- the last group with no stated reason to
stay interpreter-only. 341 of 373 builtins now compile.

The Gate C sample app 07_EXAMPLES/03_applications/keselamatan_nama.rii NOW
COMPILES. It was one of four apps the gate cites as shipping evidence and the
only one that would not build; verified by command, and its compiled output is
byte-identical to `riinac run`.

WHY THIS NEEDED A CONDITIONAL BLOCK. Every other builtin's C is a few dozen
lines. These need ~250 KB of vendored UCD tables. The emitted prelude is already
~228 KB for a hello-world and is otherwise entirely unconditional, so emitting
the tables always would MORE THAN DOUBLE every compiled binary to serve three
builtins most programs never call. emit() now scans the IR for a call to one of
the three and emits the block only then. Measured: hello-world stays 229,600
bytes with zero tables; a program calling nfc gets 492,515.

THE TABLES ARE GENERATED, NOT TRANSCRIBED. The C arrays are written out from the
same unicode_nfc_data / unicode_confusables_data statics the interpreter reads.
A hand-copied 250 KB table is a drift source no differential could realistically
cover; generating it means the two backends cannot disagree about the DATA at
all -- only about the ALGORITHM, which is ~150 lines and is what the tests
actually exercise.

A NEGATIVE CONTROL CAUGHT A TEST THAT PASSED FOR THE WRONG REASON. The blocking
case was first written as `a` + dot-below + acute, on the reasoning that the
acute cannot reach the starter past a lower-class mark. Deleting the blocking
condition from the emitted C did NOT make that test fail: there both marks
compose into the starter in turn, so blocking never applies. Rewritten as
`a` + U+0305 overline + U+0301 acute -- equal combining classes, and the
overline does not compose -- where removing the check composes `a`+acute across
the overline and shortens the result from five bytes to four. Re-ran the control:
the test now fails as intended. Recorded in the test's own comment so the input
is not "simplified" back later.

Cases chosen where the ALGORITHM decides rather than the data: Hangul (composed
and decomposed arithmetically, so it appears in no table -- a lookup-only port
leaves it decomposed); stable canonical ordering; the blocking rule above; and
that skeleton ends in NFD, not NFC, because it is a comparison key rather than a
display form. Plus hello_world_carries_no_ucd_tables, which keeps the
conditional-emission promise honest -- without it the block would silently
become unconditional again and nothing else would notice, the binaries would
just get bigger.

Every remaining interp-only builtin now has a stated reason: the TLS half (16,
awaiting the Law 8 decision), the VirtualFs trio (6, needs an in-memory FS and
quota in C), and csrf_generate (2, non-deterministic). Counts re-derived from
the regenerated doc: compiled 21 / native-only 320 / interp-only 24 /
typed-only 8.

Verified: 03_PROTO 3373/0 (+7), 05_TOOLING 323/0, clippy clean on both,
audit-docs.sh 0 discrepancies.
…found

Updates the REQ-70 row and the Part 12 Wave 1.0 status. Counts re-derived from
the regenerated docs/api/STDLIB.md: 373 registered, 341 compile -- compiled 21 /
native-only 320 / interp-only 24 / typed-only 8.

Corrects my own framing. I listed these 26 as unexamined backlog and told the
owner the remaining interpreter-only set each had a stated reason. Both were
wrong: only 24 of the 50 did, and examining the other 26 found three defects
rather than missing implementations -- a language feature (nested constructor
patterns) that did not compile, a pair-taking builtin declared unary and so
uncallable from any well-typed program, and an aliasing gap.

Also records that the Backend column was making a false promise for eight
crypto-agility builtins, now fixed with a fourth `typed-only` state derived from
the real interpreter environment; that the Unicode three needed the emitter's
first conditional prelude block, with the measured sizes; and that
keselamatan_nama.rii -- one of four Gate C sample apps, and the only one that
would not build -- now compiles.

Generalises the wave's recurring finding one step further. It already said a
family marked as lowering is not a family that agrees. Two cases in this wave
went further: they PASSED while the property they named was absent from the code.
json's nine differential cases all fed well-formed input to a parser that could
not fail, and the Unicode composition-blocking case chose an input where blocking
never applies. Both were found by deliberately breaking the implementation and
checking whether the test noticed. A green differential is evidence only about
the inputs it actually runs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants