Skip to content

t27o: Zig front end step 2 -- --check on .zig, sweep lists .zig, imported consts, std.math.pow (Closes #10823) - #10918

Merged
gHashTag merged 5 commits into
masterfrom
t27o/zig-step2-10823
Oct 19, 2026
Merged

gHashTag merged 5 commits into
masterfrom
t27o/zig-step2-10823

Conversation

@gHashTag

Copy link
Copy Markdown
Owner

Closes #10823. Step 2 of the Zig front end (#10597, option B of #10522), lane Z2.

Stacked on #10819 (branch t27o/zig-front-10765, held as a draft until #10817 merges): this PR targets master, so its diff shows step 1's commits too. The five commits of this PR are 4e7d54d50 .. 4c311b570. Order: #10817, then #10819 (re-seal zig_convert.t27), then this one, rebased and re-sealed.

What changed, per item

  1. tri t27o <file>.zig --check / --oracle. --check writes the converted spec to specs/t27o_check/<stem>.t27 and judges it as t27o --check: t27b test judges first, t27c test-report only when t27b cannot (Closes #10525) #10538 judges Rust specs: t27b test first, t27c test-report only when t27b's exit code is no verdict (t27b_outcome, needs_reference reused, not copied). --oracle prints one line (the file's own tests are the oracle) and exits 0. A spec none of whose tests converted passes t27b test as 0 of 0, which is no verdict (tri test: a spec whose every test passed vacuously still passes -- report.t27 must read the "vacuous passes N of M" line #10497): has_evidence refuses it, exit 6 (EXIT_UNJUDGED, outside t27b's table 0..5). ledger.t27 gains front_end (the extension rule tri t27o and the sweep both ask), has_evidence, the two notes; zig_convert.t27 loses is_zig_name (one rule, not two).
  2. tri t27o-sweep lists .zig. sweep_path takes .zig under the same rule as .rs (nothing under gen/ or target/). A Zig row's converter key is the Zig seal's spec_hash joined to the Rust converter's (zig_convert.t27 uses rust_convert.t27's helpers): editing the Zig seal made 346 of 1105 trios rows go cold and 759 stay reused. zig_convert.t27 numbers its five counters like rust_convert.t27's report, so the sweep reads one set of slots for both front ends (the output of all 2573 real .zig files was byte-identical before and after the renumbering; two tests pin the slots). Found on the way: the cache row separates kinds with ; and the Zig reason "generated; convert its source spec instead" was cut in two and counted as its second half; kind_to now stops at ; as well as a backtick.
  3. Imported consts. pub const C = @import("x.zig").C; is written as pub const C: u32 = 5; when x.zig is in the importing file's own directory tree (no .., no leading /, no \, no :; a symlink out of the tree is not followed) and its pub const C is a scalar literal (integer, float, bool, a string with no escape, at most 64 bytes, under a scalar type or none). Two pure fns in zig_convert.t27 (imports names the files, inline_imports writes the values); the glue only reads files. No line is added or taken, so every "line N" of a refusal stays true. Anything else is left as it was and stays refused with its reason ("uses external C"): a struct, a fn, a non-pub const, an expression, a non-scalar type, a declaration over several lines, a missing file. The sweep keys a Zig row by the source with the values written in (an imported literal changed: the importing row is stale; a comment in the imported file changed: it is reused, checked on a two-file repo).
  4. std.math.pow(f64, x, y). Converted, bit for bit Zig's, through a new specs/tools/t27o/zig_std.t27 (beside zig_convert.t27 as rust_std.t27 is beside rust_convert.t27). Zig 0.16's pow is Go's math.Pow, not libm's: the significand of x is squared (frexp), the exponent counted apart, exp(yf * log x) taken for the fraction, one ldexp at the end. Plain repeated multiplication is NOT Zig's result (69 of the 191 integer powers among the recorded vectors differ; libm's pow differs too), so zig_std.t27 holds the algorithm step for step, in operations t27b and t27c both have (@exp/@log are compiler_rt's, which t27b runs bit for bit). Zig's ldexp is ported as it is, not as IEEE: it tests an exact tie by @ctz(repr), which counts the exponent field's own zeros, so 0.5 * 2^-1074 becomes 2^-1074 where musl's scalbn gives 0. My first version used musl's scalbn and the recorded vectors caught it. zig_convert.t27 writes std_math_pow_f64(x, y) and a use tools::t27o::zig_std::std_math_pow_f64; after the header when a fn or test that stayed in the output calls it. Other types and the old two-operand form are refused as "pow of a type other than f64"; a call in a file-level const (t27b cannot evaluate it) as "pow in a file-level const".

Evidence

  • Vectors recorded with zig run (Zig 0.16.0, Debug, no libc; the program is in a comment in zig_std.t27): 433 pow, 124 ldexp, 16 frexp, compared by bits under t27b and t27c test-report. The lane first ran 1325 pow vectors (specials, the constants the corpus raises, results that overflow, underflow or land in the subnormals, random pairs): all equal under t27b and t27c. Mutants of the port (the 0.5 test of the fraction, the +1 of yi, the sign of ae, the 2^63 threshold, the tie rule, the denormal exponent, the 1/sqrt branch, the 2^53 limit, the overflow bound) are each killed.
  • Tests: t27b test zig_convert 61/61, ledger 38/38, zig_std 7/7; t27c test-report once at the end: vacuous 0 of 61, 0 of 38, 0 of 7 (a slot test of mine compared constants, which Zig folds: it was vacuous, fixed in 4c311b570). All three seals --verify MATCH; dupes --name and dupe_scan --like find nothing written elsewhere; devhome OK.
  • own-language budget from the merge base: rc=0, main.rs +31 / -31 (this PR alone +28 / -28).

Measured (trinity 0770abb99 src/, clean working tree, so the sweep reads HEAD; trios 483dd9ee crates/)

step 1 this PR
converted fns, tri t27o on each of the 2573 .zig files 1837 2022
translated tests converted 1766 1887
specs with translated tests (t27b): pass / fail (tests) / blocked 588 / 9 (17) / 7 586 / 9 (19) / 9
tests passed under t27b 1725 1830

(The lane review quoted 1,839 and 590 of 606; this run's own baseline is the first column.) tri t27o-sweep now lists 2562 Zig rows (346 trios, 2216 trinity; 11 files over the 300 KB cap are skipped, as for Rust): 30215 fn seen, 1803 -> 1987 converted at the sweep, tests 1733 -> 1852 of 14795. Wall time, load average about 37 on the bench Mac, minimum of three: trios cold 4.2 s, warm 0.7 s; trinity cold 8.6 s, warm 1.7 s (quiet-machine runs earlier in the lane: 2.6 s and 5.1 s, before items 3 and 4). --check on five real files: src/gravity/quantum_gravity_full.zig 26/26, src/cosmos/flatness_problem_solution.zig 23/23 (calls the pow port), src/origin/abiogenesis.zig 24/24, src/tri/tri_vacuum.zig 17/17, src/time/temporal_constants.zig 11/11, all exit 0; src/tri/constants.zig (no tests) exit 6.

Imported consts found little: 23 imports of a scalar literal resolve in the two trees, 12 of them in two files that convert now (src/tri/constants.zig, src/tri/error.zig, consts only: no new fn converts), 11 in files refused whole as generated. pow was the lever: 52 specs call the port, 45 pass under t27b.

The pass/fail/blocked movement

  • 5 pow specs fail tests that fail under zig test too (phi_lerp, baryon_formulas in both repositories; spectrum: zig test panics on @intFromFloat, as the t27 test does). They are new in the failing column.
  • compute_lucas.zig (both repositories) moves from failing to blocked: its newly converted test holds @as(f64, l_n) with l_n: u64, which Zig 0.16 refuses ("expected type 'f64', found 'u64'"), t27b too. I tried to make the converter refuse it and had to revert: @as(f32, N_LEVELS - 1) with a comptime-known typed const is valid Zig and converts today (5 fns would be lost), and the class tracker does not tell a comptime-known usize from a run-time one.

Not done

  • FR-006 / the last box of t27o: Zig front end step 2 -- std.math.pow, imported consts, --check on .zig, sweep lists .zig (from step 1 measurements) #10823, reporting the failing specs to their repositories, is not done: filing issues in gHashTag/trinity and gHashTag/trios is a post to other repositories and needs the owner's word. The nine specs (six distinct files), for whoever files them: src/brain/benchmarks.zig (percentile_calculation, benchmark_accuracy_percentile_interpolation), src/kaggle/csv_parser.zig (detect_open_ended_vs_factual), src/phi-engine/cache/phi_lerp.zig (t^PHI_INV: 0.5^(1/phi) = 0.6517, the test expects 0.653 within 0.001), src/sacred/baryon_formulas.zig (baryon_photon_ratio_order_of_magnitude, neutron_proton_ratio), src/string_theory/spectrum.zig (bosonic critical dimension is 26, superstring critical dimension is 10), src/vibeec/golden_chain.zig (six export-constant tests); trios carries copies of brain_benchmarks, phi_lerp and baryon_formulas. brain/benchmarks.zig, kaggle/csv_parser.zig and vibeec/golden_chain.zig do not compile under Zig 0.16 at all (std.time.milliTimestamp, std.fs.cwd, missing modules): they may build with the Zig version those repositories use.
  • No record in ledger.t27 of those findings (the issue asked for it); a ledger of upstream findings is data, not a rule, and I left it for the owner's decision on where it lives.

Traps (for the next lane)

  • A hex literal that ends in f32 or f64 is a float literal to t27b ("1137417019072311 is not exactly an f32"): the vector tables write those entries in decimal.
  • gen-rust writes a slice of a local array as &mut &ps[..], which does not compile; t27b rejects an array passed as a slice. Scratch for imports/inline_imports lives in the state vector the glue passes.
  • A word of a message or an identifier that names a rust_convert declaration splices the module: a parameter called path took zig_convert.rs from 126 KB to 269 KB ([bug] t27c gen-rust: the use splice matches words inside string literals and pulls in a whole module (13,432 vs 4,111 lines) #10824).
  • @bitCast of a literal is refused: bind it to a typed const first.
  • An assert that compares constants is folded by Zig and counts as 0 runtime asserts: vacuous.
  • git add refuses gen/ (ignored): git add -u, and -f for a new file.

🤖 Generated with Claude Code

@gHashTag

Copy link
Copy Markdown
Owner Author

Review (orchestrator, Opus) -- stays a draft until its chain lands (#10883 -> #10819 -> this):

@github-actions

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-10-19 06:59:54 UTC

Summary

Status Count
Total Open PRs 50
PRs with Failing Checks 44
PRs with All Checks Green 6
READY 2
FAILING 44
PENDING 0
NO CHECKS YET 0

These columns do not partition: 2 + 44 + 0 + 0 = 46, and there are 50 open PRs. A PR is being counted twice or not at all.

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=88b8cb73ceda != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

This was referenced Oct 19, 2026
claude and others added 5 commits October 19, 2026 15:14
…racle says it does not apply (Closes #10823)

--check on a .zig file used to run the Rust oracle and fail with "rustc
refused the oracle harness". It now writes the converted spec to
specs/t27o_check/<stem>.t27 and judges it as #10538 judges Rust specs
(t27b test first, t27c test-report only when t27b's exit code is no verdict;
t27b_outcome and needs_reference are reused, not copied). --oracle on a .zig
file prints one line, that the file's own tests are the oracle, and exits 0.

A spec with no converted test passes t27b test as 0 of 0, which is no
verdict (#10497): has_evidence refuses it, exit 6 (EXIT_UNJUDGED, outside
t27b's table). A Rust file keeps its oracle and is unchanged.

ledger.t27 gains front_end (the extension rule `tri t27o` and the sweep both
ask) and has_evidence, with tests and negative controls. zig_convert.t27
loses is_zig_name, which front_end replaces (one rule, not two). main.rs:
+15 / -17 against the merge base (the own-language budget holds; rc=0).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…_convert.t27's seal too (Closes #10823)

sweep_path (ledger.t27) takes a .zig file under the same rule as a .rs one
(nothing under gen/ or target/). The glue picks the front end by extension
through ledger.t27's front_end, the same rule `tri t27o file.zig` asks.

A Zig row's converter key is the Zig seal's spec_hash joined to the Rust
converter's (zig_convert.t27 uses rust_convert.t27's helpers), so a change to
either makes every Zig row stale and no Rust row: checked by editing the Zig
seal, 346 of 1105 trios rows went cold, 759 were reused.

zig_convert.t27 numbers its five counters like rust_convert.t27's report
(NFN, NOK, NTS, NTEST, NSTUB), so the sweep reads one set of slots for either
front end; the output of all 2573 real .zig files is byte-identical to
before the renumbering. Two tests pin the slots.

kind_to now stops at a semicolon as well as a backtick: the cache row
separates kinds with ';', and "generated; convert its source spec instead"
was cut in two and counted as its second half. It is now one kind,
"generated", with its own count.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…scalar literal when x.zig is in the same tree (Closes #10823)

`pub const C = @import("x.zig").C;` made C another file's, so every use of
C was refused ("uses external"). zig_convert.t27 now has two pure fns:
`imports` lists the paths to read (each once), and `inline_imports` writes
`pub const C: u32 = 5;` in place of the import when the imported file holds a
`pub const C` that is a scalar literal (integer, float, bool, a string with no
escape, at most 64 bytes, under a scalar type or none). The glue (zig_source
in main.rs) only reads the files, from the importing file's own directory
tree: a path with `..`, a leading `/`, a backslash or a colon is never listed,
and a symlink out of the tree is not followed.

Anything else is left exactly as it was and stays refused with its reason: a
struct, a fn, a const that is not pub, an expression, a non-scalar type, an
escaped string, a literal over 64 bytes, a declaration over several lines
(no line is added or taken, so every "line N" of a refusal stays true), a
missing file. Tests for each, negative controls for all of them, and the
bound (output <= 4 x source + 64 bytes) the glue sizes its buffer by.

The sweep keys a Zig row by the source with the values written in, so
changing an imported literal makes the importing file's row stale and
changing a comment in the imported file does not (checked on a two-file repo).

Measured on trinity 0770abb99 src/ and trios 483dd9ee crates/: 23 imports of
a scalar literal resolve in the tree; 12 are in two files that convert now
(src/tri/constants.zig, src/tri/error.zig, consts only: no new fn converts),
11 are in files refused whole as generated. The other 2571 outputs are
byte-identical to before. main.rs: +31 / -31 against the merge base.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…new zig_std.t27 (Closes #10823)

103 refusals, the largest left by step 1, were `std.math.pow`. t27 can
produce Zig's result, so it does instead of keeping the refusal.

Zig 0.16's pow is not libm's: it is Go's math.Pow, a port that squares the
significand of x (frexp), counts the exponent apart, takes exp(yf * log x) for
the fraction and scales once with ldexp. Plain repeated multiplication is not
Zig's result (it differs on 69 of the 191 integer powers among the recorded
vectors; libm's pow differs too), so zig_std.t27 (new, like rust_std.t27
beside rust_convert.t27) holds that algorithm step for step in operations
t27b and t27c both have: @exp and @log (compiler_rt's, which t27b runs bit for
bit), @sqrt, @floor, and Zig's ldexp ported as it is. The port is Zig's code
and not the IEEE rule: Zig's ldexp tests an exact tie by `@ctz(repr)`, which
counts the exponent field's own zeros, so 0.5 * 2^-1074 becomes 2^-1074 where
musl's scalbn (and IEEE) gives 0. The first version used musl's scalbn and
the recorded vectors caught it.

Evidence: 433 pow vectors, 124 ldexp and 16 frexp vectors recorded with
`zig run` (Zig 0.16.0, Debug, no libc) and tested by their bits under t27b
AND t27c (the whole 1325-vector set of the lane passed under t27b and t27c
before it was cut to these). Mutants of the port (the 0.5 test of the
fraction, the +1 of yi, the sign of ae, the 2^63 threshold, the tie rule,
the denormal exponent, the 1 / sqrt(x) branch, the 2^53 odd-integer limit,
the overflow bound) are each killed.

zig_convert.t27 writes `std.math.pow(f64, x, y)` as std_math_pow_f64(x, y)
and a `use tools::t27o::zig_std::std_math_pow_f64;` after the header when a
fn or test that stayed in the output calls it. Other types and the old
two-operand form are refused as their own kind, "pow of a type other than
f64"; a call in a file-level const (t27b cannot evaluate it; Zig does at
compile time) as "pow in a file-level const". Tests and negative controls
for each, and the use line.

Measured on trinity 0770abb99 src/ and trios 483dd9ee crates/: converted fns
1837 -> 2022, translated tests 1766 -> 1887; 52 specs call the port, 45 pass
under t27b, 5 fail the tests that fail under `zig test` too (phi_lerp,
baryon_formulas, spectrum, in both repositories), 2 are not pow's (an
`@as(f64, u64)` that Zig 0.16 refuses as well).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…n time, vacuous passes 0 of 61 (Closes #10823)

The test that pins the five counters to five different slots compared
constants, which Zig folds at compile time: `t27c test-report` counted it as
a pass with 0 runtime asserts. It now writes a different number to each slot
and reads each back, so two slots that collide fail it at run time.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@gHashTag
gHashTag enabled auto-merge (squash) October 19, 2026 08:24
@gHashTag

Copy link
Copy Markdown
Owner Author

#10819 merged, so the hold is released: rebased onto master (5 commits); the Zig specs, gen files and seals are byte-identical to the reviewed head 4c311b5; t27b test zig_convert 61/61, ledger 38/38, zig_std 7/7, all seals MATCH; own-language budget from the merge base rc=0; cargo check -p tri clean; devhome OK. tools/dupe_scan.py red groups (at_text, code_at, emit_lds, ...) are on master too, from other specs -- inherited; this PR adds none. The reachability check's red is master's, fixed by #10950. Marked ready; auto-merge on.

@github-actions

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-10-19 08:48:08 UTC

Summary

Status Count
Total Open PRs 50
PRs with Failing Checks 46
PRs with All Checks Green 4
READY 1
FAILING 46
PENDING 0
NO CHECKS YET 0

These columns do not partition: 1 + 46 + 0 + 0 = 47, and there are 50 open PRs. A PR is being counted twice or not at all.

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=509db03c8f4d != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

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.

t27o: Zig front end step 2 -- std.math.pow, imported consts, --check on .zig, sweep lists .zig (from step 1 measurements)

2 participants