Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7aa92f6
t27b: @intCast as an array index or a slice bound takes Zig's usize, …
claude Oct 8, 2026
d659b89
Merge master 76836e23e into t27b-int-cast-index-7902 (Closes #7902)
claude Oct 8, 2026
c15cbc5
Merge master e87e790ac into t27b-int-cast-index-7902 (Closes #7902)
claude Oct 8, 2026
fae3459
Merge master 430c52293 into t27b-int-cast-index-7902 (Closes #7902)
claude Oct 8, 2026
f8f4e98
Merge master 408d740d9 into t27b-int-cast-index-7902 (Closes #7902)
claude Oct 8, 2026
a1ac9c6
Merge master e66473f6b into t27b-int-cast-index-7902 (Closes #7902)
claude Oct 8, 2026
de80f41
Merge master 42707603a into t27b-int-cast-index-7902 (Closes #7902)
claude Oct 9, 2026
dbeb876
Merge master 2dfe9786f into t27b-int-cast-index-7902 (Closes #7902)
claude Oct 9, 2026
bbbb71d
Merge master 6b6386e15 into t27b-int-cast-index-7902 (Closes #7902)
claude Oct 9, 2026
133605c
Merge master 09ffc2fc5 into t27b-int-cast-index-7902 (Closes #7902)
claude Oct 9, 2026
838900e
Merge master 12ee225c5 into t27b-int-cast-index-7902 (Closes #7902)
claude Oct 9, 2026
40b425d
Merge master 6cd8fba33 into t27b-int-cast-index-7902 (Closes #7902)
claude Oct 9, 2026
64bc44f
Merge master 35e69962d into t27b-int-cast-index-7902 (Closes #7902)
claude Oct 9, 2026
fe60a6a
Merge master a97cb9e55 into t27b-int-cast-index-7902 (Closes #7902)
claude Oct 9, 2026
f7db212
Merge master f21a0b39c into t27b-int-cast-index-7902; can_widen_from …
claude Oct 9, 2026
8298f50
Merge master cde548ecd into t27b-int-cast-index-7902; no hand lines a…
claude Oct 9, 2026
87b8441
Merge master b9d22d0f4 into t27b-int-cast-index-7902; no hand lines a…
claude Oct 9, 2026
9c10b0f
Merge master 3ac8bef87 into t27b-int-cast-index-7902; no hand lines a…
claude Oct 9, 2026
d212dcd
Merge master 325b0b18a into t27b-int-cast-index-7902; no hand lines a…
claude Oct 9, 2026
1787535
Merge master ab5b3b7e1 into t27b-int-cast-index-7902; no hand lines a…
claude Oct 9, 2026
2026681
Merge master 54f3c140c into t27b-int-cast-index-7902; no hand lines a…
claude Oct 9, 2026
cd4cc67
Merge master e4338f46e into t27b-int-cast-index-7902; no hand lines a…
claude Oct 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .trinity/seals/conformance_T27bConformanceIntCastIndex.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"built_by": "t27c-bootstrap@0.5.2+cde548ecd",
"closure": [],
"config": "gen=c,rust,verilog,zig;tests=zig",
"gen_hash_c": "sha256:60642816624bf552bec7ca967bcfb0958dfdb353171fcf3a34fa22f327c47e1e",
"gen_hash_rust": "sha256:619c7d51384efc07f073f40582bbeed02ff69a9a5a254e9b4cd29cfec83044c6",
"gen_hash_verilog": "sha256:13a03a5713417efd5dc1d15a4bfec6eabe20c79b07f747da3f8cf07153738d54",
"gen_hash_zig": "sha256:23fc05be622874b1f2cd5e983099bd3be5b54237fab57f982f8ebc7877677681",
"module": "T27bConformanceIntCastIndex",
"ring": 12,
"seal_schema": 2,
"sealed_at": "2026-10-09T18:41:07Z",
"sealed_by": "t27c-bootstrap@0.5.2",
"spec_hash": "sha256:bfa085968b1359f848a7dd99847fd79408dd4e4e8f17d53f35ac0a13b7a9d2b5",
"spec_path": "specs/tri/t27b/conformance/int_cast_index.t27",
"tests": {
"failed": 0,
"failing": [],
"forced": false,
"passed": 5,
"total": 5
},
"toolchain": {
"test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d",
"zig": "0.16.0"
}
}
18 changes: 9 additions & 9 deletions .trinity/seals/t27b_T27bIntCastPlan.json
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
{
"built_by": "t27c-bootstrap@0.5.2+5d5d2cdef",
"built_by": "t27c-bootstrap@0.5.2+cde548ecd",
"closure": [],
"config": "gen=c,rust,verilog,zig;tests=zig",
"gen_hash_c": "sha256:fdb4387fd78a898156787cf3273db8dce4541a3e3ed8be38af45eba0fdf2f732",
"gen_hash_rust": "sha256:ac15c1a1652af4e7c4e6cf5c264219de80ad841e4bdca02e56457b843bfd1fe4",
"gen_hash_verilog": "sha256:40343b26cdb9e4a84eb6513f9030d363cfdc6a2186a292fa4ce7adfcbcca7084",
"gen_hash_zig": "sha256:4aa662bb5fcd6c0e1480325d2db3b1f4fcc375bb9c7628e3d49e02a0aa197488",
"gen_hash_c": "sha256:09c6e3b6ccfcd6c1a7752092d1357f5e4e3dc6436080b2676e8f603faac6efbd",
"gen_hash_rust": "sha256:642539740526856f355e411dbe5e184169baee3c51dbab5a3e7c9cfd2c9daa09",
"gen_hash_verilog": "sha256:09b576d747474bf84a25a9ee909a1de79a2487aa81ba5c102fa7a990edc6759d",
"gen_hash_zig": "sha256:36e12e2f8de71f8c0e730a73e4e4ff6e820ab6a7c0cb4454e9ecfe8f78d8cb10",
"module": "T27bIntCastPlan",
"ring": 12,
"seal_schema": 2,
"sealed_at": "2026-10-09T14:58:43Z",
"sealed_at": "2026-10-09T18:41:07Z",
"sealed_by": "t27c-bootstrap@0.5.2",
"spec_hash": "sha256:4de93edab47dc680f5c6ba63a21b777330f0f7cdc673cf751ecb25ece1913152",
"spec_hash": "sha256:cda4ab42328d2355117d71a545fb1b57fa8b5b63cdb25b5b2a5434cf1290184d",
"spec_path": "specs/tri/t27b/int_cast_plan.t27",
"tests": {
"failed": 0,
"failing": [],
"forced": false,
"passed": 11,
"total": 11
"passed": 12,
"total": 12
},
"toolchain": {
"test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d",
Expand Down
6 changes: 5 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,11 @@ measure it are written in **t27**, not in Rust or Python by hand.
after #8347 moved the node scans of `lower.rs` (`calls_in`, `decls_of`,
`ref_mutable_names`, `misprinted_ifs`, `mark_tail_returns` and four more)
to `specs/tri/t27b/ast_scan.t27`, -91, 0 and 0 on master 54f3c140c's 14601
plus 1222 and 8284),
plus 1222 and 8284; 14505 plus 1222, 15727 in all, and tests 8284, after
#7902's glue for `@intCast` as an index or a slice bound
(`specs/tri/t27b/int_cast_plan.t27`), with `Ty::can_widen_from` in `ir.rs`
now `int_cast_plan.t27`'s `widens`, -5 net, 0 and 0 on master e4338f46e's
14510 plus 1222 and 8284),
`scripts/tri_loop/t27b.py` 1829
(Python, `tri t27b`; 894 at c532fcae5, 1481 after #6317's `next`, 1568 after #6334's master look-back, 1829 after #6445's `reduce` wiring), `scripts/tri_loop/t27b_reduce.py` 632 (Python, `tri t27b reduce`, #6445; its decisions are `specs/tri/t27b/reduce.t27`), and `contrib/railway/t27b-lab/lab.py` 608 (Python, the
Railway lab; 956 on master 0fbb0a033, 986 after #7672's lane requests). Update these numbers in the PR that moves them.
Expand Down
14 changes: 2 additions & 12 deletions cli/t27b/src/ir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,20 +204,10 @@ impl Ty {
}

/// `self` can hold every value of `from`: the lossless implicit widening
/// the subset allows (Zig's rule: same signedness and not narrower, or
/// the subset allows (`widens` of specs/tri/t27b/int_cast_plan.t27: same signedness and not narrower, or
/// unsigned into a strictly wider signed type).
pub fn can_widen_from(self, from: Ty) -> bool {
if self == from {
return true;
}
if !self.is_int() || !from.is_int() {
return false;
}
if self.signed() == from.signed() {
self.bits() >= from.bits()
} else {
self.signed() && self.bits() > from.bits()
}
self == from || self.is_int() && from.is_int() && crate::lower::ic::widens(from.bits(), from.signed(), self.bits(), self.signed())
}
}

Expand Down
13 changes: 9 additions & 4 deletions cli/t27b/src/lower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ mod float;
#[path = "../../../gen/rust/tri/t27b/builtin_plan.rs"] #[allow(dead_code, unused_parens)]
mod bp; // t27c gen-rust of specs/tri/t27b/builtin_plan.t27: @abs, @max, @min, std.math.pi / e
#[path = "../../../gen/rust/tri/t27b/int_cast_plan.rs"] #[allow(dead_code, unused_parens)]
mod ic; // t27c gen-rust of specs/tri/t27b/int_cast_plan.t27: @intCast with an integer result type
pub(crate) mod ic; // t27c gen-rust of specs/tri/t27b/int_cast_plan.t27: @intCast with an integer result type; `widens` serves ir.rs
#[path = "../../../gen/rust/tri/t27b/ptr_eq_plan.rs"] #[allow(dead_code, unused_parens)]
mod pe; // t27c gen-rust of specs/tri/t27b/ptr_eq_plan.t27: `==` / `!=` of pointers and optional pointers
#[path = "../../../gen/rust/tri/t27b/libm_plan.rs"] #[allow(dead_code, unused_parens)]
Expand Down Expand Up @@ -3782,6 +3782,11 @@ impl<'a> Lower<'a> {
Ok(Val::E(Expr { ty, kind }))
}

/// An index or a slice bound, whose result type in Zig is `ic::INDEX_TYPE`: an `@intCast` there converts to it.
fn usize_operand(&mut self, n: &Node) -> R<Val> {
if n.kind == NodeKind::ExprCall && n.name == ic::SITE { self.int_cast(n, Ty::from_name(ic::INDEX_TYPE).unwrap_or(Ty::U64)) } else { self.expr(n) }
}

/// `n` folded as Zig folds an expression that names a module constant of an integer type wider than 64 bits:
/// integer literals, such constants and the operators between them (specs/tri/t27b/wide_plan.t27).
fn wide(&mut self, n: &Node) -> wp::WideFold {
Expand Down Expand Up @@ -6552,7 +6557,7 @@ impl<'a> Lower<'a> {
(op, _) => return self.reject(&format!("ExprIndex({})", op), "unexpected shape".into()),
}
let base = self.expr(&n.children[0])?;
let idx = self.expr(&n.children[1])?;
let idx = self.usize_operand(&n.children[1])?;
if base.is_poison() || idx.is_poison() {
return Err(());
}
Expand Down Expand Up @@ -6722,7 +6727,7 @@ impl<'a> Lower<'a> {
None
};
// S1: the start.
let i = self.expr(start)?;
let i = self.usize_operand(start)?;
let i = self.coerce(i, Ty::U64)?;
let ci = if let ExprKind::Const(c) = i.kind { Some(c) } else { None };
stmts.push(Stmt::Store { addr: slot_expr(scr), off: 0, value: i });
Expand All @@ -6734,7 +6739,7 @@ impl<'a> Lower<'a> {
// S2: the end, checked against the length, becomes the length.
let mut cj = alen.map(|l| l as i128);
if let Some(end) = end {
let j = self.expr(end)?;
let j = self.usize_operand(end)?;
let j = self.coerce(j, Ty::U64)?;
cj = if let ExprKind::Const(c) = j.kind { Some(c) } else { None };
if let (Some(c), Some(l)) = (cj, alen) {
Expand Down
11 changes: 11 additions & 0 deletions cli/t27b/tests/int_cast_traps.t27
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,20 @@ fn ict_pass_byte(x: u64) -> u8 {
return ict_byte(@intCast(x));
}

// An index is the result location, a usize (#7902): a negative i64 stops at the cast, before the bounds check.
fn ict_index(k: i64) -> u8 {
const xs: [4]u8 = [1, 2, 3, 4];
return xs[@intCast(k)];
}

test ict_values_that_fit_expect_pass {
assert(ict_narrow(65535) == 65535);
assert(ict_to_i32(-2147483648) == -2147483648);
assert(ict_to_u64(9223372036854775807) == 9223372036854775807);
assert(ict_to_i64(9223372036854775807) == 9223372036854775807);
assert(ict_to_u32(2147483647) == 2147483647);
assert(ict_pass_byte(255) == 255);
assert(ict_index(3) == 4);
}

test ict_unsigned_narrowing_expect_trap {
Expand Down Expand Up @@ -79,3 +86,7 @@ test ict_negative_to_u32_expect_trap {
test ict_through_a_parameter_expect_trap {
assert(ict_pass_byte(256) == 0);
}

test ict_negative_index_expect_trap {
assert(ict_index(-1) == 4);
}
2 changes: 1 addition & 1 deletion cli/t27b/tests/tail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ fn int_cast_traps_outside_its_result_type() {
let at = o.err().map(|(k, l)| (k, src.lines().nth(l as usize - 1).is_some_and(|s| s.contains("@intCast"))));
assert_eq!(at, name.ends_with("_expect_trap").then_some((TrapKind::Cast, true)), "{}", name);
}
assert_eq!(r.iter().filter(|t| t.2.is_err()).count(), 7);
assert_eq!(r.iter().filter(|t| t.2.is_err()).count(), 8);
let lit = "module a;\n\nfn id(x: u64) -> u64 {\n return x;\n}\n\ntest t {\n const b: u8 = @intCast(300);\n assert(b == 44);\n}\n";
assert!(rejected(lit).contains("ExprCall(@intCast) at line 8 (`@intCast` of 300 to u8: the literal does not fit"));
assert!(rejected(&lit.replace("b: u8 = @intCast(300)", "b = @intCast(id(3))")).contains("ExprCall(@intCast) at line 8"));
Expand Down
5 changes: 3 additions & 2 deletions docs/reports/t27b_expectations.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"reference-bug": "the reference passes it for the wrong reason; t27b is right not to",
"n/a": "outside what t27b is for (say why in the PR that sets it)"
},
"max_not_pass": 19,
"max_not_pass": 18,
"entries": [
{"path": "specs/a/b_c.t27", "t27b": "pass_vacuous"},
{"path": "specs/a_b/c.t27", "t27b": "pass_vacuous"},
Expand Down Expand Up @@ -407,7 +407,7 @@
{"path": "specs/numeric/gft8.t27", "t27b": "pass"},
{"path": "specs/numeric/gfternary.t27", "t27b": "pass"},
{"path": "specs/numeric/golden_sieve.t27", "t27b": "pass"},
{"path": "specs/numeric/ocp_mx.t27", "t27b": "blocked", "blocker": "ExprCall(@intCast)", "reason": "unimplemented"},
{"path": "specs/numeric/ocp_mx.t27", "t27b": "pass"},
{"path": "specs/numeric/requant_boundary.t27", "t27b": "pass_vacuous"},
{"path": "specs/numeric/tnf1024.t27", "t27b": "pass"},
{"path": "specs/numeric/tnf128.t27", "t27b": "pass"},
Expand Down Expand Up @@ -1097,6 +1097,7 @@
{"path": "specs/tri/t27b/conformance/frame_address_return.t27", "t27b": "blocked", "blocker": "ExprReturn(frame address)", "reason": "reference-bug"},
{"path": "specs/tri/t27b/conformance/frame_address_store.t27", "t27b": "blocked", "blocker": "StmtAssign(frame address)", "reason": "reference-bug"},
{"path": "specs/tri/t27b/conformance/int_cast.t27", "t27b": "pass"},
{"path": "specs/tri/t27b/conformance/int_cast_index.t27", "t27b": "pass"},
{"path": "specs/tri/t27b/conformance/large_frame.t27", "t27b": "pass"},
{"path": "specs/tri/t27b/conformance/lazy_signature.t27", "t27b": "pass"},
{"path": "specs/tri/t27b/conformance/libm_logf.t27", "t27b": "pass"},
Expand Down
8 changes: 7 additions & 1 deletion gen/rust/tri/t27b/int_cast_plan.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Generated from .t27 spec
// DO NOT EDIT — generated by t27c
// NOT LOWERED BY THIS BACKEND: 11 test(s), 0 invariant(s), 0 function(s) with no body.
// NOT LOWERED BY THIS BACKEND: 12 test(s), 0 invariant(s), 0 function(s) with no body.
// This backend emits declarations only. The spec's checks live in
// the Zig and Verilog outputs; do not read this file as verified.

Expand Down Expand Up @@ -36,6 +36,12 @@ pub const WHAT: &'static str = "ExprCall(@intCast)";

pub const SITE: &'static str = "@intCast";

pub const INDEX_TYPE: &'static str = "usize";

pub const INDEX_BITS: u32 = 64;

pub const INDEX_SIGNED: bool = false;

pub fn shape(operands: usize, result_is_int: bool) -> u8 {
if (operands != 1) {
return REFUSE_ARITY;
Expand Down
129 changes: 129 additions & 0 deletions specs/tri/t27b/conformance/int_cast_index.t27
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// SPDX-License-Identifier: Apache-2.0
; specs/tri/t27b/conformance/int_cast_index.t27 -- `@intCast(x)` as an array index or a slice bound, as the
; reference runs it (issue #7902, t27b coverage epic #6063: ExprCall(@intCast) in specs/numeric/ocp_mx.t27).
; Every test here passes `t27c test-report`; t27b must give the same verdict on each, or refuse the file.
; gen-zig prints `a[@intCast(k)]` as written. Zig gives an index, and each bound of a slice, the result type
; `usize`, so the cast converts k to a usize exactly as `@as(usize, @intCast(k))` would: an i64 or an i32 is
; checked (a negative one is a safety panic in Debug, which is a failed test), a u8 or a u32 widens, a usize
; is kept, and a literal folds. The index is then checked against the length as any index is. The trap cases
; are in cli/t27b/tests/int_cast_traps.t27, since a conformance test must pass. What t27b decides is
; specs/tri/t27b/int_cast_plan.t27 (INDEX_TYPE).
; Not here: `@intCast` as an arithmetic operand (`x + @intCast(k)`), which has no result type; Zig refuses it.
; phi^2 + 1/phi^2 = 3 | TRINITY

module T27bConformanceIntCastIndex;

const ICX_POW2 : [8]i64 = [1, 2, 4, 8, 16, 32, 64, 128];
const ICX_BITS : [3]i64 = [4, 6, 8];
const ICX_BYTES : [6]u8 = [10, 20, 30, 40, 50, 60];

// ocp_mx's shape: a signed i64 index, converted to a usize.
fn icx_pow2(e: i64) -> i64 {
return ICX_POW2[@intCast(e)];
}

// An index read from one table indexes another; the cast's operand is arithmetic.
fn icx_half(kind: i64) -> i64 {
return ICX_POW2[@intCast(ICX_BITS[@intCast(kind)] - 1)];
}

// Two indexed reads under the operators of ocp_mx's `units`.
fn icx_units(e: i64, m: i64, mant_bits: i64) -> i64 {
if (e == 0) { return m; }
return (ICX_POW2[@intCast(mant_bits)] + m) * ICX_POW2[@intCast(e - 1)];
}

fn icx_by_u8(k: u8) -> i64 {
return ICX_POW2[@intCast(k)];
}

fn icx_by_u32(k: u32) -> i64 {
return ICX_POW2[@intCast(k)];
}

fn icx_by_usize(k: usize) -> i64 {
return ICX_POW2[@intCast(k)];
}

fn icx_by_i32(k: i32) -> i64 {
return ICX_POW2[@intCast(k)];
}

// An array parameter, indexed by an i64 counter: ocp_mx's block loops.
fn icx_sum(vals: [4]i64, n: i64) -> i64 {
var s : i64 = 0;
var i : i64 = 0;
while (i < n) {
s = s + vals[@intCast(i)];
i = i + 1;
}
return s;
}

// A write through a cast index.
fn icx_fill(n: i64) -> i64 {
var buf : [4]i64 = [0, 0, 0, 0];
var i : i64 = 0;
while (i < n) {
buf[@intCast(i)] = (i + 1) * 10;
i = i + 1;
}
return buf[0] + buf[1] + buf[2] + buf[3];
}

// Both bounds of a slice cast from i64.
fn icx_window_sum(a: i64, b: i64) -> u32 {
const w : []const u8 = ICX_BYTES[@intCast(a)..@intCast(b)];
var s : u32 = 0;
for x in w {
s = s + x;
}
return s;
}

fn icx_window_len(a: i64, b: i64) -> usize {
const w : []const u8 = ICX_BYTES[@intCast(a)..@intCast(b)];
return w.len;
}

test icx_a_signed_index_reads_its_element {
assert(icx_pow2(0) == 1);
assert(icx_pow2(3) == 8);
assert(icx_pow2(7) == 128);
assert(icx_half(0) == 8);
assert(icx_half(1) == 32);
assert(icx_half(2) == 128);
}

test icx_operators_around_cast_indexes {
assert(icx_units(0, 5, 1) == 5);
assert(icx_units(1, 1, 1) == 3);
assert(icx_units(3, 2, 2) == 24);
assert(icx_units(4, 0, 3) == 64);
}

test icx_unsigned_and_narrow_indexes {
assert(icx_by_u8(2) == 4);
assert(icx_by_u8(7) == 128);
assert(icx_by_u32(5) == 32);
assert(icx_by_usize(6) == 64);
assert(icx_by_i32(1) == 2);
assert(icx_by_i32(4) == 16);
}

test icx_loops_over_an_array_parameter {
const v : [4]i64 = [3, -4, 5, 100];
assert(icx_sum(v, 0) == 0);
assert(icx_sum(v, 3) == 4);
assert(icx_sum(v, 4) == 104);
assert(icx_fill(2) == 30);
assert(icx_fill(4) == 100);
}

test icx_slice_bounds_cast_from_i64 {
assert(icx_window_len(1, 4) == 3);
assert(icx_window_len(2, 2) == 0);
assert(icx_window_sum(0, 6) == 210);
assert(icx_window_sum(1, 3) == 50);
assert(icx_window_sum(5, 6) == 60);
}
Loading
Loading