Skip to content
Merged
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
54 changes: 39 additions & 15 deletions cli/t27b/src/lower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1946,7 +1946,15 @@ impl<'a> Lower<'a> {
fn call_stmt(&mut self, c: &Node, out: &mut Vec<Stmt>) -> R<()> {
self.see(c);
match c.name.as_str() {
"assert" => {
// `@compileAssert` is not a separate construct in the reference:
// t27c's Zig backend lowers it through the very arm that lowers
// `assert` (`node.name == "@compileAssert" || node.name ==
// "assert"`), so both give the same `if (!(cond))` check. Inside
// an `invariant` that check sits in a `comptime` block and a false
// condition fails the reference's compile; here it fails the
// invariant when run. Either way the file does not pass, and the
// operands are held to the same comptime rules (`self.comptime`).
"assert" | "@compileAssert" => {
// `assert(cond, "msg")`: t27c's Zig backend emits
// `if (!(cond)) @panic("msg")` and never evaluates the
// message, so the verdict is the one-argument assert's. Only a
Expand Down Expand Up @@ -2457,6 +2465,14 @@ impl<'a> Lower<'a> {
/// `@intCast`, and traps when the value is outside `to`. In Wrap mode every
/// narrowing truncates, as a C cast does.
fn cast(&mut self, v: Val, to: Ty) -> R<Val> {
// `x as f64` with an integer `x`: t27c's Zig backend emits
// `@as(f64, @floatFromInt(x))` (its `is_float_expr` says the operand
// is not a float), so this is exactly `@floatFromInt` with result
// type f64. A float operand (`@floatCast`) and f64 to an integer
// (`@intFromFloat`) stay refused below.
if to == Ty::F64 && (matches!(v, Val::Ct(_)) || matches!(&v, Val::E(e) if e.ty.is_int())) {
return self.int_to_f64(v, "ExprCast(f64)");
}
if to == Ty::F64 || matches!(v, Val::Cf(..) | Val::E(Expr { ty: Ty::F64, .. })) {
if v.is_poison() {
return Err(());
Expand Down Expand Up @@ -3759,20 +3775,7 @@ impl<'a> Lower<'a> {
return Err(());
}
if to_float {
let e = match v {
Val::Ct(c) => return Ok(Val::E(self.coerce(Val::Ct(c), Ty::F64)?)),
Val::E(e) if e.ty.is_int() => e,
v => {
let d = self.val_desc(&v);
let d = match &v { Val::E(e) => e.ty.name().to_string(), _ => d };
return self.reject(&what, format!("operand is {}, not an integer", d));
}
};
if let ExprKind::Const(c) = e.kind {
// i128 to f64 rounds to nearest, ties to even, as SCVTF does.
return Ok(Val::E(Expr { ty: Ty::F64, kind: ExprKind::Const(f64_bits(c as f64)) }));
}
return Ok(Val::E(Expr { ty: Ty::F64, kind: ExprKind::IntToFloat(Box::new(e)) }));
return self.int_to_f64(v, &what);
}
let e = match v {
v @ Val::Cf(_, true) => self.coerce(v, Ty::F64)?,
Expand All @@ -3795,6 +3798,27 @@ impl<'a> Lower<'a> {
Ok(Val::E(Expr { ty, kind: ExprKind::FloatToInt { arg: Box::new(e), site } }))
}

/// `@floatFromInt(v)` with result type f64: the integer operand `v`
/// converted, rounding to nearest. `what` names the construct in a
/// rejection.
fn int_to_f64(&mut self, v: Val, what: &str) -> R<Val> {
let e = match v {
Val::Poison => return Err(()),
Val::Ct(c) => return Ok(Val::E(self.coerce(Val::Ct(c), Ty::F64)?)),
Val::E(e) if e.ty.is_int() => e,
v => {
let d = self.val_desc(&v);
let d = match &v { Val::E(e) => e.ty.name().to_string(), _ => d };
return self.reject(what, format!("operand is {}, not an integer", d));
}
};
if let ExprKind::Const(c) = e.kind {
// i128 to f64 rounds to nearest, ties to even, as SCVTF does.
return Ok(Val::E(Expr { ty: Ty::F64, kind: ExprKind::Const(f64_bits(c as f64)) }));
}
Ok(Val::E(Expr { ty: Ty::F64, kind: ExprKind::IntToFloat(Box::new(e)) }))
}

/// A slice of type `t` (a `Slice` or `Str`) of all `len` elements at
/// `ptr`, as a fresh temporary.
fn slice_of(&mut self, ptr: Expr, len: u32, t: LTy) -> R<Val> {
Expand Down
7 changes: 5 additions & 2 deletions cli/t27b/tests/differential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2188,7 +2188,8 @@ test nan_to_int {
}

/// What stays refused, each named: `@sqrt` and `std.math.*`, a conversion
/// with no result type, f32, `as` to or from f64, compile-time arithmetic
/// with no result type, f32, `as` from f64 or from a bool to f64 (an
/// integer `as f64` is `@floatFromInt`, see `source.rs`), compile-time arithmetic
/// on a literal that is not exactly an f64 (Zig folds it in f128), and
/// `x * 2^k` on f64 (t27c gen rewrites it into a shift that cannot compile).
#[test]
Expand All @@ -2205,7 +2206,9 @@ fn f64_refusals_name_the_construct() {
("return std.math.sqrt(x);", "ExprCall(std.*)"),
("return @floatFromInt(n) + x;", "ExprCall(@floatFromInt)"),
("const y: f32 = 1.0;\nreturn x;", "type f32"),
("return n as f64;", "ExprCast(f64)"),
("return x as f64;", "ExprCast(f64)"),
("const k: i32 = x as i32;\nreturn x;", "ExprCast(f64)"),
("return (n > 0) as f64;", "ExprCast(f64)"),
("return x + 0.1 * 3.0;", "ExprBinary(*)"),
("return x * 2;", "ExprBinary(f64 * 2^k)"),
("return x % 2.0;", "ExprBinary(%)"),
Expand Down
56 changes: 56 additions & 0 deletions cli/t27b/tests/source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,62 @@ test t1 {
assert_eq!(prog.unchecked, vec!["all_id".to_string()]);
}

/// `@compileAssert` is `assert`: t27c's Zig backend lowers both through one
/// arm. In an invariant (a `comptime` block there) a false one fails the
/// reference's compile and fails the invariant here; in a test it is a
/// runtime check in both. An integer `as f64` is `@floatFromInt` with result
/// type f64, which is what the reference emits for it.
#[test]
fn compile_assert_is_assert() {
let src = "module ca;

const E: u8 = 3;
const N: u16 = 10;

fn half(x: u32) -> f64 {
return x as f64 / 2.0;
}

invariant widths {
@compileAssert(E + 7 == N);
}

invariant exponent_bounds {
@compileAssert((E as f64 - 0.5) * 2.618033988749895 <= N as f64 - 1.0);
@compileAssert(N as f64 - 1.0 <= (E as f64 + 0.5) * 2.618033988749895);
}

invariant broken {
@compileAssert(E as f64 > 3.5, \"exponent too small\");
}

test runtime_operand {
@compileAssert(half(7) == 3.5);
}

test runtime_false {
@compileAssert(half(7) == 3.0);
}
";
let r = run(src);
assert_eq!(
names_ok(&r),
vec![
("widths", true, true),
("exponent_bounds", true, true),
("broken", true, false),
("runtime_operand", false, true),
("runtime_false", false, false),
]
);
assert_eq!(r[2].2, Err((TrapKind::Assert, 20)));
assert_eq!(r[4].2, Err((TrapKind::Assert, 28)));

// The message must be a string literal, as for `assert`.
let m = rejected("module ca2;\nconst E: u8 = 3;\ninvariant i {\n @compileAssert(E == 3, E);\n}\n");
assert!(m.contains("unsupported construct ExprCall(assert with non-literal message)"), "{}", m);
}

#[test]
fn partially_parsed_invariant_is_rejected() {
let src = "module inv;
Expand Down
9 changes: 9 additions & 0 deletions docs/now/2026-10-05-t27b-compileassert.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# NOW -- t27b: @compileAssert lowers as assert, integer `as f64` as @floatFromInt (2026-10-05)

## t27b @compileAssert and integer as f64 (Closes #6319)

- `@compileAssert(cond[, "msg"])` lowers through the same arm as `assert` in `cli/t27b/src/lower.rs`, because t27c's Zig backend does exactly that: `if (!(cond))` fails the compile inside an `invariant` (a `comptime` block) and is a runtime check inside a `test`.
- `x as f64` with an integer `x` lowers as `@floatFromInt` with result type f64, which is what t27c emits (`@as(f64, @floatFromInt(x))`). A float or bool operand and f64 to an integer stay refused as `ExprCast(f64)`.
- Why both: the 9 `specs/numeric/gf*.t27` files whose only lab blocker was `ExprCall(@compileAssert)` stopped next at `EXP_BITS as f64`; with both they pass, invariants checked and the JIT agreeing with the interpreter.
- Verdict check: a false `@compileAssert` in a test fails that test in both paths; in an invariant the reference reports the file as not compiling and t27b reports the invariant broken -- not a pass in either.
- Test: `compile_assert_is_assert` in `cli/t27b/tests/source.rs` (true and false, invariant and test, f64 operands, message rule).
Loading