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
19 changes: 10 additions & 9 deletions .trinity/seals/conformance_T27bConformanceLazySignature.json
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
{
"built_by": "t27c-bootstrap@0.5.2+5d5d2cdef",
"built_by": "t27c-bootstrap@0.5.2+b258b9007",
"closure": [],
"config": "gen=c,rust,verilog,zig;tests=zig",
"gen_hash_c": "sha256:7cd34999bad2cf4147e9673fdc19e46b9d277f7c14a5e132aac6c3aafbf68251",
"gen_hash_rust": "sha256:5f5c3149f1f2fb1c5c0857a72c0a5ae16281c6e1146ad430b456b861a40052c4",
"gen_hash_verilog": "sha256:03bae060d5af0903b1cc6070972dadc2b66856ed32829a6de376051664186cc3",
"gen_hash_zig": "sha256:179da2d3b22dd83eaed934cb2c11c30328fe88b22f72365763ed9a5668c6e18b",
"gen_hash_c": "sha256:48e61719d9652215991783bfb8ac8dbe64de5f63ea5cc7b4becc486ac6e14bc2",
"gen_hash_rust": "sha256:5bfc144669060ccd8427846443c1ca5a92fcf85823eae3d15d93afc9ad152695",
"gen_hash_verilog": "sha256:fb7289752984f7574f1338dec896555f9f020862b9dd3b3954d3dabcf4caacae",
"gen_hash_zig": "sha256:99096a379a7d2e54ba3ef0ff4af3c8fad9f603a0ea65b118b527cef0bca72e68",
"module": "T27bConformanceLazySignature",
"ring": 12,
"seal_schema": 2,
"sealed_at": "2026-10-09T14:59:43Z",
"sealed_at": "2026-10-19T11:31:38Z",
"sealed_by": "t27c-bootstrap@0.5.2",
"spec_hash": "sha256:dd14bb1bdf3ccee51b4d8826ef1f94a2a7cbd546c6b8351beed99864dd9ccbfb",
"spec_hash": "sha256:fc31f992b370b61093b637072e3917bcfc0b39b3c18443aae22ba129b1a4a288",
"spec_path": "specs/tri/t27b/conformance/lazy_signature.t27",
"tests": {
"failed": 0,
"failing": [],
"forced": false,
"passed": 3,
"total": 3
"total": 3,
"vacuous": 0
},
"toolchain": {
"test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d",
"test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10",
"zig": "0.16.0"
}
}
18 changes: 9 additions & 9 deletions .trinity/seals/t27b_T27bAstShape.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"built_by": "t27c-bootstrap@0.5.2+6ea5f5173",
"built_by": "t27c-bootstrap@0.5.2+b258b9007",
"closure": [
{
"spec": "specs/tri/t27b/ast_scan.t27",
Expand All @@ -11,23 +11,23 @@
}
],
"config": "gen=c,rust,verilog,zig;tests=zig",
"gen_hash_c": "sha256:5d364480d3529b6009756d84116d9d481e321d8c320ce24a843041a7f29dfc26",
"gen_hash_rust": "sha256:4e704d25b7f4b792aa6ce89f4f3a50806f8c54b01b67c558f08f8ff2bf394c3d",
"gen_hash_verilog": "sha256:2c2142b41244b4590d814516841a2317aed3338f3219604f5e0be4ea79402c0f",
"gen_hash_zig": "sha256:6674efe14d8e5383ab45ce65943404f8240f2094b6e05b4a5987e095783cc78d",
"gen_hash_c": "sha256:928f3d4e3daebf7d936f277297ae93d434cf27c0de7e401a629f3120db95d1a2",
"gen_hash_rust": "sha256:53db31308fcab46e8ac8ab03e978473e2f0292b639319ff03f5f3db7395e3b98",
"gen_hash_verilog": "sha256:1ba2d510ff7fb2a66b6af29af31cd3019f3790a1b23ab21ee44eb4183df3de99",
"gen_hash_zig": "sha256:4ae04819eacf8c61a60c5ee713cac0dc292581df3476cba6bdd80c480553cdbe",
"module": "T27bAstShape",
"ring": 12,
"seal_schema": 2,
"sealed_at": "2026-10-18T17:53:21Z",
"sealed_at": "2026-10-19T09:47:24Z",
"sealed_by": "t27c-bootstrap@0.5.2",
"spec_hash": "sha256:ae2a008a6d8716ece11fb99ceb2301cbb0918cdf508b18445c45648cd09a28b4",
"spec_hash": "sha256:0307a528afa66ef858b319d8f523dd02d0a6cdb274e027c89627f58e97716422",
"spec_path": "specs/tri/t27b/ast_shape.t27",
"tests": {
"failed": 0,
"failing": [],
"forced": false,
"passed": 13,
"total": 13,
"passed": 14,
"total": 14,
"vacuous": 0
},
"toolchain": {
Expand Down
27 changes: 16 additions & 11 deletions .trinity/seals/t27b_T27bLazySigPlan.json
Original file line number Diff line number Diff line change
@@ -1,28 +1,33 @@
{
"built_by": "t27c-bootstrap@0.5.2+0c90f130b",
"closure": [],
"built_by": "t27c-bootstrap@0.5.2+b258b9007",
"closure": [
{
"spec": "specs/tri/t27b/literal_plan; // ends_with; no name here is also one of literal_plan's.t27",
"spec_hash": "missing"
}
],
"config": "gen=c,rust,verilog,zig;tests=zig",
"gen_hash_c": "sha256:1778a933ad395e520374b5b07cd20f59b762ee167c2261e8ba278d28708823ad",
"gen_hash_rust": "sha256:0db67a8fee63a2171b92446401939fb9f63717b3fcbe0c36055c02bec6c622e0",
"gen_hash_verilog": "sha256:a4a78b2712b754add4f119bcdbc2b048dd0a524afe185dc139fcec688f2c004d",
"gen_hash_zig": "sha256:b56648caf0aeff7e88610aaa4ead507b5a77ce164462c51f17434be227cac7f0",
"gen_hash_c": "sha256:ead2c0e6a5434b5712c413c0d7665466dcb0bac06d9f110e01bcc8e8054990d9",
"gen_hash_rust": "sha256:020ab7da823994a4637c017477b01060efcc26ec0fbb1654c47303fac1529ba4",
"gen_hash_verilog": "sha256:f034de34241bcff76c6446e91d52706a79181b2156cebe314b1ffae41819a0e3",
"gen_hash_zig": "sha256:395e2cf2b97a16de07baa83feb06c4edb261ca985199712bf4ffa1afe2ac5e4c",
"module": "T27bLazySigPlan",
"ring": 12,
"seal_schema": 2,
"sealed_at": "2026-10-10T21:19:42Z",
"sealed_at": "2026-10-19T10:59:10Z",
"sealed_by": "t27c-bootstrap@0.5.2",
"spec_hash": "sha256:9d572bcf73ed610686ef5019165c33a80d8aa977ea0b149e5672ffb724153913",
"spec_hash": "sha256:864275b577659678b0208e8a1cddc4153c15daa50e437782b1e6a45d9a53e5fc",
"spec_path": "specs/tri/t27b/lazy_sig_plan.t27",
"tests": {
"failed": 0,
"failing": [],
"forced": false,
"passed": 9,
"total": 9,
"passed": 16,
"total": 16,
"vacuous": 0
},
"toolchain": {
"test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d",
"test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10",
"zig": "0.16.0"
}
}
55 changes: 14 additions & 41 deletions cli/t27b/src/lower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,8 @@ struct Lower<'a> {
/// bench or module-level declaration names, and every fn those name, to
/// a fixed point (`analyzed_fns`).
analyzed: HashSet<String>,
/// Refusals withdrawn from the bodies of fns nothing analyzed reaches (`lazy_body`).
held: Vec<Reject>,
/// Lowering a fn outside `analyzed`: Zig compiles a fn body only when
/// something analyzed references it, so a body stub there is never seen.
unanalyzed_fn: bool,
Expand Down Expand Up @@ -573,6 +575,7 @@ fn lower_mode<'a>(
ret_poison: false,
decl_int: None,
analyzed: HashSet::new(),
held: Vec::new(),
unanalyzed_fn: false,
unresolved: HashSet::new(),
layout_err: false,
Expand Down Expand Up @@ -844,6 +847,7 @@ fn lower_mode<'a>(
_ => {}
}
}
l.restore_held(&items);
l.literal_writes();
if !l.errors.is_empty() {
return Err(l.errors);
Expand Down Expand Up @@ -1157,7 +1161,7 @@ impl<'a> Lower<'a> {
}
}
let rt = n.extra_return_type.trim();
let ret = if rt.is_empty() || rt == "void" || self.unanalyzed_undefined_ret(n) {
let ret = if rt.is_empty() || rt == "void" {
None
} else if tt::anon_struct(rt.as_bytes()) {
Some(self.anon_struct_ret(format!("{} (result of `{}`)", rt, n.name), rt)?)
Expand Down Expand Up @@ -1674,7 +1678,7 @@ impl<'a> Lower<'a> {
if !self.unanalyzed_fn && tt::returns_value(n.extra_return_type.as_bytes()) {
mark_tail_returns(&n.children, &mut self.tail_returns);
}
let lowered = self.stmts(&n.children);
let lowered = self.lazy_body(n, &n.children);
self.tail_returns.clear();
body.extend(lowered?);
let ret = ret.map(|t| reg_ty(&t).unwrap_or(Ty::Ptr));
Expand Down Expand Up @@ -2689,9 +2693,9 @@ impl<'a> Lower<'a> {
/// `expected == actual` or `|expected - actual| <= tolerance` (false
/// for a NaN). The operands are evaluated once, left to right.
///
/// Everywhere else the reference decides by where the statement sits: a
/// fn nothing analyzed reaches (or a bench, an uncalled fn) is never
/// analyzed, so the statement lowers to a trap no test reaches; an
/// Everywhere else the reference decides by where the statement sits: in a
/// fn nothing analyzed reaches (or a bench, an uncalled fn) the refusal
/// below is withdrawn with the body (`lazy_body`); an
/// `invariant` is a container-level `comptime` block, where Zig's
/// AstGen refuses `try` ("'try' outside function scope"); a reachable
/// fn returns a non-error type in the reference's Zig ("expected type
Expand All @@ -2710,11 +2714,6 @@ impl<'a> Lower<'a> {
"t27c emits an invariant as a container-level `comptime` block, where Zig refuses `try` (outside function scope)".into(),
);
}
if self.unanalyzed_fn {
let site = self.site(TrapKind::Stub, "try in a fn the reference never analyzes".into(), Ty::Bool);
out.push(Stmt::Assert { cond: Expr { ty: Ty::Bool, kind: ExprKind::Const(0) }, site });
return Ok(());
}
if !self.in_test {
return self.reject(
"ExprUnary(try) in a fn",
Expand Down Expand Up @@ -2840,18 +2839,11 @@ impl<'a> Lower<'a> {
fn return_stmt(&mut self, v: Option<&Node>, out: &mut Vec<Stmt>) -> R<()> {
// `return undefined;` (also a port's `fn stub() u32 { undefined; }`
// since #6315) compiles in the reference and hands the caller an
// undefined value. In a fn nothing analyzed reaches, Zig never sees
// the body and the return is the stub trap no test can hit. Where
// analysis reaches the fn, an aggregate result is left unwritten in
// the caller's memory (`init`, as before #6315); a scalar one t27b
// refuses rather than guess a value.
// undefined value. Where analysis reaches the fn, an aggregate result
// is left unwritten in the caller's memory (`init`, as before #6315);
// a scalar one t27b refuses rather than guess a value (withdrawn with
// the body in a fn nothing analyzed reaches: `lazy_body`).
if let Some(c) = v.filter(|c| c.kind == NodeKind::ExprIdentifier && c.name == "undefined") {
if self.ret.is_some() && !self.ret_poison && self.unanalyzed_fn {
self.see(c);
let site = self.site(TrapKind::Stub, "`return undefined;` in a fn the reference never analyzes".into(), Ty::Bool);
out.push(Stmt::Assert { cond: Expr { ty: Ty::Bool, kind: ExprKind::Const(0) }, site });
return Ok(());
}
// In a void fn `undefined` is the one `void` value, so the
// statement is a plain `return;`. Not in a test: its Zig result
// is an error union, and an undefined one is no value t27b can
Expand Down Expand Up @@ -5275,25 +5267,6 @@ impl<'a> Lower<'a> {
return self.slice_of(addr_of(&arr), 0, want.clone());
}
}
// `&[_]T{ ... } ** n` where a slice is wanted: t27c prints
// `&.{ ... } ** n`, which Zig reads as `(&.{ ... }) ** n`, a pointer
// to a tuple, and refuses once it analyzes it ("expected indexable").
// In a fn nothing analyzed reaches it is only parsed, so it lowers to
// the stub trap no test reaches; anywhere else it stays refused.
if self.unanalyzed_fn
&& n.kind == NodeKind::ExprUnary
&& n.extra_op == "&"
&& n.children.len() == 1
&& is_repeat_op(&n.children[0])
&& matches!(want, LTy::Str | LTy::Slice(..))
{
self.see(n);
let k = self.new_slot(want)?;
let site = self.site(TrapKind::Stub, "`&` of a repeated array literal in a fn the reference never analyzes".into(), Ty::Bool);
let trap = Stmt::Assert { cond: Expr { ty: Ty::Bool, kind: ExprKind::Const(0) }, site };
let addr = Expr { ty: Ty::Ptr, kind: ExprKind::Seq { stmts: vec![trap], value: Box::new(slot_expr(k)) } };
return Ok(Val::M(Place { addr, off: 0, ty: want.clone(), mutable: false, temp: Some(k) }));
}
let v = self.expr(n)?;
self.coerce_to(v, want)
}
Expand Down Expand Up @@ -6210,14 +6183,14 @@ impl<'a> Lower<'a> {
let (x, y) = (&self.data[*ka as usize][..*la as usize], &self.data[*kb as usize][..*lb as usize]);
Expr { ty: Ty::Bool, kind: ExprKind::Const((x == y) as i128) }
} else {
self.eql_used = true; // before the coercions: t27c prints `std.mem.eql` whether or not they hold (`restore_held`)
let mut args = Vec::new();
for v in [a, b] {
match self.coerce_to(v, &LTy::Str)? {
Val::M(p) => args.push(addr_of(&p)),
_ => return Err(()),
}
}
self.eql_used = true;
Expr { ty: Ty::Bool, kind: ExprKind::Call { func: self.nfuncs, args } }
};
if !negate {
Expand Down
61 changes: 38 additions & 23 deletions cli/t27b/src/lower/unanalyzed.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Signatures of fns the reference never analyzes.
//! Signatures and bodies of fns the reference never analyzes.
//!
//! Zig analyzes a fn lazily: one that no test, invariant or constant reaches
//! (`analyzed_fns`) is parsed but never type-checked, so its return type is
Expand All @@ -8,17 +8,10 @@

use super::{Lower, Reject, R};
use crate::compiler::{Node, NodeKind};
use crate::ir::{Expr, ExprKind, Stmt, Ty, TrapKind};
use std::collections::HashSet;

impl<'a> Lower<'a> {
/// `-> undefined` on a fn nothing analyzed reaches: lowered as a fn with
/// no result, whose `undefined;` body is the stub trap no test reaches.
/// On a reached fn the type stays refused (`type undefined`), as the
/// reference's Zig does not compile it.
pub(super) fn unanalyzed_undefined_ret(&self, n: &Node) -> bool {
n.extra_return_type.trim() == "undefined" && !self.analyzed.contains(&n.name)
}

/// `signature(n)` was just refused, and `nerr` errors were recorded
/// before it. Zig resolves a fn's parameter and result types only when
/// the fn is analyzed, so on a fn nothing analyzed reaches, a struct
Expand All @@ -35,25 +28,47 @@ impl<'a> Lower<'a> {
if self.errors[nerr..].iter().any(|e| e.construct.contains("undeclared")) {
return false;
}
if let Some(u) = self.undeclared_in(n) {
if let Some(u) = self.undeclared_in(n, true) {
let _: R<()> = self.reject("ExprIdentifier(undeclared)", format!("`{}` in `{}`, a body Zig's AstGen still resolves", u, n.name));
return false;
}
self.withdraw(nerr);
self.unresolved.insert(n.name.clone());
true
}

/// Withdraws the refusals recorded since the first `nerr`, and returns them. A struct whose failure was withdrawn
/// reports it again where an analyzed use lays it out (recovery mode names a cached failure only once).
fn withdraw(&mut self, nerr: usize) -> Vec<Reject> {
let dropped: Vec<Reject> = self.errors.drain(nerr..).collect();
// A struct whose failure was withdrawn reports it again where an
// analyzed use lays it out (recovery mode names a cached failure
// only once).
for sd in self.structs.iter_mut() {
let withdrawn = sd
.fail
.as_ref()
.is_some_and(|f| dropped.iter().any(|d| d.construct == f.construct && d.detail == f.detail));
if withdrawn {
if sd.fail.as_ref().is_some_and(|f| dropped.iter().any(|d| d.construct == f.construct && d.detail == f.detail)) {
sd.fail = None;
}
}
self.unresolved.insert(n.name.clone());
true
dropped
}

/// The body `ns` of fn `n`. Zig type-checks it only if something analyzed reaches `n`: in a fn nothing reaches, the
/// refusals that are not AstGen's (`ls::body_plan`) are withdrawn and the body is the stub trap no test reaches.
pub(super) fn lazy_body(&mut self, n: &Node, ns: &[Node]) -> R<Vec<Stmt>> {
let nerr = self.errors.len();
let r = self.stmts(ns);
if !self.unanalyzed_fn || self.errors.len() == nerr || self.errors[nerr..].iter().any(|e| super::ls::body_plan(e.construct.as_bytes(), e.detail.as_bytes()) == super::ls::KEEP) {
return r;
}
let dropped = self.withdraw(nerr);
self.held.extend(dropped);
let site = self.site(TrapKind::Stub, format!("body of `{}`, which the reference never analyzes", n.name), Ty::Bool);
Ok(vec![Stmt::Assert { cond: Expr { ty: Ty::Bool, kind: ExprKind::Const(0) }, site }])
}

/// After every body, the refusals `lazy_body` withdrew come back if AstGen refuses the file anyway (`ls::withdrawn_come_back`).
pub(super) fn restore_held(&mut self, items: &[&Node]) {
let prelude = items.iter().any(|c| matches!(c.kind, NodeKind::TestBlock | NodeKind::InvariantBlock));
if !self.held.is_empty() && super::ls::withdrawn_come_back(items.iter().any(|f| f.kind == NodeKind::FnDecl && self.undeclared_in(f, prelude).is_some()), !prelude, self.eql_used || super::marked(items.iter().copied(), super::sh::string_eq).next().is_some()) {
self.errors.append(&mut self.held);
}
}

/// A refused signature type Zig resolves only when the fn is analyzed, as `layout` failures are
Expand All @@ -64,8 +79,8 @@ impl<'a> Lower<'a> {
super::ls::plan(t.as_bytes(), param, k > 0 && self.zig_spelled(&t[k..])) == super::ls::UNRESOLVED
}

/// A name the body of `f` uses that neither `f` nor the file declares and Zig does not know (`ls::zig_knows`).
fn undeclared_in(&self, f: &Node) -> Option<String> {
/// A name the body of `f` uses that neither `f` nor the file declares and Zig does not know (`ls::zig_knows_in`).
fn undeclared_in(&self, f: &Node, prelude: bool) -> Option<String> {
fn walk(ns: &[Node], local: &mut HashSet<String>, used: &mut Vec<String>) {
for n in ns {
local.extend(n.params.iter().map(|(p, _)| p.trim().to_string()));
Expand All @@ -79,7 +94,7 @@ impl<'a> Lower<'a> {
}
let (mut local, mut used) = (HashSet::new(), Vec::new());
walk(std::slice::from_ref(f), &mut local, &mut used);
used.into_iter().find(|u| !u.is_empty() && !local.contains(u) && !self.fns.contains_key(u) && !self.type_decls.contains_key(u) && !super::ls::zig_knows(u.as_bytes()))
used.into_iter().find(|u| !u.is_empty() && !local.contains(u) && !self.fns.contains_key(u) && !self.type_decls.contains_key(u) && !super::ls::zig_knows_in(u.as_bytes(), prelude))
}

/// A call to a fn `unresolved_sig` withdrew. Only a fn nothing analyzed
Expand Down
2 changes: 1 addition & 1 deletion cli/t27b/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ pub fn run(src: &str) -> Vec<(String, bool, Outcome)> {
/// The first rejection message, which must exist.
pub fn rejected(src: &str) -> String {
match lower_src(src) {
Ok(_) => panic!("expected a rejection"),
Ok(_) => panic!("expected a rejection of {}", src),
Err(e) => e[0].clone(),
}
}
Expand Down
6 changes: 3 additions & 3 deletions cli/t27b/tests/differential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2254,7 +2254,7 @@ test nan_to_int {
#[test]
fn f64_refusals_name_the_construct() {
let first = |body: &str| -> String {
let src = format!("module f64rej;\nfn f(x: f64, n: i32) f64 {{\n{}\n}}\n", body);
let src = format!("module f64rej;\nfn f(x: f64, n: i32) f64 {{\n{}\n}}\ntest t {{ assert(f(1.0, 1) == 0.0); }}\n", body);
match f64_lower(&src) {
Ok(_) => panic!("expected a rejection for {}", body),
Err(e) => e[0].clone(),
Expand Down Expand Up @@ -2581,7 +2581,7 @@ test f32_out_of_range {
#[test]
fn f32_refusals_name_the_construct() {
let first = |body: &str| -> String {
let src = format!("module f32rej;\nfn f(x: f32, n: i32) f32 {{\n{}\n}}\n", body);
let src = format!("module f32rej;\nfn f(x: f32, n: i32) f32 {{\n{}\n}}\ntest t {{ assert(f(1.0, 1) == 0.0); }}\n", body);
match f64_lower(&src) {
Ok(_) => panic!("expected a rejection for {}", body),
Err(e) => e[0].clone(),
Expand Down Expand Up @@ -2882,7 +2882,7 @@ fn shift_source_programs_run_in_both_engines() {
#[test]
fn shift_refusals_name_the_construct() {
let first = |body: &str| -> String {
let src = format!("module shiftrej;\nconst KB: u32 = 40;\nfn f(x: u32, n: u32) u32 {{\n{}\n}}\n", body);
let src = format!("module shiftrej;\nconst KB: u32 = 40;\nfn f(x: u32, n: u32) u32 {{\n{}\n}}\ntest t {{ assert(f(1, 1) == 0); }}\n", body);
match f64_lower(&src) {
Ok(_) => panic!("expected a rejection for {}", body),
Err(e) => e[0].clone(),
Expand Down
Loading
Loading