Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
104 changes: 104 additions & 0 deletions cli/t27b/src/lower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1478,13 +1478,29 @@ impl<'a> Lower<'a> {
}
return self.rodata(init, t);
}
let saved_sites = self.sites.len();
let v = if node.extra_type.trim().is_empty() {
self.expr(init)?
} else {
let ty = self.ty(&node.extra_type)?;
let v = self.expr_as(init, &LTy::S(ty))?;
Val::E(self.coerce(v, ty)?)
};
// `const AREA = W * H;` with a typed `H`: Zig folds the typed
// arithmetic at compile time, and a step out of range is an error.
let v = match v {
Val::E(x) if !matches!(x.kind, ExprKind::Const(_)) => match const_eval(&x, self.mode == OverflowMode::Trap) {
Some(Some(c)) => {
self.sites.truncate(saved_sites);
Val::E(Expr { kind: ExprKind::Const(c), ty: x.ty })
}
Some(None) => {
return self.reject("ConstDecl", format!("`{}` overflows {}", node.name, x.ty.name()))
}
None => Val::E(x),
},
v => v,
};
match &v {
Val::Ct(_) | Val::Cf(..) | Val::S(..) | Val::A(..) | Val::Poison => Ok(v),
Val::E(e) if matches!(e.kind, ExprKind::Const(_)) => Ok(v),
Expand Down Expand Up @@ -4601,6 +4617,8 @@ impl<'a> Lower<'a> {
Some(Val::E(Expr { kind: ExprKind::Const(c), ty })) if ty.is_int() => Some(c),
_ => None,
}
} else if let Some(e) = len_expr(len) {
self.fold_len(&e)?
} else {
None
};
Expand All @@ -4611,6 +4629,34 @@ impl<'a> Lower<'a> {
}
}

/// A length spelled as an expression (`N+1`, `(W+1)*2`): t27c prints the
/// text unchanged and Zig folds it, so it is folded here the way a
/// module constant is -- run-time names are not visible, and a typed
/// constant keeps its type. None when the value is not an integer known
/// at compile time.
fn fold_len(&mut self, e: &Node) -> R<Option<i128>> {
let saved_line = self.line;
let saved_ct = std::mem::replace(&mut self.comptime, true);
let saved_sites = self.sites.len();
let r = self.expr(e);
self.comptime = saved_ct;
// The trap sites of the folded tree are never emitted.
self.sites.truncate(saved_sites);
self.line = saved_line;
Ok(match r? {
Val::Poison => return Err(()),
Val::Ct(c) => Some(c),
Val::E(x) => match const_eval(&x, self.mode == OverflowMode::Trap) {
Some(Some(c)) => Some(c),
Some(None) => {
return self.reject("type [N]T", format!("length overflows {}", x.ty.name()))
}
None => None,
},
_ => None,
})
}

/// The fieldless, zero-size struct that stands for `void`. Its key is
/// a keyword, so no declared struct can take it.
fn void_struct(&mut self) -> u32 {
Expand Down Expand Up @@ -7360,6 +7406,64 @@ fn find_const<'n>(n: &'n Node, name: &str) -> Option<&'n Node> {
n.children.iter().find_map(|c| find_const(c, name))
}

/// An array length kept as text (`N+1`) parsed back as an expression, when it
/// is built only from integer literals, names, `+ - *` and parentheses.
/// The value of a typed integer expression built only from constants, `+ - *`
/// and widenings, as Zig evaluates it at compile time: every checked step
/// must fit its type, and `+% -% *%` wrap. None when the tree is not of that
/// shape; Some(None) when a checked step leaves its type's range (a compile
/// error in Zig). `trap` is false when `+` itself lowers to a wrapping op,
/// so a wrapping op cannot be told from a checked one and is not folded.
fn const_eval(e: &Expr, trap: bool) -> Option<Option<i128>> {
if !e.ty.is_int() {
return None;
}
let v = match &e.kind {
ExprKind::Const(c) => Some(*c),
ExprKind::Widen(x) => const_eval(x, trap)?,
ExprKind::Arith { op, lhs, rhs, .. } => {
let (x, y) = match (const_eval(lhs, trap)?, const_eval(rhs, trap)?) {
(Some(x), Some(y)) => (x, y),
_ => return Some(None),
};
match op {
ArithOp::Add => x.checked_add(y),
ArithOp::Sub => x.checked_sub(y),
ArithOp::Mul => x.checked_mul(y),
ArithOp::AddW if trap => return Some(Some(e.ty.wrap(x.wrapping_add(y)))),
ArithOp::SubW if trap => return Some(Some(e.ty.wrap(x.wrapping_sub(y)))),
ArithOp::MulW if trap => return Some(Some(e.ty.wrap(x.wrapping_mul(y)))),
_ => return None,
}
}
_ => return None,
};
Some(v.filter(|v| e.ty.fits(*v)))
}

fn len_expr(len: &str) -> Option<Node> {
if !len.contains(['+', '-', '*', '(']) {
return None;
}
let src = format!("module r {{ const r = {}; }}", len);
let ast = crate::compiler::Compiler::parse_ast_strict(&src).ok()?;
let mut e = find_const(&ast, "r")?.clone();
fn ok(n: &Node) -> bool {
match n.kind {
NodeKind::ExprLiteral => n.children.is_empty() && parse_int(&n.value).is_some(),
NodeKind::ExprIdentifier => n.children.is_empty() && !n.name.contains("::"),
NodeKind::ExprBinary => matches!(n.extra_op.as_str(), "+" | "-" | "*") && n.children.len() == 2 && n.children.iter().all(ok),
NodeKind::ExprUnary => n.extra_op == "-" && n.children.len() == 1 && ok(&n.children[0]),
_ => false,
}
}
if !ok(&e) {
return None;
}
zero_lines(&mut e);
Some(e)
}

/// Whether a repeat element parsed back from its text is one of the shapes
/// `repeat_elem` takes.
fn simple_repeat_elem(n: &Node) -> bool {
Expand Down
98 changes: 98 additions & 0 deletions cli/t27b/tests/source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2915,6 +2915,104 @@ fn only_discards(x: u32, K: u32, y: u32) -> u32 {
return y;
}

// ------------------------------------------------------------ array length expressions

/// An array length spelled as an expression (`[W+1]u8`, `[(W+1)*2]u16`) is
/// folded at compile time, as Zig folds the text t27c prints: an untyped
/// constant is a comptime_int, a typed one keeps its type.
#[test]
fn array_length_expression_folds() {
let src = "module a;

const W = 4;
const H: u32 = 3;
const AREA = W * H;

fn total(xs: [W * H]u32) -> u32 {
var s: u32 = 0;
for (xs) |x| {
s = s + x;
}
return s;
}

test ok {
var a: [W + 1]u8 = undefined;
a[0] = 1;
assert(a.len == 5);
var c: [(W + 1) * 2]u16 = undefined;
c[9] = 3;
assert(c.len == 10);
var d: [H * 2]u64 = undefined;
d[5] = 1;
assert(d.len == 6);
var b: [AREA - 2]u8 = undefined;
b[0] = 1;
assert(b.len == 10);
var r: [W * H]u32 = undefined;
for (0..r.len) |i| {
r[i] = 2;
}
assert(total(r) == 24);
}

test fails {
var a: [W - 1]u8 = undefined;
a[0] = 1;
assert(a.len == 4);
}
";
let r = run(src);
assert_eq!(names_ok(&r), vec![("ok", false, true), ("fails", false, false)]);
}

/// A length that is negative, reads a run-time value or calls a fn is
/// refused as `type [N]T`; one that overflows its typed constant is refused
/// by the fold. Zig refuses all but the call.
#[test]
fn array_length_expression_rejections() {
let cases: &[(&str, bool)] = &[
("const W = 4;\nfn f() -> u32 {\n var a: [W - 5]u8 = undefined;\n a[0] = 1;\n return 1;\n}\n", true),
("fn f() -> u32 {\n var n: u32 = 3;\n n = n + 1;\n var a: [n + 1]u8 = undefined;\n a[0] = 1;\n return 1;\n}\n", true),
("fn g(x: u32) -> u32 {\n return x;\n}\nfn f() -> u32 {\n var a: [g(2) + 1]u8 = undefined;\n a[0] = 1;\n return 1;\n}\n", true),
("const H: u8 = 200;\nfn f() -> u32 {\n var a: [H + 100]u8 = undefined;\n a[0] = 1;\n return 1;\n}\n", false),
];
for (body, len) in cases {
let m = rejected(&format!("module a;\n\n{}\ntest t {{\n assert(f() == 1);\n}}\n", body));
let want = if *len { "t27b: unsupported construct type [N]T at line" } else { "t27b: unsupported construct" };
assert!(m.starts_with(want), "{}: {}", body, m);
}
}

/// A module const built from typed constants is folded as Zig folds it at
/// compile time: a checked step out of range is refused, `+%` wraps.
#[test]
fn typed_constant_arithmetic_folds() {
let src = "module a;

const H: u32 = 3;
const A = H * 5;
const Q: u8 = 250;
const R = Q +% 10;

fn next() -> u32 {
var x: u32 = A;
x = x + 1;
return x;
}

test ok {
assert(next() == 16);
assert(R == 4);
}
";
let r = run(src);
assert_eq!(names_ok(&r), vec![("ok", false, true)]);
let m = rejected("module a;\n\nconst B: u8 = 200;\nconst OV = B + 100;\n\ntest t {\n assert(OV == 44);\n}\n");
assert!(m.starts_with("t27b: unsupported construct ConstDecl at line"), "{}", m);
assert!(m.contains("`OV` overflows u8"), "{}", m);
}

test ok {
assert(triple_first(5, 100) == 15);
assert(seven(3, 4) == 7);
Expand Down
10 changes: 10 additions & 0 deletions docs/now/2026-10-07-t27b-array-len-expr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# NOW -- t27b: an array length written as a constant expression (2026-10-07)

## `[W*H]u8` and `[N]T` where N is itself an expression (Closes #7347)

- New conformance spec `specs/tri/t27b/conformance/array_len_expr.t27`. It gives 5 pass with 0 vacuous under `t27c test-report`, and the same 5 under t27b, with 16 runtime asserts.
- t27b refused an array type whose length is not a bare literal or a bare constant name (`type [N]T`). It now folds the length at compile time: integer literals, module constants (typed or untyped, including one defined by another expression), `+ - *`, the wrapping forms, parentheses and unary minus. A length that does not fold to a non-negative integer is still refused: a division, a call, a run-time value, a negative length, or a typed constant that overflows its type. Module constants with a declared type are folded the same way, so `const R = Q +% 10` with `Q: u8 = 250` is 4, and `const OV = B + 100` with `B: u8 = 200` is refused as the reference refuses it.
- 7 mutants each fail the same tests in t27b and in the reference. Of 22 probes, 9 pass in both, 2 fail in both, and 8 are blocked by the reference and refused by t27b. The other 3 are refused by t27b while the reference passes: a length using `/`, a length using a fn call, and a 50 KB local array over the frame limit.
- Code is in `cli/t27b/src/lower.rs`. Tests are in `cli/t27b/tests/source.rs`. Both files are listed in `tools/policy/foreign-exceptions.txt` under the owner's approval on #6063.
- Lab corpus at --jobs 2 against master: t27b pass where the reference passes 818 -> 819, mismatch 0. Ledger `docs/reports/t27b_expectations.json`: the new spec moves to pass. `routes/memory.t27` now stops at `FnDecl(frame size)`.
Refs #6063. Closes #7347.
93 changes: 93 additions & 0 deletions specs/tri/t27b/conformance/array_len_expr.t27
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// SPDX-License-Identifier: Apache-2.0
; specs/tri/t27b/conformance/array_len_expr.t27 -- an array type whose length is a compile-time
; expression, `[N + 1]T`, as the reference runs it (gHashTag/t27#6063 lane: type [N]T).
; Every test here passes `t27c test-report`; t27b must give the same verdict on each, or refuse the file.
; t27c's Zig backend prints the length text unchanged, so Zig folds it: module constants, literals,
; `+ - *` and parentheses. The fold follows Zig: an untyped constant is a comptime_int, a typed one
; keeps its type (so `H * 2` with `H: u32` is a u32 and must not overflow it).
; Not here: a length that calls a fn, reads a run-time value, or goes negative; Zig refuses the last two.
; phi^2 + 1/phi^2 = 3 | TRINITY

module T27bConformanceArrayLenExpr;

const W = 4;
const H: u32 = 3;
const AREA = W * H;

const Grid = struct {
cells: [W * H]u32,
pad: [W + 2]u8,
};

fn ramp() -> [W * H]u32 {
var a: [W * H]u32 = undefined;
var i: u32 = 0;
while (i < AREA) {
a[i] = i * 2;
i = i + 1;
}
return a;
}

fn total(xs: [W * H]u32) -> u32 {
var s: u32 = 0;
for (xs) |x| {
s = s + x;
}
return s;
}

fn first_last(xs: [AREA - 2]u8) -> u8 {
return xs[0] + xs[xs.len - 1];
}

test length_is_the_folded_sum {
var a: [W + 1]u8 = undefined;
assert(a.len == 5);
for (0..a.len) |i| {
a[i] = 7;
}
assert(a[0] == 7);
assert(a[4] == 7);
}

test product_length_as_result_and_parameter {
const r = ramp();
assert(r.len == 12);
assert(r[11] == 22);
assert(total(r) == 132);
}

test length_from_a_constant_that_is_itself_an_expression {
var b: [AREA - 2]u8 = undefined;
assert(b.len == 10);
var i: usize = 0;
while (i < b.len) {
b[i] = 1;
i = i + 1;
}
b[0] = 3;
b[9] = 4;
assert(first_last(b) == 7);
}

test parenthesised_and_typed_lengths {
var c: [(W + 1) * 2]u16 = undefined;
assert(c.len == 10);
var d: [H * 2]u64 = undefined;
assert(d.len == 6);
d[5] = 9;
c[9] = 8;
assert(d[5] == 9);
assert(c[9] == 8);
}

test lengths_inside_a_struct {
var g: Grid = undefined;
g.cells = ramp();
g.pad[5] = 2;
assert(g.pad.len == 6);
assert(g.cells.len == 12);
assert(g.cells[3] == 6);
assert(g.pad[5] == 2);
}
6 changes: 6 additions & 0 deletions tools/policy/foreign-exceptions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,12 @@ bootstrap/stage0/FROZEN_HASH
cli/t27b/src/lower.rs
cli/t27b/tests/source.rs

# owner 2026-10-06, epic #6063 (label owner-approved-foreign on #6063; "add the label yourself and do the
# work", translated): t27b coverage lane 1, issue #7347 -- an array length that is a compile-time expression
# (`[N + 1]T`) is folded as Zig folds it. Conformance spec first: specs/tri/t27b/conformance/array_len_expr.t27.
cli/t27b/src/lower.rs
cli/t27b/tests/source.rs

# owner 2026-10-06, label owner-approved-foreign on epic #6063 and on issues #7175 / #7179 ("add the label
# yourself and do the work", translated): t27b coverage lane 2 -- a fn no test reaches may name a struct
# t27b cannot lay out (Zig never resolves its signature), and `x as T` with a float operand spelled as one
Expand Down
Loading