Repository navigation
t27b: run-time 128-bit integers (type i128 at run time in phi_f64_literals, eval_arith and source_text) #7915
Description
Part of #6063 (t27b coverage), item C10 of #6488. Port side: #6198.
What phi_f64_literals.t27 needs
specs/port/bootstrap/src/phi_f64_literals.t27 passes the reference (8 of 8 tests). t27b refuses it with type i128 at line 52 (fn normalise_rhs(rhs: str, scope: str) -> i128), and then with ExprCall(rejected fn) at its two calls. The fn returns one of three literals at run time (7286977268806824, -52, 0), and the tests compare the result with ==. The spec also declares struct Expectation { want: i128, ... }, which nothing uses.
That is the only gap. With i128 replaced by i64 in a copy of the file, t27b passes 8/8 (t27c lab, master 76836e2).
Why t27b refuses it
#7657 (specs/tri/t27b/wide_plan.t27) folds integers wider than 64 bits only while they stay compile-time values. Here the value crosses a call and lives at run time. t27b's IR has no integer wider than 64 bits.
What a fix needs
This needs an IR type, not glue: a 128-bit value held in two words (a register pair, or 16 bytes of memory, align 16), with:
- a literal split into its two words, and copies, parameters and returns of the pair;
==,!=and the ordered comparisons (the high word signed, the low word unsigned);asto and from 64-bit types, with Zig's checks;- arithmetic with Zig's overflow traps, as t27 routines in the style of
specs/tri/t27b/libm.t27, or refused by name until those exist.
Under the owner's rule this is new t27b logic. The plan and the routines belong in .t27, and the IR and layout changes are more than a coverage lane's glue budget (#7371: 40 lines per file, 80 per PR). So t27b keeps refusing the file as type i128 until this lands. One spec in the corpus needs it today.
User Scenarios
- Given
specs/port/bootstrap/src/phi_f64_literals.t27declaresfn normalise_rhs(rhs: str, scope: str) -> i128and returns run-time i128 literals (7286977268806824, -52, 0) that are compared with==, When t27b compiles the spec, Then it must accepti128as a run-time type and produce working comparisons without "type i128" rejection. - Given t27b's IR currently has no integer wider than 64 bits, When support for run-time i128 is implemented, Then i128 values must be represented as a 128-bit pair (two 64-bit words) in registers or 16-byte aligned memory, with correct signed high-word and unsigned low-word semantics for comparisons.
Requirements
FR-001: t27b MUST add an IR type for 128-bit integers represented as two 64-bit words (register pair or 16-byte aligned memory).
FR-002: t27b MUST support i128 literals split into high/low words, and copies, parameters, and returns of the i128 pair.
FR-003: t27b MUST implement ==, !=, <, <=, >, >= for i128 with signed high-word and unsigned low-word comparison semantics.
FR-004: t27b MUST implement as casts between i128 and 64-bit integer types with Zig's overflow checks.
FR-005: t27b MUST provide arithmetic operations (add, sub, mul, div, rem) for i128 as t27 routines in specs/tri/t27b/libm.t27 style with Zig's overflow traps, or refuse by name until routines exist.
FR-006: The implementation MUST reside in .t27 spec files per the owner's rule; IR and layout changes go in the t27b compiler.
Success Criteria
t27b specs/port/bootstrap/src/phi_f64_literals.t27compiles without "type i128" or "ExprCall(rejected fn)" errors andexit 0t27b specs/port/bootstrap/src/phi_f64_literals.t27 --runexecutes all 8 tests andexit 0specs/tri/t27b/wide_plan.t27is updated to document run-time i128 representation and operationsexit 0specs/tri/t27b/libm.t27contains i128 arithmetic routines with overflow trapsexit 0
Boundary
- specs/port/bootstrap/src/phi_f64_literals.t27
- specs/tri/t27b/wide_plan.t27
- specs/tri/t27b/libm.t27
- scripts/tri_loop/t27b.py
Activity
gHashTag commentedon Oct 10, 2026
Measured on the t27c lab at master dae6b17, with t27c and t27b built from it and zig 0.16.0. This comment widens #7915 to the whole
type i128cluster of the t27b ledger: three specs share one gap.1. What the three specs do with i128 at run time
The reference passes all three under
t27c test-report(8/8, 12/12 and 16/16). t27b refuses each one first attype i128.-> i128normalise_rhs(line 52)digit_value,radix,parse_int(lines 63, 72, 83)pow2,ty_min,ty_max,wrap,mag,shift_amount,ev_value: i128ok,fits,wrap,checked,mag,mul_too_large,shift_amount,arith,ev_trap,ev_value. Inarith(op, bits, signed, boolean, a, b), AAPCS64 puts a in x4:x5 and b in x6:x7const result = normalise_rhs(..)const base,const d,const x(line 166),var vassigned byv = v * base + dconst m,const ma,const width,const amtExpectation.want(line 14; the struct is never used)EvalArith.value: built in a literal, returned by value, read as.valueNO_INT(-1),I128_MAX(2^127-1),U32_MAX,U64_MAX(2^64-1)ONE,ZERO,U64_MAX(2^64-1)ev_value(.., 18446744073709551615, ..)== 7286977268806824and== -52==!===with a literal==and!===<<=>>=>=,>and<=+v * base + da + b-I128_MAX - dZERO - x,pow2(n) - ONE,m - pow2(n),a - b,width - ONE*v * basea * b*%a *% b, which wraps at 128 bits for (2^64-1)^2@divTrunc@divTrunc(I128_MAX - d, base): a 127-bit dividend over a run-time divisor of 2, 8, 10 or 16@divTrunc(U64_MAX, ma), and@divTrunc(a, b)with negative operands@rem@rem(a, b), including@rem(-2^63, -1)&|^<<>>ONE << n,a << nanda >> n. gen-zig prints<< @intCast(n)with n a u32, so the amount is a checked cast to u7@intCast(c - 48), u8 to i128 (always fits)@intCast(bits), u32 to i128 (always fits)amt as u32, which gen-zig prints as@as(u32, @intCast(amt)), a checked castNone of the three uses:
-or~on an i128;+%or-%;/or%on an i128 (Zig refuses both, see gen (Zig): / and % between i128 operands emit plain / and %, which Zig refuses #8066);as i128(the parser refuses it, see parser:as i128andas u128are refused as cast targets #8067);Covered today: nothing at run time.
specs/tri/t27b/wide_plan.t27(#7657, see #7423) folds integers wider than 64 bits only while they stay compile-time values. That covers literals,const X: i128 = .., operators between those, comparisons, andasback to a type of at most 64 bits. It refuses a wide value placed beside a run-time one, which is source_text's refusal at line 296 (I128_MAXbeside a call).2. Zig 0.16 Debug semantics, measured on the lab (x86_64-linux, the reference host)
@sizeOf(i128)is 16 and@alignOf(i128)is 16.struct { t: u8, v: i128 }is 32 bytes, align 16.+,-and*with a result outside i128 panic withinteger overflow.@divTrunc(x, 0)and@rem(x, 0)panic withdivision by zero.@divTrunc(minInt, -1)panics withinteger overflow.@rem(minInt, -1)returns 0 and does not panic.<<drops the bits shifted out and does not panic (3 << 127is minInt).>>is arithmetic (-7 >> 1is -4).<< @intCast(n)with n = 128, and@as(u32, @intCast(x))with x = 2^32, panic withinteger does not fit in destination type.3. Minimal repro
unsupported construct type i128 at line 3, thenExprCall(rejected fn) at line 11.4. Design note: why this stops at the measurement
There is no PR this round. Even the smallest honest slice (the repro and phi_f64_literals: a return, a literal and
==) must first decide how an i128 exists at run time. That is a decision about the backend's register model, not a coverage lane:Ty(cli/t27b/src/ir.rs) is a scalar that fits in one register. codegen.rs keeps every scalar in one X, W or D register (temps in x9-x15, homes in x19-x28, arguments in x0-x7), and keeps aggregates in frame memory behind a Ptr.Ty::min,Ty::maxandTy::wrapshift bybits, which overflows at 128.+can need 129 bits.There are two designs, and both are exact.
A. A register pair in codegen. A new
Ty::I128holds lo and hi in two X registers. This is the design the requested instruction sequences need:adds/adcsandsubs/sbcs, with the V flag of the high half as the overflow trap;mul/umulhon the halves plus the cross terms, with the overflow check on the 256-bit product;cmpplussbcs(orccmp);__udivmodti4, spliced in the way libm.t27 is.A touches every place codegen moves a value: temps and spills, variable homes, AAPCS64 calls (an i128 takes an even-numbered pair, x0:x1 to x6:x7, and returns in x0:x1), loads and stores (ldp/stp), and Select, Widen and Cast. eval_arith.t27 must also learn 128-bit overflow without 129-bit arithmetic. a64.t27 already has
adds,subs,umulh,smulh,ldpandstp;adcs,sbcsandccmpwould be added there.Estimate for A, in hand lines: ir.rs ~25, codegen.rs ~300-450, lower.rs ~60-100, eval.rs ~20, tests ~40. That is 450-650 in total.
B. A 16-byte value in memory, decided in lowering. The value is two u64 words with Zig's size and alignment, held like a struct. Codegen, the IR and the interpreter do not change. Every operation becomes a call of a t27 routine spliced in under t27b's reserved prefix (
__t27b_i128_add,_mul,_divtrunc,_cmp, ...). Each routine returns a status, and the lowering turns that status into a trap at the operator's own site and line.Estimate for B: lower.rs ~110-150 hand lines (the type, materializing literals and constants, dispatching operators, compares, casts, shifts,
@divTruncand@rem, the trap check, and the splice) plus tests ~20-30. The routines (~350 lines) and the plan are t27.B has these costs:
umulh: t27 has no high multiply, so a 64x64 product is built from 32-bit halves;Which to pick: B is cheaper for these three specs. A makes i128 a first-class type: fast, exported under AAPCS64, and checked independently by the interpreter.
Either design needs more than the ~150 hand lines a lane may carry. Under the net <= 0 rule (
check_budget()in specs/policy/own_language.t27, see #8236), the same PR must also port an equal amount of lower.rs and codegen.rs into plans. The body of #7915 already refused this as more than a lane's glue budget (#7371), and the net <= 0 rule is stricter. So the choice between A and B goes to the owner first.5. Order once a design is chosen
==and!=, and an untyped const of a call.varand assignment;@intCastfrom u8 to i128; ordered compares; checked+,-and*; and@divTrunc. This needs the 128-bit division routine in t27, with_expect_trapcases for a zero divisor and for minInt / -1.*%;@rem;&,|and^;<<and>>with the checked u7 amount; and the checked cast to u32.Each slice ships its conformance spec under specs/tri/t27b/conformance/ and
_expect_trapcases for each new trap, and sets its ledger row to pass.[-]t27b: run-time 128-bit integers (i128 returned and compared at run time, specs/port/bootstrap/src/phi_f64_literals.t27)[/-][+]t27b: run-time 128-bit integers (type i128 at run time in phi_f64_literals, eval_arith and source_text)[/+]gHashTag commentedon Oct 10, 2026
Owner decision (2026-10-10): option A, register pair.
Stages: phi_f64_literals, then source_text, then eval_arith. A pure-port PR comes first if one is needed to make room.
t27b: run-time i128, stage 1 -- a result in x0:x1, literals and compa…
t27b: run-time i128, stage 2 -- annotated locals, pair constants, @in…
t27b: run-time i128 stage 3 -- parameters in register pairs, i128 str…
gHashTag commentedon Oct 11, 2026
All three stages of the run-time i128 (option A, the register pair) are on master:
specs/tri/t27b/eval_arith.t27passest27b test --check: 13/13, 180 runtime asserts. The t27b ledger'smax_not_passis 0. At the merge commit cd88ab1, the t27b-native ratchet reports no unexpected failure; it is red only for 2 unlisted specs from other PRs. Closing.