From d255aae9ba2c6576d64db93a1d48a88295bef649 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 16:20:25 +0700 Subject: [PATCH 1/5] t27b: do not lower bodies no test reaches -- Sema-level refusals in an unreached fn are withdrawn, from one plan (Closes #10897) Zig type-checks only what an analyzed declaration uses, so a fn nothing reaches is parsed and name-checked (AstGen) but never type-checked (Sema). t27b lowered every fn and refused the first construct it could not, so a t27o spec whose only trouble is `return TelegramStarsGateway;` (a Rust unit struct written as a bare type name) was refused while t27c passes it. The rule is lazy_sig_plan.t27's, carried from the signature to the body: - lazy_sig_plan.t27 `body_plan(construct, detail)`: which refusals recorded in an unreached body are AstGen's (kept: break outside a loop, a local shadowing a module var, two `else` prongs, a char prong, a char/string literal Zig cannot read, a struct field set twice, `_ = p;` of a used parameter, and the spellings t27c prints that Zig cannot parse: tuple, map type, tuple assignment, array literal text) and which are Sema's (withdrawn). Every kept class was pinned against `t27c test-report` on a fn no test reaches. - `withdrawn_come_back(names_missing, bare, eql_used)`: the withdrawn refusals return after every body when some fn names something declared nowhere, or when the module has no test and so no `std` (t27c writes `std` only into a module with a test or an invariant) yet prints a string `==` as `std.mem.eql`. - `zig_knows_in(name, prelude)` and `is_undefined`: `std`/`assert` are Zig's only with the prelude; the `-> undefined` result becomes a lazy signature type. It subsumes the four per-construct stubs (`try`, `return undefined`, `&` of a repeated literal, `-> undefined`). A reached fn keeps every refusal; conformance/lazy_signature.t27 gains the unit struct returned as a value, and tests/tail.rs pins that a test calling it, and a call to a fn declared nowhere, stay refused. Five tests whose sources never reached the fn they refuse now call it (the reference compiles them as written). Hand-written lines in cli/t27b: +69 -78 (net -9). Co-Authored-By: Claude Sonnet 5.5 --- ...formance_T27bConformanceLazySignature.json | 19 +-- .trinity/seals/t27b_T27bLazySigPlan.json | 20 +-- cli/t27b/src/lower.rs | 55 ++----- cli/t27b/src/lower/unanalyzed.rs | 61 +++++--- cli/t27b/tests/common/mod.rs | 2 +- cli/t27b/tests/differential.rs | 6 +- cli/t27b/tests/source.rs | 18 +-- cli/t27b/tests/tail.rs | 7 +- gen/rust/tri/t27b/lazy_sig_plan.rs | 58 ++++++- specs/tri/t27b/conformance/lazy_signature.t27 | 14 ++ specs/tri/t27b/lazy_sig_plan.t27 | 148 ++++++++++++++++++ 11 files changed, 309 insertions(+), 99 deletions(-) diff --git a/.trinity/seals/conformance_T27bConformanceLazySignature.json b/.trinity/seals/conformance_T27bConformanceLazySignature.json index 9970bd29a6..7b08cef0cc 100644 --- a/.trinity/seals/conformance_T27bConformanceLazySignature.json +++ b/.trinity/seals/conformance_T27bConformanceLazySignature.json @@ -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:645a2490c635acec56e01793336a579d0196153720d2da856106edb7846699cb", + "gen_hash_rust": "sha256:6aa15ee11052c06244259fa6b42dbcf0ece274d13e1a13ded38ba3fb10083547", + "gen_hash_verilog": "sha256:0b3f6b264f6f5c963c10422eaf3b48c6158e130713ea59e283212bb51ea84d19", + "gen_hash_zig": "sha256:6349038e39284199925a86167610194f5c5f49cc537e1188c2fceeae874302fd", "module": "T27bConformanceLazySignature", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-09T14:59:43Z", + "sealed_at": "2026-10-19T09:10:46Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:dd14bb1bdf3ccee51b4d8826ef1f94a2a7cbd546c6b8351beed99864dd9ccbfb", + "spec_hash": "sha256:1139bcf6bd224dff9b172a63a90810e39ba89bd4be4e88a0aacd31289a558c52", "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" } } \ No newline at end of file diff --git a/.trinity/seals/t27b_T27bLazySigPlan.json b/.trinity/seals/t27b_T27bLazySigPlan.json index 7dc9c1ee99..10a59b3b53 100644 --- a/.trinity/seals/t27b_T27bLazySigPlan.json +++ b/.trinity/seals/t27b_T27bLazySigPlan.json @@ -1,28 +1,28 @@ { - "built_by": "t27c-bootstrap@0.5.2+0c90f130b", + "built_by": "t27c-bootstrap@0.5.2+b258b9007", "closure": [], "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:2ea83445e8b2bde258c000cdd93a62c812fa13ecb6f151e8ee16ef3808a32655", + "gen_hash_rust": "sha256:25adba263a266c8762cf05c59ffae70adbf5855a3fe796584cc8796eeab28984", + "gen_hash_verilog": "sha256:ef530c1ae9efdddcff900ac0e605d82d04b400a366a58b1c8c30d28ece9094b6", + "gen_hash_zig": "sha256:dba77f51ff91e0f065dfc704a060b9e33a8db0800633f9d833afe7758d8ce967", "module": "T27bLazySigPlan", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-10T21:19:42Z", + "sealed_at": "2026-10-19T09:10:01Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:9d572bcf73ed610686ef5019165c33a80d8aa977ea0b149e5672ffb724153913", + "spec_hash": "sha256:669cd3569fa5f78e727f30971dd8642d10504a7406ac041cc480dbcff6dd9208", "spec_path": "specs/tri/t27b/lazy_sig_plan.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 9, - "total": 9, + "passed": 15, + "total": 15, "vacuous": 0 }, "toolchain": { - "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", "zig": "0.16.0" } } \ No newline at end of file diff --git a/cli/t27b/src/lower.rs b/cli/t27b/src/lower.rs index 65c580fc04..0bf73630e5 100644 --- a/cli/t27b/src/lower.rs +++ b/cli/t27b/src/lower.rs @@ -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, + /// Refusals withdrawn from the bodies of fns nothing analyzed reaches (`lazy_body`). + held: Vec, /// 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, @@ -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, @@ -844,6 +847,7 @@ fn lower_mode<'a>( _ => {} } } + l.restore_held(&items); l.literal_writes(); if !l.errors.is_empty() { return Err(l.errors); @@ -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)?) @@ -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)); @@ -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 @@ -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", @@ -2840,18 +2839,11 @@ impl<'a> Lower<'a> { fn return_stmt(&mut self, v: Option<&Node>, out: &mut Vec) -> 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 @@ -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) } @@ -6210,6 +6183,7 @@ 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, <y::Str)? { @@ -6217,7 +6191,6 @@ impl<'a> Lower<'a> { _ => return Err(()), } } - self.eql_used = true; Expr { ty: Ty::Bool, kind: ExprKind::Call { func: self.nfuncs, args } } }; if !negate { diff --git a/cli/t27b/src/lower/unanalyzed.rs b/cli/t27b/src/lower/unanalyzed.rs index f4c9f61dfb..d57753ffe2 100644 --- a/cli/t27b/src/lower/unanalyzed.rs +++ b/cli/t27b/src/lower/unanalyzed.rs @@ -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 @@ -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 @@ -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 { let dropped: Vec = 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> { + 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) { + self.errors.append(&mut self.held); + } } /// A refused signature type Zig resolves only when the fn is analyzed, as `layout` failures are @@ -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 { + /// 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 { fn walk(ns: &[Node], local: &mut HashSet, used: &mut Vec) { for n in ns { local.extend(n.params.iter().map(|(p, _)| p.trim().to_string())); @@ -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 diff --git a/cli/t27b/tests/common/mod.rs b/cli/t27b/tests/common/mod.rs index 516b82c316..07f4bb40fd 100644 --- a/cli/t27b/tests/common/mod.rs +++ b/cli/t27b/tests/common/mod.rs @@ -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(), } } diff --git a/cli/t27b/tests/differential.rs b/cli/t27b/tests/differential.rs index 2733cd9d5a..bfd0a277f2 100644 --- a/cli/t27b/tests/differential.rs +++ b/cli/t27b/tests/differential.rs @@ -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(), @@ -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(), @@ -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(), diff --git a/cli/t27b/tests/source.rs b/cli/t27b/tests/source.rs index c491d07acc..a59314808b 100644 --- a/cli/t27b/tests/source.rs +++ b/cli/t27b/tests/source.rs @@ -56,7 +56,7 @@ fn run(src: &str) -> Vec<(String, bool, Outcome)> { /// The first rejection message, which must exist. 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(), } } @@ -1040,10 +1040,10 @@ fn slice_rejections_are_precise() { ("test t { assert(A[1..4].len == 3); }", "ExprIndex(slice)", "end 4 out of bounds for `[3]u32`"), ("test t { assert(A[2..1].len == 0); }", "ExprIndex(slice)", "start 2 is past end 1"), ("test t { assert(\"ab\"[2] == 0); }", "ExprIndex", "index 2 out of bounds for a string of length 2"), - ("fn f(xs: []u32) void { xs.len = 0; }\ntest t { }", "StmtAssign", "assignment through a constant"), - ("fn f(xs: []const u32) void { xs[0] = 1; }\ntest t { }", "StmtAssign", "assignment through a constant"), + ("fn f(xs: []u32) void { xs.len = 0; }\ntest t { f(&A); }", "StmtAssign", "assignment through a constant"), + ("fn f(xs: []const u32) void { xs[0] = 1; }\ntest t { f(&A); }", "StmtAssign", "assignment through a constant"), ("const S: []const u32 = A[0..];\ntest t { assert(S.len == 3); }", "ConstDecl(slice)", "module-level slice"), - ("fn f(xs: []u32) u32 { return xs.ptr; }\ntest t { }", "ExprFieldAccess(slice)", "`.ptr` of a []u32"), + ("fn f(xs: []u32) u32 { return xs.ptr; }\ntest t { assert(f(&A) == 0); }", "ExprFieldAccess(slice)", "`.ptr` of a []u32"), ]; for (body, construct, detail) in cases { let m = rejected(&format!("{}{}\n", head, body)); @@ -2838,7 +2838,7 @@ fn tuples_rejections() { ), ("fn f() -> (u32, u32) { return (1, 2, 3); }", "ExprTuple", "3 values for `(u32, u32)`"), ("test t { const (a, b, c) = p(); assert(a == 1); }", "StmtLocal(destructure)", "3 names for 2 elements"), - ("fn g(i: usize) -> u32 { const t = p(); return t[i]; }", "ExprIndex(tuple)", "not known at compile time"), + ("fn g(i: usize) -> u32 { const t = p(); return t[i]; }\ntest t { assert(g(0) == 1); }", "ExprIndex(tuple)", "not known at compile time"), ("test t { const t = p(); assert(t[2] == 1); }", "ExprIndex(tuple)", "index 2 out of bounds"), ("test t { const t = q(); assert(t.0.x == 1); }", "ExprFieldAccess", "no field `0.x`"), ("fn g(v: u32) -> u32 { const t = (v, 2); return t[0]; }", "ExprTuple", ""), @@ -2863,7 +2863,7 @@ fn untyped_locals_follow_the_reference() { (" var x = undefined;\n return 0;\n}", "StmtLocal", "neither type nor value"), (" const c = undefined;\n return c;\n}", "ExprIdentifier", "`c`"), ] { - let m = rejected(&format!("{}{}\n", head, body)); + let m = rejected(&format!("{}{}\ntest t {{ assert(f() == 0); }}\n", head, body)); assert!(m.starts_with(&format!("t27b: unsupported construct {}", construct)), "{}: {}", body, m); assert!(m.contains(detail), "{}: {}", body, m); } @@ -2939,7 +2939,7 @@ fn parameter_discard_rejections() { ("fn f(x: u32, y: u32) -> u32 { if (y > 1) { _ = x; _ = x; } return x; }", "StmtAssign(discard)", "`_ = x;`"), ("fn f(x: u32, y: u32) -> u32 { _ = x; if (y > 1) { _ = x; } return y + x; }", "StmtAssign(discard)", "`_ = x;`"), ("fn f(K: u32, y: u32) -> u32 { if (y > 1) { _ = K; } return y + K; }", "StmtAssign(discard)", "`_ = K;`"), - ("fn f(x: u32) -> u32 { var y: u32 = x; _ = y; return 1; }", "StmtAssign(undeclared)", "`_`"), + ("fn f(x: u32) -> u32 { var y: u32 = x; _ = y; return 1; }\ntest t { assert(f(1) == 1); }", "StmtAssign(undeclared)", "`_`"), ]; for (body, construct, detail) in cases { let m = rejected(&format!("{}{}\n", head, body)); @@ -3012,8 +3012,8 @@ fn type_alias_rejections() { ("const S = str;\nfn f(x: S) -> u32 { return 1; }", "type (alias)", "`S`"), ("const W = [4]str;\nfn f(x: W) -> u32 { return 1; }", "type (alias)", "`W`"), // a struct literal of a scalar alias: refused, not recursed into - ("const Code = u8;\nconst Duo = Code;\nfn f() -> u32 {\n const p: Duo = Duo{ .lo = 3, .hi = 9 };\n return 1;\n}", "ExprStructLit", "`Duo` is"), - ("const Code = u8;\nfn f() -> u32 { return Code; }", "ExprIdentifier(type as value)", "`Code`"), + ("const Code = u8;\nconst Duo = Code;\nfn f() -> u32 {\n const p: Duo = Duo{ .lo = 3, .hi = 9 };\n return 1;\n}\ntest t { assert(f() == 1); }", "ExprStructLit", "`Duo` is"), + ("const Code = u8;\nfn f() -> u32 { return Code; }\ntest t { assert(f() == 1); }", "ExprIdentifier(type as value)", "`Code`"), ]; for (body, construct, detail) in cases { let m = rejected(&format!("module a;\n{}\n", body)); diff --git a/cli/t27b/tests/tail.rs b/cli/t27b/tests/tail.rs index c258db0a1e..68588a6312 100644 --- a/cli/t27b/tests/tail.rs +++ b/cli/t27b/tests/tail.rs @@ -160,7 +160,7 @@ fn wide_integer_constants_fold_or_are_refused() { let head = "module a;\n\nconst A: u128 = 340282366920938463463374607431768211455;\nconst B: u256 = 18446744073709551616;\nconst C: i128 = 5;\nconst D: u128 = 0 - 1;\nconst E: u128 = E + 1;\n\nfn g(x: u128) -> u256 {\n return x;\n}\n\n"; let t = |body: &str| format!("{}test t {{\n assert({});\n}}\n", head, body); assert_eq!(names_ok(&run(&t("B < A and (A >> 64) as u64 == 18446744073709551615 and (0 - C) as i8 == -5"))), vec![("t", false, true)]); - let h = format!("{}fn h(x: u64) -> bool {{\n return A > x;\n}}\n", head); + let h = format!("{}fn h(x: u64) -> bool {{\n return A > x;\n}}\ntest t {{\n assert(h(1));\n}}\n", head); for (src, ty, why) in [(t("A + 1 > 0"), "u128", "overflows"), (t("B as u64 == 0"), "u256", "does not fit"), (t("A << 128 == 0"), "u128", "shift amount"), (t("C / 0 == 1"), "i128", "by zero"), (t("A + B > 0"), "u128", "two different"), (t("(0 - C) / 2 < 0"), "i128", "negative"), (t("D > 0"), "u128", "does not fit"), (h, "u128", "beside a value"), (t("E > 0"), "u128", "beside a value"), (t("g(1) == 0"), "u128", "")] { @@ -375,7 +375,8 @@ fn a_discarded_value_runs_unless_the_reference_deletes_it() { /// `anytype` and `[*]T` in the signature of a fn no test reaches (gh.t27, trios-scarab-types SR-00 and SR-02, #7857): /// Zig never resolves them, so the reference passes the file (specs/tri/t27b/lazy_sig_plan.t27; `t27c test-report` /// passes the conformance spec 3/3, none vacuous). A call from a test, an undeclared element type and an undeclared -/// name in the withdrawn body keep their refusals, as the reference refuses each. +/// name in the withdrawn body keep their refusals, as the reference refuses each. The unit struct returned as a value +/// by `gate_new` (#10897) is refused as soon as a test calls it (BLOCKED, "expected type 'spec.Gate', found 'type'"). #[test] fn a_signature_zig_never_resolves() { let src = include_str!("../../../specs/tri/t27b/conformance/lazy_signature.t27"); @@ -385,6 +386,8 @@ fn a_signature_zig_never_resolves() { assert!(rejected(&src.replace("assert(issue_score(i) == 34);", "format_into(i, 5);")).contains("type anytype")); assert!(rejected(&src.replace("?[*]Issue", "?[*]Missing")).contains("type [*]T")); assert!(rejected(&src.replace("all: [*]const Issue", "all: [*]const Issue, m: Missing")).contains("type [*]T")); + assert!(rejected(&src.replace("== 34);", "== 34);\n gate_new();")).contains("unknown name `Gate`")); + assert!(common::lower_src(&src.replace("return gate_new();", "return gate_nowhere();")).unwrap_err().join("\n").contains("ExprCall(undeclared fn)")); let m = common::lower_src(&src.replace("f.write_num", "undeclared_thing")).unwrap_err().join("\n"); assert!(m.contains("ExprIdentifier(undeclared) at line 23 (`undeclared_thing` in `format_into`"), "{}", m); } diff --git a/gen/rust/tri/t27b/lazy_sig_plan.rs b/gen/rust/tri/t27b/lazy_sig_plan.rs index ec40f0df49..4fe00860b7 100644 --- a/gen/rust/tri/t27b/lazy_sig_plan.rs +++ b/gen/rust/tri/t27b/lazy_sig_plan.rs @@ -1,6 +1,6 @@ // Generated from .t27 spec // DO NOT EDIT — generated by t27c -// NOT LOWERED BY THIS BACKEND: 9 test(s), 0 invariant(s), 0 function(s) with no body. +// NOT LOWERED BY THIS BACKEND: 15 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. @@ -74,6 +74,10 @@ pub fn is_comptime_int(t: &[u8]) -> bool { return ((t.len() == 12) && begins(t, b"comptime_int")); } +pub fn is_undefined(t: &[u8]) -> bool { + return ((t.len() == 9) && begins(t, b"undefined")); +} + pub fn plan(t: &[u8], param: bool, elem_spelled: bool) -> u8 { if (is_anytype(t) || is_comptime_int(t)) { if param { @@ -81,6 +85,12 @@ pub fn plan(t: &[u8], param: bool, elem_spelled: bool) -> u8 { } return KEEP; } + if is_undefined(t) { + if param { + return KEEP; + } + return UNRESOLVED; + } if ((many_item_elem(t) != 0) && elem_spelled) { return UNRESOLVED; } @@ -137,3 +147,49 @@ pub fn zig_knows(name: &[u8]) -> bool { return listed(name, b"_ std assert assert_eq true false null undefined void bool noreturn type anyerror anyopaque anytype comptime_int comptime_float usize isize f16 f32 f64 f80 f128"); } +pub fn zig_knows_in(name: &[u8], prelude: bool) -> bool { + if (!(prelude) && listed(name, b"std assert assert_eq")) { + return false; + } + return zig_knows(name); +} + +pub fn ends(t: &[u8], word: &[u8]) -> bool { + if (word.len() > t.len()) { + return false; + } + return begins(&t[((t.len() - word.len())) as usize..(t.len()) as usize], word); +} + +pub fn body_plan(construct: &[u8], detail: &[u8]) -> u8 { + if (begins(construct, b"StmtBreak") || begins(construct, b"StmtContinue")) { + return KEEP; + } + if (begins(construct, b"StmtLocal(shadows") || begins(construct, b"StmtLocal(destructure")) { + return KEEP; + } + if (begins(construct, b"StmtAssign(discard") || begins(construct, b"StmtAssign(tuple")) { + return KEEP; + } + if (begins(construct, b"ExprSwitch(two else") || begins(construct, b"ExprSwitch(char prong")) { + return KEEP; + } + if ((begins(construct, b"ExprLiteral(char") || begins(construct, b"ExprLiteral(string")) || begins(construct, b"ExprLiteral(literal")) { + return KEEP; + } + if (begins(construct, b"ExprArrayLiteral(text") || begins(construct, b"ExprTuple")) { + return KEEP; + } + if (begins(construct, b"type (tuple") || begins(construct, b"type [K:V")) { + return KEEP; + } + if (begins(construct, b"ExprStructLit") && ends(detail, b"initialised twice")) { + return KEEP; + } + return UNRESOLVED; +} + +pub fn withdrawn_come_back(names_missing: bool, bare: bool, eql_used: bool) -> bool { + return (names_missing || (bare && eql_used)); +} + diff --git a/specs/tri/t27b/conformance/lazy_signature.t27 b/specs/tri/t27b/conformance/lazy_signature.t27 index 05ba7f12f8..7f744560e1 100644 --- a/specs/tri/t27b/conformance/lazy_signature.t27 +++ b/specs/tri/t27b/conformance/lazy_signature.t27 @@ -63,3 +63,17 @@ test a_tie_goes_to_the_second { assert(heavier(a, b) == 7); assert(issue_score(b) == 75); } + +// The bodies (#10897): `gate_new` returns the unit struct `Gate` as a value, which Zig refuses ("expected type +// 'spec.Gate', found 'type'") only in a body it type-checks, so `gate_new` and `gate_default` compile while no test +// reaches them. It is the shape t27o writes for a Rust unit struct (trios-bridge BR-00). A test that calls either one, +// or a call to a fn declared nowhere, makes the reference refuse the file (tests/tail.rs). +struct Gate {} + +fn gate_new() -> Gate { + return Gate; +} + +fn gate_default() -> Gate { + return gate_new(); +} diff --git a/specs/tri/t27b/lazy_sig_plan.t27 b/specs/tri/t27b/lazy_sig_plan.t27 index d872c2d5a1..3d0cbea7a3 100644 --- a/specs/tri/t27b/lazy_sig_plan.t27 +++ b/specs/tri/t27b/lazy_sig_plan.t27 @@ -19,6 +19,14 @@ ; name check reads expressions only (names, calls, struct literals), not the type text of a local's annotation. ; Not covered: a fn a test reaches. There Zig instantiates `anytype` per call (the parameter no instance reads is ; another lane) and lays out `[*]T`; both keep their refusals here. +; -- +; The body (t27o epic #10879, #10897). Sema type-checks only what an analyzed declaration uses, so the body of a fn +; nothing analyzed reaches is parsed and name-checked by AstGen but never type-checked: `return Gw;` for a unit struct `Gw` (Zig: "expected type 'spec.Gw', found 'type'"), +; a list t27b cannot type, a builtin it lacks, all compile when no test calls the fn. `body_plan` asks about each +; refusal t27b recorded in such a body: UNRESOLVED withdraws it and the body lowers to the stub trap no test +; reaches; KEEP is a refusal AstGen makes in every fn (a name declared nowhere, a break outside a loop, a shadowed +; name, two `else` prongs). It subsumes the per-construct stubs that stood here for `try`, `return undefined`, `&` of +; a repeated literal and `-> undefined`; a reached fn keeps every refusal (pinned by conformance/lazy_signature.t27). ; ASCII only (L3) ; phi^2 + 1/phi^2 = 3 | TRINITY @@ -81,6 +89,12 @@ pub fn is_comptime_int(t: []const u8) -> bool { return t.len == 12 and begins(t, "comptime_int"); } +/// Is `t` exactly `undefined`? t27c prints a port's `-> undefined` stub as `fn f() undefined { undefined; }` +/// (#6315); Zig refuses that result type ("expected type 'type', found '@TypeOf(undefined)'") once a fn is analyzed. +pub fn is_undefined(t: []const u8) -> bool { + return t.len == 9 and begins(t, "undefined"); +} + /// The decision for one signature type t27b refused. param: it is a parameter's type, the only place `anytype` /// parses. elem_spelled: for `[*]T`, T is a type Zig reads in this file. UNRESOLVED: Zig resolves the type only /// when the fn is analyzed. @@ -89,6 +103,10 @@ pub fn plan(t: []const u8, param: bool, elem_spelled: bool) -> u8 { if (param) { return UNRESOLVED; } return KEEP; } + if (is_undefined(t)) { + if (param) { return KEEP; } + return UNRESOLVED; + } if (many_item_elem(t) != 0 and elem_spelled) { return UNRESOLVED; } return KEEP; } @@ -133,6 +151,48 @@ pub fn zig_knows(name: []const u8) -> bool { return listed(name, "_ std assert assert_eq true false null undefined void bool noreturn type anyerror anyopaque anytype comptime_int comptime_float usize isize f16 f32 f64 f80 f128"); } +/// `zig_knows`, in a module whose Zig has the prelude or not. t27c writes `const std = @import("std");` and +/// `__t27_assert_fail`, which `assert` and `assert_eq` print through, only into a module with a test or an +/// invariant (`has_tests` in compiler.rs); in any other `std` and `assert` are undeclared and AstGen refuses the +/// fn that names them, analyzed or not (`t27c test-report`: "use of undeclared identifier 'std'"). +pub fn zig_knows_in(name: []const u8, prelude: bool) -> bool { + if (!prelude and listed(name, "std assert assert_eq")) { return false; } + return zig_knows(name); +} + +/// Does `t` end with `word`? +fn ends(t: []const u8, word: []const u8) -> bool { + if (word.len > t.len) { return false; } + return begins(t[t.len - word.len .. t.len], word); +} + +/// The decision for one refusal t27b recorded while lowering the body of a fn nothing analyzed reaches. construct +/// and detail: the refusal's name and words. UNRESOLVED: Zig meets the construct only in Sema, which never runs on +/// this body, so the refusal is withdrawn. KEEP: AstGen or the parser refuses it in every fn (each pinned against +/// `t27c test-report` on a fn no test reaches): `break` outside a loop; a local that shadows a module var; two +/// `else` prongs; a char prong, a char or string literal Zig cannot read; a struct field set twice; `_ = p;` of a +/// parameter the fn uses; and the spellings t27c prints verbatim that Zig cannot parse (a tuple, a map type, a +/// tuple assignment, an array literal kept as text). +pub fn body_plan(construct: []const u8, detail: []const u8) -> u8 { + if (begins(construct, "StmtBreak") or begins(construct, "StmtContinue")) { return KEEP; } + if (begins(construct, "StmtLocal(shadows") or begins(construct, "StmtLocal(destructure")) { return KEEP; } + if (begins(construct, "StmtAssign(discard") or begins(construct, "StmtAssign(tuple")) { return KEEP; } + if (begins(construct, "ExprSwitch(two else") or begins(construct, "ExprSwitch(char prong")) { return KEEP; } + if (begins(construct, "ExprLiteral(char") or begins(construct, "ExprLiteral(string") or begins(construct, "ExprLiteral(literal")) { return KEEP; } + if (begins(construct, "ExprArrayLiteral(text") or begins(construct, "ExprTuple")) { return KEEP; } + if (begins(construct, "type (tuple") or begins(construct, "type [K:V")) { return KEEP; } + if (begins(construct, "ExprStructLit") and ends(detail, "initialised twice")) { return KEEP; } + return UNRESOLVED; +} + +/// Whether the refusals withdrawn from unreached bodies come back, after every body is lowered: AstGen refuses the +/// whole file, whichever fn it reads, when some fn names something declared nowhere (names_missing), or when the +/// module has no test and no invariant (bare) yet t27c printed a string `==` as `std.mem.eql` (eql_used), `std` +/// being written only where a test or an invariant is (`zig_knows_in`). +pub fn withdrawn_come_back(names_missing: bool, bare: bool, eql_used: bool) -> bool { + return names_missing or (bare and eql_used); +} + // ---------------------------------------------------------------------------------------------------------- // Tests: the corpus shapes, then what keeps its refusal. // ---------------------------------------------------------------------------------------------------------- @@ -227,3 +287,91 @@ test any_other_name_needs_a_declaration { assert(!zig_knows("st")); assert(!zig_knows("print")); } + +test an_undefined_result_is_resolved_lazily { + // A port's `fn f() undefined { undefined; }` (#6315): the type is read once the fn is analyzed. + assert(is_undefined("undefined")); + assert(plan("undefined", false, false) == UNRESOLVED); + assert(plan("undefined", true, false) == KEEP); + assert(!is_undefined("undefine") and !is_undefined("undefineds") and !is_undefined("?undefined") and !is_undefined("")); +} + +test sema_level_refusals_in_an_unreached_body_are_withdrawn { + // t27o: a unit struct used as a value (`return Gw;`), a list t27b cannot type, a builtin it lacks, a mismatch. + assert(body_plan("ExprIdentifier(undeclared)", "unknown name `Gw`") == UNRESOLVED); + assert(body_plan("ExprArrayLiteral", "array literal with no result type") == UNRESOLVED); + assert(body_plan("type mismatch", "expected u8, found u32") == UNRESOLVED); + assert(body_plan("ExprCall(@floatFromInt)", "") == UNRESOLVED); + assert(body_plan("ExprUnary(try) in a fn", "") == UNRESOLVED); + assert(body_plan("ExprReturn(undefined)", "") == UNRESOLVED); + assert(body_plan("ExprCall(unresolved fn)", "") == UNRESOLVED); + assert(body_plan("StmtLocal", "`var x` initialised with an untyped integer needs a type") == UNRESOLVED); + assert(body_plan("StmtAssign", "assignment through a constant") == UNRESOLVED); + assert(body_plan("ExprStructLit", "missing field `y` of `Pt`") == UNRESOLVED); + assert(body_plan("ExprStructLit", "`Duo` is u8, not a struct") == UNRESOLVED); + assert(body_plan("ExprFieldAccess", "`Pt` has no field `zzz`") == UNRESOLVED); + assert(body_plan("ExprLiteral(null)", "") == UNRESOLVED); + assert(body_plan("ExprLiteral(float literal)", "") == UNRESOLVED); + assert(body_plan("ExprIndex(tuple)", "") == UNRESOLVED); + assert(body_plan("type [N]T", "") == UNRESOLVED); +} + +test astgen_level_refusals_in_an_unreached_body_keep_their_refusal { + // Scope and syntax rules AstGen applies to every fn. + assert(body_plan("StmtBreak", "outside a loop") == KEEP); + assert(body_plan("StmtContinue", "outside a loop") == KEEP); + assert(body_plan("StmtLocal(shadows module var)", "") == KEEP); + assert(body_plan("StmtAssign(discard)", "") == KEEP); + assert(body_plan("ExprSwitch(two else prongs)", "") == KEEP); + assert(body_plan("ExprStructLit", "field `x` initialised twice") == KEEP); + // A literal Zig's lexer cannot read (`'\\0'` is no escape there); a null or a float t27b cannot type is Sema's. + assert(body_plan("ExprLiteral(char escape)", "") == KEEP); + assert(body_plan("ExprLiteral(char byte)", "") == KEEP); + assert(body_plan("ExprLiteral(char literal)", "") == KEEP); + assert(body_plan("ExprLiteral(string literal)", "") == KEEP); + assert(body_plan("ExprLiteral(literal)", "") == KEEP); + // A spelling t27c prints verbatim and Zig cannot parse. + assert(body_plan("ExprSwitch(char prong)", "") == KEEP); + assert(body_plan("type (tuple)", "`(u32, u32)`") == KEEP); + assert(body_plan("type [K:V]", "") == KEEP); + assert(body_plan("ExprTuple", "") == KEEP); + assert(body_plan("StmtAssign(tuple)", "") == KEEP); + assert(body_plan("StmtLocal(destructure)", "") == KEEP); + assert(body_plan("ExprArrayLiteral(text element)", "") == KEEP); + assert(body_plan("ExprArrayLiteral(text form)", "") == KEEP); +} + +test the_withdrawn_refusals_come_back_when_astgen_refuses_the_file { + // A name declared nowhere in any fn (the glue's check), whatever fn t27b withdrew. + assert(withdrawn_come_back(true, false, false)); + assert(withdrawn_come_back(true, true, true)); + // trios-algorithm-arena SR-ALG-00: no test, and a string `==` printed as `std.mem.eql`, with no `std` import. + assert(withdrawn_come_back(false, true, true)); + // A module with a test has `std`; with no string comparison there is nothing to print. + assert(!withdrawn_come_back(false, false, true)); + assert(!withdrawn_come_back(false, true, false)); + assert(!withdrawn_come_back(false, false, false)); +} + +test the_end_of_a_text_is_found { + assert(ends("field `x` initialised twice", "initialised twice")); + assert(ends("abc", "abc") and ends("abc", "c") and ends("abc", "")); + assert(!ends("abc", "abcd") and !ends("", "a") and !ends("initialised twice now", "initialised twice")); +} + +test the_prelude_names_exist_only_in_a_module_with_a_test { + // trios-algorithm-arena SR-ALG-00: a body naming `std.mem.eql` in a module with no test block, which t27c + // prints without the `std` import. + assert(zig_knows_in("std", true)); + assert(zig_knows_in("assert", true)); + assert(zig_knows_in("assert_eq", true)); + assert(!zig_knows_in("std", false)); + assert(!zig_knows_in("assert", false)); + assert(!zig_knows_in("assert_eq", false)); + // Builtins, primitive types and values need no prelude. + assert(zig_knows_in("@intCast", false)); + assert(zig_knows_in("u8", false)); + assert(zig_knows_in("undefined", false)); + assert(!zig_knows_in("std2", true)); + assert(!zig_knows_in("print", true)); +} From 5a3e19ebe8483bcaf7358603db10f89870c7c1eb Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 17:46:55 +0700 Subject: [PATCH 2/5] t27b: keep the refusals of an unreached body when AstGen refuses the file anyway -- invalid builtins and a string == in a module with no test (Closes #10897) Found by running t27c test-report on the five repository specs that the first commit turned from blocked to pass: three of them the reference cannot compile. - specs/tri/t27b/lazy_sig_plan.t27 `zig_builtin`: `@stdout` (specs/port/tools/check_graph_law8.t27) is "invalid builtin function" to AstGen in every fn, so `body_plan` keeps an `ExprCall(@name)` refusal unless Zig 0.16 has that builtin; `@floatCast`, `@floatFromInt`, `@max` and the rest of the list stay withdrawn. - specs/tri/t27b/ast_shape.t27 `string_eq`: every `==` / `!=` with a string literal operand, which t27c prints as `std.mem.eql` (specs/port/tools/check_json_parses.t27: `text.trim() == ""`); `restore_held` asks it besides `eql_used`, which `str_eq` now sets before its coercions (it was set only after they held, so a refused `==` never counted: SR-ALG-00 of trios-algorithm-arena). NOT built or run on this Mac: the disk alert forbade a cargo build, so only the t27 side was run (`t27c test-report`: lazy_sig_plan 16/16, ast_shape 14/14, none vacuous; seals saved and verified; gen files regenerated with `t27c gen-rust`). The Rust glue is one expression in restore_held. The first commit is the last one measured; CI builds this one. Hand-written lines in cli/t27b: unchanged (+70 -79, net -9). Co-Authored-By: Claude Sonnet 5.5 --- .trinity/seals/t27b_T27bAstShape.json | 18 +++---- .trinity/seals/t27b_T27bLazySigPlan.json | 16 +++---- cli/t27b/src/lower/unanalyzed.rs | 2 +- gen/rust/tri/t27b/ast_shape.rs | 21 +++++++- gen/rust/tri/t27b/lazy_sig_plan.rs | 17 ++++++- specs/tri/t27b/ast_shape.t27 | 61 +++++++++++++++++++++++- specs/tri/t27b/lazy_sig_plan.t27 | 27 ++++++++++- 7 files changed, 138 insertions(+), 24 deletions(-) diff --git a/.trinity/seals/t27b_T27bAstShape.json b/.trinity/seals/t27b_T27bAstShape.json index d7d2446b8f..3c03af0181 100644 --- a/.trinity/seals/t27b_T27bAstShape.json +++ b/.trinity/seals/t27b_T27bAstShape.json @@ -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", @@ -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": { diff --git a/.trinity/seals/t27b_T27bLazySigPlan.json b/.trinity/seals/t27b_T27bLazySigPlan.json index 10a59b3b53..9395f5df80 100644 --- a/.trinity/seals/t27b_T27bLazySigPlan.json +++ b/.trinity/seals/t27b_T27bLazySigPlan.json @@ -2,23 +2,23 @@ "built_by": "t27c-bootstrap@0.5.2+b258b9007", "closure": [], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:2ea83445e8b2bde258c000cdd93a62c812fa13ecb6f151e8ee16ef3808a32655", - "gen_hash_rust": "sha256:25adba263a266c8762cf05c59ffae70adbf5855a3fe796584cc8796eeab28984", - "gen_hash_verilog": "sha256:ef530c1ae9efdddcff900ac0e605d82d04b400a366a58b1c8c30d28ece9094b6", - "gen_hash_zig": "sha256:dba77f51ff91e0f065dfc704a060b9e33a8db0800633f9d833afe7758d8ce967", + "gen_hash_c": "sha256:a753f664e8a40e730f0a1fdeacc1579faf765d0f279100387479359b36ee26f2", + "gen_hash_rust": "sha256:7d5fe1d174e13091753a8c2262b402b1274ccbe37e9c6de38a760e7c9960460a", + "gen_hash_verilog": "sha256:df2d520ae32aeb23850224818b88fd9f62ccb73b577dcce8bb9e8d92754c8e23", + "gen_hash_zig": "sha256:a60c8a231a5137948057e6ecda429903399c70aa8d665befe2985c15fcb45a48", "module": "T27bLazySigPlan", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-19T09:10:01Z", + "sealed_at": "2026-10-19T09:46:54Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:669cd3569fa5f78e727f30971dd8642d10504a7406ac041cc480dbcff6dd9208", + "spec_hash": "sha256:1d24f1ab68c2b4a1dd1e50b40bf014e4756802e238bb745f5212172f44930771", "spec_path": "specs/tri/t27b/lazy_sig_plan.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 15, - "total": 15, + "passed": 16, + "total": 16, "vacuous": 0 }, "toolchain": { diff --git a/cli/t27b/src/lower/unanalyzed.rs b/cli/t27b/src/lower/unanalyzed.rs index d57753ffe2..70133dfcb2 100644 --- a/cli/t27b/src/lower/unanalyzed.rs +++ b/cli/t27b/src/lower/unanalyzed.rs @@ -66,7 +66,7 @@ impl<'a> Lower<'a> { /// 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) { + 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); } } diff --git a/gen/rust/tri/t27b/ast_shape.rs b/gen/rust/tri/t27b/ast_shape.rs index 2f62e1d0f7..711a4d0aef 100644 --- a/gen/rust/tri/t27b/ast_shape.rs +++ b/gen/rust/tri/t27b/ast_shape.rs @@ -1,6 +1,6 @@ // Generated from .t27 spec // DO NOT EDIT — generated by t27c -// NOT LOWERED BY THIS BACKEND: 13 test(s), 0 invariant(s), 0 function(s) with no body. +// NOT LOWERED BY THIS BACKEND: 14 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. @@ -248,6 +248,23 @@ pub fn reach_named(nodes: &[u8], text: &[u8], from: usize, to: usize, marks: &mu } } +pub fn string_literal(nodes: &[u8], text: &[u8], i: usize) -> bool { + return ((kind(nodes, i) == K_EXPR_LITERAL) && field_eq(nodes, text, i, F_KIND, "string")); +} + +pub fn string_eq(nodes: &[u8], text: &[u8], marks: &mut [u8]) -> () { + let mut i: usize = 0; + while (i < count(nodes)) { + if (((kind(nodes, i) == K_EXPR_BINARY) && (children(nodes, i) >= 2)) && (field_eq(nodes, text, i, F_OP, "==") || field_eq(nodes, text, i, F_OP, "!="))) { + let b: usize = (second_child(nodes, i) as _); + if (string_literal(nodes, text, (i + 1)) || string_literal(nodes, text, b)) { + marks[(i) as usize] = MARK; + } + } + i = (i + 1); + } +} + pub fn analyzed(nodes: &[u8], text: &[u8], marks: &mut [u8]) -> () { let mut r: usize = 0; while (r < count(nodes)) { @@ -279,7 +296,7 @@ pub fn analyzed(nodes: &[u8], text: &[u8], marks: &mut [u8]) -> () { } } -pub const SHAPE_TEXT: &'static str = "**-undefinedunion(enumunion (enumbodystringfgh 'a'x.ya:b u8"; +pub const SHAPE_TEXT: &'static str = "**-undefinedunion(enumunion (enumbodystringfgh 'a'x.ya:b u8==!="; pub const MARK: u8 = 1; diff --git a/gen/rust/tri/t27b/lazy_sig_plan.rs b/gen/rust/tri/t27b/lazy_sig_plan.rs index 4fe00860b7..8c84195975 100644 --- a/gen/rust/tri/t27b/lazy_sig_plan.rs +++ b/gen/rust/tri/t27b/lazy_sig_plan.rs @@ -1,6 +1,6 @@ // Generated from .t27 spec // DO NOT EDIT — generated by t27c -// NOT LOWERED BY THIS BACKEND: 15 test(s), 0 invariant(s), 0 function(s) with no body. +// NOT LOWERED BY THIS BACKEND: 16 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. @@ -16,6 +16,8 @@ pub const STAR: u8 = 42; pub const CLOSE: u8 = 93; +pub const CLOSE_PAREN: u8 = 41; + pub const SPACE: u8 = 32; pub const AT: u8 = 64; @@ -154,6 +156,10 @@ pub fn zig_knows_in(name: &[u8], prelude: bool) -> bool { return zig_knows(name); } +pub fn zig_builtin(name: &[u8]) -> bool { + return listed(name, b"@abs @addrSpaceCast @addWithOverflow @alignCast @alignOf @as @atomicLoad @atomicRmw @atomicStore @bitCast @bitOffsetOf @bitReverse @bitSizeOf @branchHint @breakpoint @byteSwap @call @cDefine @cImport @cInclude @ceil @clz @cmpxchgStrong @cmpxchgWeak @compileError @compileLog @constCast @cos @ctz @cUndef @cVaArg @cVaCopy @cVaEnd @cVaStart @disableInstrumentation @disableIntrinsics @divExact @divFloor @divTrunc @embedFile @Enum @EnumLiteral @enumFromInt @errorFromInt @errorName @errorReturnTrace @errSetCast @exp @exp2 @export @extern @field @fieldParentPtr @FieldType @floatCast @floatFromInt @floor @Fn @frameAddress @hasDecl @hasField @import @inComptime @Int @intCast @intFromBool @intFromEnum @intFromError @intFromFloat @intFromPtr @log @log10 @log2 @max @memcpy @memmove @memset @min @mod @mulAdd @mulWithOverflow @offsetOf @panic @Pointer @popCount @prefetch @ptrCast @ptrFromInt @reduce @rem @returnAddress @round @select @setEvalBranchQuota @setFloatMode @setRuntimeSafety @shlExact @shlWithOverflow @shrExact @shuffle @sin @sizeOf @splat @sqrt @src @Struct @subWithOverflow @tagName @tan @This @trap @trunc @truncate @Tuple @Type @typeInfo @typeName @TypeOf @Union @unionInit @Vector @volatileCast @wasmMemoryGrow @wasmMemorySize @workGroupId @workGroupSize @workItemId"); +} + pub fn ends(t: &[u8], word: &[u8]) -> bool { if (word.len() > t.len()) { return false; @@ -162,6 +168,15 @@ pub fn ends(t: &[u8], word: &[u8]) -> bool { } pub fn body_plan(construct: &[u8], detail: &[u8]) -> u8 { + if begins(construct, b"ExprCall(@") { + let mut e: usize = 9; + while ((e < construct.len()) && (construct[(e) as usize] != CLOSE_PAREN)) { + e = (e + 1); + } + if !(zig_builtin(&construct[(9) as usize..(e) as usize])) { + return KEEP; + } + } if (begins(construct, b"StmtBreak") || begins(construct, b"StmtContinue")) { return KEEP; } diff --git a/specs/tri/t27b/ast_shape.t27 b/specs/tri/t27b/ast_shape.t27 index d1d12edc7a..180e342b94 100644 --- a/specs/tri/t27b/ast_shape.t27 +++ b/specs/tri/t27b/ast_shape.t27 @@ -242,6 +242,25 @@ pub fn reach_named(nodes: []const u8, text: []const u8, from: usize, to: usize, } } +/// Is node i a string literal (`extra_kind == "string"`)? +pub fn string_literal(nodes: []const u8, text: []const u8, i: usize) -> bool { + return kind(nodes, i) == K_EXPR_LITERAL and field_eq(nodes, text, i, F_KIND, "string"); +} + +/// Every `==` and `!=` whose first or second operand is a string literal, in any fn: t27c prints it as +/// `std.mem.eql(u8, ..)` (compiler.rs, `ExprBinary`), which needs the `std` that t27c writes only into a module +/// with a test or an invariant (lazy_sig_plan.t27, `withdrawn_come_back`). +pub fn string_eq(nodes: []const u8, text: []const u8, marks: []u8) -> void { + var i : usize = 0; + while (i < count(nodes)) { + if (kind(nodes, i) == K_EXPR_BINARY and children(nodes, i) >= 2 and (field_eq(nodes, text, i, F_OP, "==") or field_eq(nodes, text, i, F_OP, "!="))) { + const b : usize = @intCast(second_child(nodes, i)); + if (string_literal(nodes, text, i + 1) or string_literal(nodes, text, b)) { marks[i] = MARK; } + } + i = i + 1; + } +} + /// The fns Zig's lazy analysis may reach in `zig test` (`analyzed_fns`): the roots are every top-level item /// that is not a fn (tests; invariants, which t27c emits as `comptime` blocks; constants and vars), and a /// fn is reached when a root, or the body of a reached fn, names it: any node under the root, or under the @@ -281,10 +300,10 @@ pub fn analyzed(nodes: []const u8, text: []const u8, marks: []u8) -> void { // same trees, built with ast_walk.t27's `node` and `set`. Their texts are slices of SHAPE_TEXT: // 0..2 `**`, 0..1 `*`, 2..3 `-`, 3..12 `undefined`, 12..22 `union(enum`, 22..33 `union (enum`, // 33..37 `body`, 37..43 `string`, 43 `f`, 44 `g`, 45 `h`, 46..50 ` 'a'`, 47..50 `'a'`, 48 `a`, -// 50..53 `x.y`, 50 `x`, 53..56 `a:b`, 56..58 two spaces, 58..60 `u8`. +// 50..53 `x.y`, 50 `x`, 53..56 `a:b`, 56..58 two spaces, 58..60 `u8`, 60..62 `==`, 62..64 `!=`. // ---------------------------------------------------------------------------------------------------------- -const SHAPE_TEXT : str = "**-undefinedunion(enumunion (enumbodystringfgh 'a'x.ya:b u8"; +const SHAPE_TEXT : str = "**-undefinedunion(enumunion (enumbodystringfgh 'a'x.ya:b u8==!="; // Shared fixture helpers come from ast_scan (#8756); no second bodies here. @@ -737,3 +756,41 @@ test analyzed_fns_are_named_by_a_root_or_by_a_reached_body { analyzed(ns, SHAPE_TEXT, m[0 .. 21]); assert ast_scan::mask(m[0 .. 21], 21) == 0; } + +test a_comparison_with_a_string_literal_is_marked { + // 0 Bin == { 1 Lit "string", 2 Ident }, 3 Bin != { 4 Ident, 5 Lit "string" }, 6 Bin == { 7 Ident, 8 Lit }, + // 9 Bin + { 10 Lit "string", 11 Ident }, 12 Bin == { 13 Lit "string" } + var buf : [1008]u8 = undefined; + var m : [14]u8 = undefined; + node(buf[0 .. 1008], 0, K_EXPR_BINARY, 3, 0, 0); + set(buf[0 .. 1008], 0, F_OP, 60, 62); + node(buf[0 .. 1008], 1, K_EXPR_LITERAL, 2, 0, 0); + set(buf[0 .. 1008], 1, F_KIND, 37, 43); + node(buf[0 .. 1008], 2, K_EXPR_IDENTIFIER, 3, 50, 51); + node(buf[0 .. 1008], 3, K_EXPR_BINARY, 6, 0, 0); + set(buf[0 .. 1008], 3, F_OP, 62, 64); + node(buf[0 .. 1008], 4, K_EXPR_IDENTIFIER, 5, 50, 51); + node(buf[0 .. 1008], 5, K_EXPR_LITERAL, 6, 0, 0); + set(buf[0 .. 1008], 5, F_KIND, 37, 43); + node(buf[0 .. 1008], 6, K_EXPR_BINARY, 9, 0, 0); + set(buf[0 .. 1008], 6, F_OP, 60, 62); + node(buf[0 .. 1008], 7, K_EXPR_IDENTIFIER, 8, 50, 51); + node(buf[0 .. 1008], 8, K_EXPR_LITERAL, 9, 0, 0); + node(buf[0 .. 1008], 9, K_EXPR_BINARY, 12, 0, 0); + set(buf[0 .. 1008], 9, F_OP, 58, 60); + node(buf[0 .. 1008], 10, K_EXPR_LITERAL, 11, 0, 0); + set(buf[0 .. 1008], 10, F_KIND, 37, 43); + node(buf[0 .. 1008], 11, K_EXPR_IDENTIFIER, 12, 50, 51); + node(buf[0 .. 1008], 12, K_EXPR_BINARY, 14, 0, 0); + set(buf[0 .. 1008], 12, F_OP, 60, 62); + node(buf[0 .. 1008], 13, K_EXPR_LITERAL, 14, 0, 0); + set(buf[0 .. 1008], 13, F_KIND, 37, 43); + ast_scan::clear(m[0 .. 14], 14); + string_eq(buf[0 .. 1008], SHAPE_TEXT, m[0 .. 14]); + // Marked: the first (literal first), the second (literal second). Not the third (no string), the fourth + // (`+`), nor the fifth (one operand only). + assert ast_scan::mask(m[0 .. 14], 14) == 9; + assert string_literal(buf[0 .. 1008], SHAPE_TEXT, 1); + assert !string_literal(buf[0 .. 1008], SHAPE_TEXT, 8); + assert !string_literal(buf[0 .. 1008], SHAPE_TEXT, 2); +} diff --git a/specs/tri/t27b/lazy_sig_plan.t27 b/specs/tri/t27b/lazy_sig_plan.t27 index 3d0cbea7a3..47e1bff2ed 100644 --- a/specs/tri/t27b/lazy_sig_plan.t27 +++ b/specs/tri/t27b/lazy_sig_plan.t27 @@ -41,6 +41,7 @@ const QUESTION : u8 = 63; const OPEN : u8 = 91; const STAR : u8 = 42; const CLOSE : u8 = 93; +const CLOSE_PAREN : u8 = 41; const SPACE : u8 = 32; const AT : u8 = 64; const LOWER_U : u8 = 117; @@ -160,6 +161,13 @@ pub fn zig_knows_in(name: []const u8, prelude: bool) -> bool { return zig_knows(name); } +/// `name` (with its `@`) is a builtin Zig 0.16 has. AstGen refuses any other (`@stdout`: "invalid builtin function") in +/// every fn, analyzed or not. t27b lacks some of these (`@floatCast`, `@max` on mixed operands); a fn nothing analyzed +/// reaches may call them, so those are withdrawn, and a name outside the list is kept. +pub fn zig_builtin(name: []const u8) -> bool { + return listed(name, "@abs @addrSpaceCast @addWithOverflow @alignCast @alignOf @as @atomicLoad @atomicRmw @atomicStore @bitCast @bitOffsetOf @bitReverse @bitSizeOf @branchHint @breakpoint @byteSwap @call @cDefine @cImport @cInclude @ceil @clz @cmpxchgStrong @cmpxchgWeak @compileError @compileLog @constCast @cos @ctz @cUndef @cVaArg @cVaCopy @cVaEnd @cVaStart @disableInstrumentation @disableIntrinsics @divExact @divFloor @divTrunc @embedFile @Enum @EnumLiteral @enumFromInt @errorFromInt @errorName @errorReturnTrace @errSetCast @exp @exp2 @export @extern @field @fieldParentPtr @FieldType @floatCast @floatFromInt @floor @Fn @frameAddress @hasDecl @hasField @import @inComptime @Int @intCast @intFromBool @intFromEnum @intFromError @intFromFloat @intFromPtr @log @log10 @log2 @max @memcpy @memmove @memset @min @mod @mulAdd @mulWithOverflow @offsetOf @panic @Pointer @popCount @prefetch @ptrCast @ptrFromInt @reduce @rem @returnAddress @round @select @setEvalBranchQuota @setFloatMode @setRuntimeSafety @shlExact @shlWithOverflow @shrExact @shuffle @sin @sizeOf @splat @sqrt @src @Struct @subWithOverflow @tagName @tan @This @trap @trunc @truncate @Tuple @Type @typeInfo @typeName @TypeOf @Union @unionInit @Vector @volatileCast @wasmMemoryGrow @wasmMemorySize @workGroupId @workGroupSize @workItemId"); +} + /// Does `t` end with `word`? fn ends(t: []const u8, word: []const u8) -> bool { if (word.len > t.len) { return false; } @@ -170,10 +178,15 @@ fn ends(t: []const u8, word: []const u8) -> bool { /// and detail: the refusal's name and words. UNRESOLVED: Zig meets the construct only in Sema, which never runs on /// this body, so the refusal is withdrawn. KEEP: AstGen or the parser refuses it in every fn (each pinned against /// `t27c test-report` on a fn no test reaches): `break` outside a loop; a local that shadows a module var; two -/// `else` prongs; a char prong, a char or string literal Zig cannot read; a struct field set twice; `_ = p;` of a +/// `else` prongs; a builtin Zig has not (`@stdout`); a char prong, a char or string literal Zig cannot read; a struct field set twice; `_ = p;` of a /// parameter the fn uses; and the spellings t27c prints verbatim that Zig cannot parse (a tuple, a map type, a /// tuple assignment, an array literal kept as text). pub fn body_plan(construct: []const u8, detail: []const u8) -> u8 { + if (begins(construct, "ExprCall(@")) { + var e : usize = 9; + while (e < construct.len and construct[e] != CLOSE_PAREN) { e = e + 1; } + if (!zig_builtin(construct[9 .. e])) { return KEEP; } + } if (begins(construct, "StmtBreak") or begins(construct, "StmtContinue")) { return KEEP; } if (begins(construct, "StmtLocal(shadows") or begins(construct, "StmtLocal(destructure")) { return KEEP; } if (begins(construct, "StmtAssign(discard") or begins(construct, "StmtAssign(tuple")) { return KEEP; } @@ -302,6 +315,8 @@ test sema_level_refusals_in_an_unreached_body_are_withdrawn { assert(body_plan("ExprArrayLiteral", "array literal with no result type") == UNRESOLVED); assert(body_plan("type mismatch", "expected u8, found u32") == UNRESOLVED); assert(body_plan("ExprCall(@floatFromInt)", "") == UNRESOLVED); + assert(body_plan("ExprCall(@floatCast)", "") == UNRESOLVED); + assert(body_plan("ExprCall(@max)", "") == UNRESOLVED); assert(body_plan("ExprUnary(try) in a fn", "") == UNRESOLVED); assert(body_plan("ExprReturn(undefined)", "") == UNRESOLVED); assert(body_plan("ExprCall(unresolved fn)", "") == UNRESOLVED); @@ -330,6 +345,9 @@ test astgen_level_refusals_in_an_unreached_body_keep_their_refusal { assert(body_plan("ExprLiteral(char literal)", "") == KEEP); assert(body_plan("ExprLiteral(string literal)", "") == KEEP); assert(body_plan("ExprLiteral(literal)", "") == KEEP); + // A builtin Zig does not have (check_graph_law8.t27's `@stdout`: "invalid builtin function"). + assert(body_plan("ExprCall(@stdout)", "call to `@stdout`") == KEEP); + assert(body_plan("ExprCall(@noSuch)", "") == KEEP); // A spelling t27c prints verbatim and Zig cannot parse. assert(body_plan("ExprSwitch(char prong)", "") == KEEP); assert(body_plan("type (tuple)", "`(u32, u32)`") == KEEP); @@ -375,3 +393,10 @@ test the_prelude_names_exist_only_in_a_module_with_a_test { assert(!zig_knows_in("std2", true)); assert(!zig_knows_in("print", true)); } + +test zig_has_these_builtins { + assert(zig_builtin("@floatFromInt") and zig_builtin("@floatCast") and zig_builtin("@max") and zig_builtin("@TypeOf")); + assert(zig_builtin("@intCast") and zig_builtin("@This") and zig_builtin("@workItemId") and zig_builtin("@abs")); + assert(!zig_builtin("@stdout") and !zig_builtin("floatCast") and !zig_builtin("@") and !zig_builtin("")); + assert(!zig_builtin("@floatCas") and !zig_builtin("@floatCasts") and !zig_builtin("@Max")); +} From fa19acf648009161e75742f003dcb8cc4040e10b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 18:00:48 +0700 Subject: [PATCH 3/5] t27b: lazy_sig_plan reuses literal_plan's ends_with instead of a private copy (Closes #10897) Review of #10999: the body plan's suffix test duplicated ends_with from specs/tri/t27b/literal_plan.t27, a module in the same directory. The two modules share no name, so the use splice pulls ends_with alone (#10556 does not bite). gen/rust regenerated, seal re-saved and verified; t27b test 16/16. Co-Authored-By: Claude Opus 5.5 --- .trinity/seals/t27b_T27bLazySigPlan.json | 19 ++++++++++++------- gen/rust/tri/t27b/lazy_sig_plan.rs | 23 +++++++++++++++-------- specs/tri/t27b/lazy_sig_plan.t27 | 16 ++++++---------- 3 files changed, 33 insertions(+), 25 deletions(-) diff --git a/.trinity/seals/t27b_T27bLazySigPlan.json b/.trinity/seals/t27b_T27bLazySigPlan.json index 9395f5df80..95eecd9896 100644 --- a/.trinity/seals/t27b_T27bLazySigPlan.json +++ b/.trinity/seals/t27b_T27bLazySigPlan.json @@ -1,17 +1,22 @@ { "built_by": "t27c-bootstrap@0.5.2+b258b9007", - "closure": [], + "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:a753f664e8a40e730f0a1fdeacc1579faf765d0f279100387479359b36ee26f2", - "gen_hash_rust": "sha256:7d5fe1d174e13091753a8c2262b402b1274ccbe37e9c6de38a760e7c9960460a", - "gen_hash_verilog": "sha256:df2d520ae32aeb23850224818b88fd9f62ccb73b577dcce8bb9e8d92754c8e23", - "gen_hash_zig": "sha256:a60c8a231a5137948057e6ecda429903399c70aa8d665befe2985c15fcb45a48", + "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-19T09:46:54Z", + "sealed_at": "2026-10-19T10:59:10Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:1d24f1ab68c2b4a1dd1e50b40bf014e4756802e238bb745f5212172f44930771", + "spec_hash": "sha256:864275b577659678b0208e8a1cddc4153c15daa50e437782b1e6a45d9a53e5fc", "spec_path": "specs/tri/t27b/lazy_sig_plan.t27", "tests": { "failed": 0, diff --git a/gen/rust/tri/t27b/lazy_sig_plan.rs b/gen/rust/tri/t27b/lazy_sig_plan.rs index 8c84195975..980ab18d85 100644 --- a/gen/rust/tri/t27b/lazy_sig_plan.rs +++ b/gen/rust/tri/t27b/lazy_sig_plan.rs @@ -160,13 +160,6 @@ pub fn zig_builtin(name: &[u8]) -> bool { return listed(name, b"@abs @addrSpaceCast @addWithOverflow @alignCast @alignOf @as @atomicLoad @atomicRmw @atomicStore @bitCast @bitOffsetOf @bitReverse @bitSizeOf @branchHint @breakpoint @byteSwap @call @cDefine @cImport @cInclude @ceil @clz @cmpxchgStrong @cmpxchgWeak @compileError @compileLog @constCast @cos @ctz @cUndef @cVaArg @cVaCopy @cVaEnd @cVaStart @disableInstrumentation @disableIntrinsics @divExact @divFloor @divTrunc @embedFile @Enum @EnumLiteral @enumFromInt @errorFromInt @errorName @errorReturnTrace @errSetCast @exp @exp2 @export @extern @field @fieldParentPtr @FieldType @floatCast @floatFromInt @floor @Fn @frameAddress @hasDecl @hasField @import @inComptime @Int @intCast @intFromBool @intFromEnum @intFromError @intFromFloat @intFromPtr @log @log10 @log2 @max @memcpy @memmove @memset @min @mod @mulAdd @mulWithOverflow @offsetOf @panic @Pointer @popCount @prefetch @ptrCast @ptrFromInt @reduce @rem @returnAddress @round @select @setEvalBranchQuota @setFloatMode @setRuntimeSafety @shlExact @shlWithOverflow @shrExact @shuffle @sin @sizeOf @splat @sqrt @src @Struct @subWithOverflow @tagName @tan @This @trap @trunc @truncate @Tuple @Type @typeInfo @typeName @TypeOf @Union @unionInit @Vector @volatileCast @wasmMemoryGrow @wasmMemorySize @workGroupId @workGroupSize @workItemId"); } -pub fn ends(t: &[u8], word: &[u8]) -> bool { - if (word.len() > t.len()) { - return false; - } - return begins(&t[((t.len() - word.len())) as usize..(t.len()) as usize], word); -} - pub fn body_plan(construct: &[u8], detail: &[u8]) -> u8 { if begins(construct, b"ExprCall(@") { let mut e: usize = 9; @@ -198,7 +191,7 @@ pub fn body_plan(construct: &[u8], detail: &[u8]) -> u8 { if (begins(construct, b"type (tuple") || begins(construct, b"type [K:V")) { return KEEP; } - if (begins(construct, b"ExprStructLit") && ends(detail, b"initialised twice")) { + if (begins(construct, b"ExprStructLit") && ends_with(detail, "initialised twice")) { return KEEP; } return UNRESOLVED; @@ -208,3 +201,17 @@ pub fn withdrawn_come_back(names_missing: bool, bare: bool, eql_used: bool) -> b return (names_missing || (bare && eql_used)); } +pub fn ends_with(t: &[u8], w: &'static str) -> bool { + if (t.len() < w.len()) { + return false; + } + let mut k: usize = 0; + while (k < w.len()) { + if (t[(((t.len() - w.len()) + k)) as usize] != w.as_bytes()[(k) as usize]) { + return false; + } + k = (k + 1); + } + return true; +} + diff --git a/specs/tri/t27b/lazy_sig_plan.t27 b/specs/tri/t27b/lazy_sig_plan.t27 index 47e1bff2ed..cffe0ecd2f 100644 --- a/specs/tri/t27b/lazy_sig_plan.t27 +++ b/specs/tri/t27b/lazy_sig_plan.t27 @@ -32,6 +32,8 @@ module T27bLazySigPlan; +use tri::t27b::literal_plan; // ends_with; no name here is also one of literal_plan's + // The answer. pub const KEEP : u8 = 0; pub const UNRESOLVED : u8 = 1; @@ -168,12 +170,6 @@ pub fn zig_builtin(name: []const u8) -> bool { return listed(name, "@abs @addrSpaceCast @addWithOverflow @alignCast @alignOf @as @atomicLoad @atomicRmw @atomicStore @bitCast @bitOffsetOf @bitReverse @bitSizeOf @branchHint @breakpoint @byteSwap @call @cDefine @cImport @cInclude @ceil @clz @cmpxchgStrong @cmpxchgWeak @compileError @compileLog @constCast @cos @ctz @cUndef @cVaArg @cVaCopy @cVaEnd @cVaStart @disableInstrumentation @disableIntrinsics @divExact @divFloor @divTrunc @embedFile @Enum @EnumLiteral @enumFromInt @errorFromInt @errorName @errorReturnTrace @errSetCast @exp @exp2 @export @extern @field @fieldParentPtr @FieldType @floatCast @floatFromInt @floor @Fn @frameAddress @hasDecl @hasField @import @inComptime @Int @intCast @intFromBool @intFromEnum @intFromError @intFromFloat @intFromPtr @log @log10 @log2 @max @memcpy @memmove @memset @min @mod @mulAdd @mulWithOverflow @offsetOf @panic @Pointer @popCount @prefetch @ptrCast @ptrFromInt @reduce @rem @returnAddress @round @select @setEvalBranchQuota @setFloatMode @setRuntimeSafety @shlExact @shlWithOverflow @shrExact @shuffle @sin @sizeOf @splat @sqrt @src @Struct @subWithOverflow @tagName @tan @This @trap @trunc @truncate @Tuple @Type @typeInfo @typeName @TypeOf @Union @unionInit @Vector @volatileCast @wasmMemoryGrow @wasmMemorySize @workGroupId @workGroupSize @workItemId"); } -/// Does `t` end with `word`? -fn ends(t: []const u8, word: []const u8) -> bool { - if (word.len > t.len) { return false; } - return begins(t[t.len - word.len .. t.len], word); -} - /// The decision for one refusal t27b recorded while lowering the body of a fn nothing analyzed reaches. construct /// and detail: the refusal's name and words. UNRESOLVED: Zig meets the construct only in Sema, which never runs on /// this body, so the refusal is withdrawn. KEEP: AstGen or the parser refuses it in every fn (each pinned against @@ -194,7 +190,7 @@ pub fn body_plan(construct: []const u8, detail: []const u8) -> u8 { if (begins(construct, "ExprLiteral(char") or begins(construct, "ExprLiteral(string") or begins(construct, "ExprLiteral(literal")) { return KEEP; } if (begins(construct, "ExprArrayLiteral(text") or begins(construct, "ExprTuple")) { return KEEP; } if (begins(construct, "type (tuple") or begins(construct, "type [K:V")) { return KEEP; } - if (begins(construct, "ExprStructLit") and ends(detail, "initialised twice")) { return KEEP; } + if (begins(construct, "ExprStructLit") and ends_with(detail, "initialised twice")) { return KEEP; } return UNRESOLVED; } @@ -372,9 +368,9 @@ test the_withdrawn_refusals_come_back_when_astgen_refuses_the_file { } test the_end_of_a_text_is_found { - assert(ends("field `x` initialised twice", "initialised twice")); - assert(ends("abc", "abc") and ends("abc", "c") and ends("abc", "")); - assert(!ends("abc", "abcd") and !ends("", "a") and !ends("initialised twice now", "initialised twice")); + assert(ends_with("field `x` initialised twice", "initialised twice")); + assert(ends_with("abc", "abc") and ends_with("abc", "c") and ends_with("abc", "")); + assert(!ends_with("abc", "abcd") and !ends_with("", "a") and !ends_with("initialised twice now", "initialised twice")); } test the_prelude_names_exist_only_in_a_module_with_a_test { From f69b6179faa5f6ab19880bb20e22e2f1b09f1b7a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 18:32:24 +0700 Subject: [PATCH 4/5] t27b: the lazy_signature fixture names its unit struct LazyUnit, not Gate -- Gate is crm-sellers' type and the types ratchet counted a conflict (Closes #10897) Co-Authored-By: Claude Sonnet 5.5 --- .../conformance_T27bConformanceLazySignature.json | 12 ++++++------ cli/t27b/tests/tail.rs | 6 +++--- specs/tri/t27b/conformance/lazy_signature.t27 | 14 +++++++------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.trinity/seals/conformance_T27bConformanceLazySignature.json b/.trinity/seals/conformance_T27bConformanceLazySignature.json index 7b08cef0cc..e5849d7f1a 100644 --- a/.trinity/seals/conformance_T27bConformanceLazySignature.json +++ b/.trinity/seals/conformance_T27bConformanceLazySignature.json @@ -2,16 +2,16 @@ "built_by": "t27c-bootstrap@0.5.2+b258b9007", "closure": [], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:645a2490c635acec56e01793336a579d0196153720d2da856106edb7846699cb", - "gen_hash_rust": "sha256:6aa15ee11052c06244259fa6b42dbcf0ece274d13e1a13ded38ba3fb10083547", - "gen_hash_verilog": "sha256:0b3f6b264f6f5c963c10422eaf3b48c6158e130713ea59e283212bb51ea84d19", - "gen_hash_zig": "sha256:6349038e39284199925a86167610194f5c5f49cc537e1188c2fceeae874302fd", + "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-19T09:10:46Z", + "sealed_at": "2026-10-19T11:31:38Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:1139bcf6bd224dff9b172a63a90810e39ba89bd4be4e88a0aacd31289a558c52", + "spec_hash": "sha256:fc31f992b370b61093b637072e3917bcfc0b39b3c18443aae22ba129b1a4a288", "spec_path": "specs/tri/t27b/conformance/lazy_signature.t27", "tests": { "failed": 0, diff --git a/cli/t27b/tests/tail.rs b/cli/t27b/tests/tail.rs index 68588a6312..b4ab16b090 100644 --- a/cli/t27b/tests/tail.rs +++ b/cli/t27b/tests/tail.rs @@ -376,7 +376,7 @@ fn a_discarded_value_runs_unless_the_reference_deletes_it() { /// Zig never resolves them, so the reference passes the file (specs/tri/t27b/lazy_sig_plan.t27; `t27c test-report` /// passes the conformance spec 3/3, none vacuous). A call from a test, an undeclared element type and an undeclared /// name in the withdrawn body keep their refusals, as the reference refuses each. The unit struct returned as a value -/// by `gate_new` (#10897) is refused as soon as a test calls it (BLOCKED, "expected type 'spec.Gate', found 'type'"). +/// by `lazy_unit_new` (#10897) is refused as soon as a test calls it (BLOCKED, "expected type 'spec.LazyUnit', found 'type'"). #[test] fn a_signature_zig_never_resolves() { let src = include_str!("../../../specs/tri/t27b/conformance/lazy_signature.t27"); @@ -386,8 +386,8 @@ fn a_signature_zig_never_resolves() { assert!(rejected(&src.replace("assert(issue_score(i) == 34);", "format_into(i, 5);")).contains("type anytype")); assert!(rejected(&src.replace("?[*]Issue", "?[*]Missing")).contains("type [*]T")); assert!(rejected(&src.replace("all: [*]const Issue", "all: [*]const Issue, m: Missing")).contains("type [*]T")); - assert!(rejected(&src.replace("== 34);", "== 34);\n gate_new();")).contains("unknown name `Gate`")); - assert!(common::lower_src(&src.replace("return gate_new();", "return gate_nowhere();")).unwrap_err().join("\n").contains("ExprCall(undeclared fn)")); + assert!(rejected(&src.replace("== 34);", "== 34);\n lazy_unit_new();")).contains("unknown name `LazyUnit`")); + assert!(common::lower_src(&src.replace("return lazy_unit_new();", "return lazy_unit_nowhere();")).unwrap_err().join("\n").contains("ExprCall(undeclared fn)")); let m = common::lower_src(&src.replace("f.write_num", "undeclared_thing")).unwrap_err().join("\n"); assert!(m.contains("ExprIdentifier(undeclared) at line 23 (`undeclared_thing` in `format_into`"), "{}", m); } diff --git a/specs/tri/t27b/conformance/lazy_signature.t27 b/specs/tri/t27b/conformance/lazy_signature.t27 index 7f744560e1..5850caaf66 100644 --- a/specs/tri/t27b/conformance/lazy_signature.t27 +++ b/specs/tri/t27b/conformance/lazy_signature.t27 @@ -64,16 +64,16 @@ test a_tie_goes_to_the_second { assert(issue_score(b) == 75); } -// The bodies (#10897): `gate_new` returns the unit struct `Gate` as a value, which Zig refuses ("expected type -// 'spec.Gate', found 'type'") only in a body it type-checks, so `gate_new` and `gate_default` compile while no test +// The bodies (#10897): `lazy_unit_new` returns the unit struct `LazyUnit` as a value, which Zig refuses ("expected type +// 'spec.LazyUnit', found 'type'") only in a body it type-checks, so `lazy_unit_new` and `lazy_unit_default` compile while no test // reaches them. It is the shape t27o writes for a Rust unit struct (trios-bridge BR-00). A test that calls either one, // or a call to a fn declared nowhere, makes the reference refuse the file (tests/tail.rs). -struct Gate {} +struct LazyUnit {} -fn gate_new() -> Gate { - return Gate; +fn lazy_unit_new() -> LazyUnit { + return LazyUnit; } -fn gate_default() -> Gate { - return gate_new(); +fn lazy_unit_default() -> LazyUnit { + return lazy_unit_new(); } From d11f776e737e3c3074fa4037562b7038dcde81ca Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 18:35:07 +0700 Subject: [PATCH 5/5] ledger(t27b): parser and deps_card pass now, notebooklm is blocked by its bench block, cap 129 -> 127 (Closes #10897) The lazy-body rule of this PR lets specs/compiler/parser.t27 and specs/t27f/deps_card.t27 pass under t27b (the ratchet read UNEXPECTED PASS for both) and moves notebooklm.t27's first blocker from "type mismatch" to BenchBlock (MOVED, a finding the ledger steward refuses). Rows and cap are recounted: 127 not-pass rows, cap 127. Same shape as #10862. Co-Authored-By: Claude Sonnet 5.5 --- docs/reports/t27b_expectations.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index a95ac49b63..7d016c9dc8 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -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": 129, + "max_not_pass": 127, "entries": [ {"path": "specs/a/b_c.t27", "t27b": "pass"}, {"path": "specs/a_b/c.t27", "t27b": "pass"}, @@ -246,7 +246,7 @@ {"path": "specs/compiler/linker.t27", "t27b": "pass"}, {"path": "specs/compiler/meta_compile.t27", "t27b": "pass"}, {"path": "specs/compiler/operator_text.t27", "t27b": "pass"}, - {"path": "specs/compiler/parser.t27", "t27b": "blocked", "blocker": "ExprCall(method)", "reason": "unimplemented"}, + {"path": "specs/compiler/parser.t27", "t27b": "pass"}, {"path": "specs/compiler/rust_assert_message.t27", "t27b": "pass_vacuous"}, {"path": "specs/compiler/rust_constant_case.t27", "t27b": "pass"}, {"path": "specs/compiler/rust_enum_order.t27", "t27b": "pass"}, @@ -540,7 +540,7 @@ {"path": "specs/memory/graph/temporal.t27", "t27b": "pass"}, {"path": "specs/memory/graph/types.t27", "t27b": "pass"}, {"path": "specs/memory/memory_primitives.t27", "t27b": "pass"}, - {"path": "specs/memory/notebooklm.t27", "t27b": "blocked", "blocker": "type mismatch", "reason": "unimplemented"}, + {"path": "specs/memory/notebooklm.t27", "t27b": "blocked", "blocker": "BenchBlock", "reason": "unimplemented"}, {"path": "specs/memory/tmem/bridge.t27", "t27b": "pass"}, {"path": "specs/memory/tmem/conformance.t27", "t27b": "pass"}, {"path": "specs/memory/tmem/edge_demo.t27", "t27b": "pass"}, @@ -1523,7 +1523,7 @@ {"path": "specs/system/unified-agent-operations.t27", "t27b": "pass"}, {"path": "specs/t27f/build.t27", "t27b": "pass"}, {"path": "specs/t27f/deck.t27", "t27b": "pass"}, - {"path": "specs/t27f/deps_card.t27", "t27b": "blocked", "blocker": "ExprCall(std.*)", "reason": "unimplemented"}, + {"path": "specs/t27f/deps_card.t27", "t27b": "pass"}, {"path": "specs/t27f/frame_cache.t27", "t27b": "pass"}, {"path": "specs/t27f/frame_shard.t27", "t27b": "blocked", "blocker": "type [N]T", "reason": "unimplemented"}, {"path": "specs/t27f/frontend.t27", "t27b": "pass"},