diff --git a/.trinity/seals/conformance_T27bConformanceLazyConst.json b/.trinity/seals/conformance_T27bConformanceLazyConst.json new file mode 100644 index 0000000000..7e988dbcd5 --- /dev/null +++ b/.trinity/seals/conformance_T27bConformanceLazyConst.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+b258b9007", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:1e01b01a5774a4f17d641c392abfc898e6b4e392ed5f7e55d737bfb9ac8f6bdf", + "gen_hash_rust": "sha256:b461506b690e7222d3f0d3ef69590ac682864a230fd8581b9a9bae5461941dd9", + "gen_hash_verilog": "sha256:cd59e58a1d7bec8e439c41fd713094e76bd8b50fff81efc5ab3a7950b92f67bf", + "gen_hash_zig": "sha256:6b8043cb91521a566655f3d95e75f93e4a6b73b3eaa2183d433ce63295188ae4", + "module": "T27bConformanceLazyConst", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-19T12:26:38Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:a0754e2b43945728b942b864b40cd89b7f72068cb2372540eaacc30111ee73a4", + "spec_path": "specs/tri/t27b/conformance/lazy_const.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 2, + "total": 2, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/conformance_T27bConformanceLazySignature.json b/.trinity/seals/conformance_T27bConformanceLazySignature.json index e5849d7f1a..56188a3f22 100644 --- a/.trinity/seals/conformance_T27bConformanceLazySignature.json +++ b/.trinity/seals/conformance_T27bConformanceLazySignature.json @@ -9,9 +9,9 @@ "module": "T27bConformanceLazySignature", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-19T11:31:38Z", + "sealed_at": "2026-10-19T15:26:06Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:fc31f992b370b61093b637072e3917bcfc0b39b3c18443aae22ba129b1a4a288", + "spec_hash": "sha256:a0b54d456fb8418dedcdfd83c66e7b3c563f6b8cf55c5a8b591e36294dab6fe1", "spec_path": "specs/tri/t27b/conformance/lazy_signature.t27", "tests": { "failed": 0, diff --git a/.trinity/seals/t27b_T27bAstShape.json b/.trinity/seals/t27b_T27bAstShape.json index 3c03af0181..cf1aebf494 100644 --- a/.trinity/seals/t27b_T27bAstShape.json +++ b/.trinity/seals/t27b_T27bAstShape.json @@ -11,23 +11,23 @@ } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:928f3d4e3daebf7d936f277297ae93d434cf27c0de7e401a629f3120db95d1a2", - "gen_hash_rust": "sha256:53db31308fcab46e8ac8ab03e978473e2f0292b639319ff03f5f3db7395e3b98", - "gen_hash_verilog": "sha256:1ba2d510ff7fb2a66b6af29af31cd3019f3790a1b23ab21ee44eb4183df3de99", - "gen_hash_zig": "sha256:4ae04819eacf8c61a60c5ee713cac0dc292581df3476cba6bdd80c480553cdbe", + "gen_hash_c": "sha256:d436ba26211e8f4b0bf0a42634f9036fc96c0327201ec7a875437f1cbf06a541", + "gen_hash_rust": "sha256:24b7e530c344a51633df34fc9f759a9958298143f3c00a8f8bd6959c16dc256c", + "gen_hash_verilog": "sha256:8bae3a3038c28a1cc571f6a754a9da76b5380cc4750c0c6b403368cb72979bf3", + "gen_hash_zig": "sha256:e7eff7053e5019ce8000f03963b1637df929e17f8c8a38e614ba6f90ab5ac907", "module": "T27bAstShape", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-19T09:47:24Z", + "sealed_at": "2026-10-19T14:55:05Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:0307a528afa66ef858b319d8f523dd02d0a6cdb274e027c89627f58e97716422", + "spec_hash": "sha256:16a9ea7c24f3552cb386c4e0dea10e1d806b641faa94a884c812a7d7ba66dd93", "spec_path": "specs/tri/t27b/ast_shape.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 14, - "total": 14, + "passed": 15, + "total": 15, "vacuous": 0 }, "toolchain": { diff --git a/.trinity/seals/t27b_T27bLazySigPlan.json b/.trinity/seals/t27b_T27bLazySigPlan.json index 95eecd9896..193d9ee4e2 100644 --- a/.trinity/seals/t27b_T27bLazySigPlan.json +++ b/.trinity/seals/t27b_T27bLazySigPlan.json @@ -7,23 +7,23 @@ } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:ead2c0e6a5434b5712c413c0d7665466dcb0bac06d9f110e01bcc8e8054990d9", - "gen_hash_rust": "sha256:020ab7da823994a4637c017477b01060efcc26ec0fbb1654c47303fac1529ba4", - "gen_hash_verilog": "sha256:f034de34241bcff76c6446e91d52706a79181b2156cebe314b1ffae41819a0e3", - "gen_hash_zig": "sha256:395e2cf2b97a16de07baa83feb06c4edb261ca985199712bf4ffa1afe2ac5e4c", + "gen_hash_c": "sha256:ccd92540be347fc5add77a5d9f9f7ffcf99d04e16576963b100f599ff1f33830", + "gen_hash_rust": "sha256:4e03980cb6ca2928e5d30b3e6860baad60f62838e43e1572e777692e12eb5507", + "gen_hash_verilog": "sha256:8060aed536a6d445489c2d2a0d144e2c38c3ad2cd88b923f1c406c24f6b5b1cf", + "gen_hash_zig": "sha256:5a986dff40ab7950b4a31a9386b81f23d51964bea5c785d06ef2ac98a3ac2aad", "module": "T27bLazySigPlan", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-19T10:59:10Z", + "sealed_at": "2026-10-19T14:56:37Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:864275b577659678b0208e8a1cddc4153c15daa50e437782b1e6a45d9a53e5fc", + "spec_hash": "sha256:325cfa203e2aff509bc1b8f77b96d8a03a91117580e7313314688fb1a26d8a71", "spec_path": "specs/tri/t27b/lazy_sig_plan.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 16, - "total": 16, + "passed": 19, + "total": 19, "vacuous": 0 }, "toolchain": { diff --git a/cli/t27b/src/lower.rs b/cli/t27b/src/lower.rs index 0bf73630e5..5b23909f90 100644 --- a/cli/t27b/src/lower.rs +++ b/cli/t27b/src/lower.rs @@ -770,12 +770,11 @@ fn lower_mode<'a>( l.nfuncs = next_id; let mut const_names: Vec = l.const_nodes.keys().cloned().collect(); const_names.sort(); - let hw: HashSet<&str> = marked(items.iter().copied(), vp::hw_consts).map(|n| n.name.as_str()).collect(); for name in const_names { - if l.unreferenced_tuple_const(&items, &name) || hw.contains(name.as_str()) || l.alias_target(&name).is_some() || wp::is_wide(l.const_nodes[&name].extra_type.trim().as_bytes()) { + if l.alias_target(&name).is_some() || wp::is_wide(l.const_nodes[&name].extra_type.trim().as_bytes()) { continue; } - let _ = l.global(&name); + l.eager_const(&name); } for node in std::mem::take(&mut l.var_nodes) { if l.module_var(node).is_err() { @@ -7339,7 +7338,6 @@ fn flat<'n>(ns: impl IntoIterator) -> (Vec, Vec) { } const _: () = assert!(NodeKind::StmtExpr as u8 == aw::K_STMT_EXPR && NodeKind::ExprRange as u8 == aw::K_EXPR_RANGE); #[path = "../../../gen/rust/tri/t27b/verilog_emit.rs"] #[allow(dead_code, unused_parens, unused_variables, while_true)] mod ve; // t27c gen-rust of specs/tri/t27b/verilog_emit.t27: `t27b verilog` (#8796) -#[path = "../../../gen/rust/tri/t27b/verilog_plan.rs"] #[allow(dead_code, unused_parens, unused_variables, while_true)] mod vp; // t27c gen-rust of specs/tri/t27b/verilog_plan.t27: hw_consts, the consts only the hardware reads #[path = "../../../gen/rust/tri/t27b/ts_emit.rs"] #[allow(dead_code, unused_parens, unused_variables, while_true)] mod tse; // specs/tri/t27b/ts_emit.t27: `t27b ts` (#9994) /// `t27b verilog` (`t27b ts` when `ts`): `flat`'s bytes and per node four u32 (flags 1 var, 2 pub; line; its params' span in `p`). pub fn verilog(ast: &Node, ts: bool) -> Result { diff --git a/cli/t27b/src/lower/arraylit.rs b/cli/t27b/src/lower/arraylit.rs index a28af54b87..f123291f66 100644 --- a/cli/t27b/src/lower/arraylit.rs +++ b/cli/t27b/src/lower/arraylit.rs @@ -15,10 +15,6 @@ //! use is `&x` as such an argument (one E for all) or `x.len`, it takes the //! same slice-local path, and `&x` passes the slice of it (`arg_as`). //! -//! An untyped module constant bound to a list is `.{ ... }`, a tuple; one -//! nothing names is never analyzed by the reference, so it is skipped -//! (`unreferenced_tuple_const`), not evaluated. -//! //! `return [ ... ];` (or `&[_]E{ ... }` for a const slice) in a fn returning a slice, or a slice field's literal, is //! `@constCast(&[_]E{ ... })` (`slice_lit`, plan `specs/tri/t27b/slice_lit_plan.t27`); one whose //! elements hold a str is a global written at entry (`entry_static`, #8091). @@ -28,17 +24,6 @@ use super::*; impl<'a> Lower<'a> { - /// An untyped module constant whose value is an array literal and whose - /// name no other node carries: Zig never analyzes it. - pub(super) fn unreferenced_tuple_const(&self, items: &[&Node], name: &str) -> bool { - let Some(node) = self.const_nodes.get(name) else { return false }; - let untyped_list = node.extra_type.trim().is_empty() - && node.children.len() == 1 - && node.children[0].kind == NodeKind::ExprArrayLiteral; - let named: usize = items.iter().map(|n| name_count(std::slice::from_ref(*n), name)).sum(); - untyped_list && named == 1 - } - /// Called from `begin_body`: every `const xs = &[_]T{ ... }`, and every /// `const x = [N]T{ ... }` used as `&x`, whose uses all fit becomes a /// slice local of the callees' element type. diff --git a/cli/t27b/src/lower/unanalyzed.rs b/cli/t27b/src/lower/unanalyzed.rs index 70133dfcb2..bda0cd1287 100644 --- a/cli/t27b/src/lower/unanalyzed.rs +++ b/cli/t27b/src/lower/unanalyzed.rs @@ -6,7 +6,7 @@ //! `fn f() undefined { undefined; }`; Zig would refuse that type ("expected //! type 'type', found '@TypeOf(undefined)'") only once something calls `f`. -use super::{Lower, Reject, R}; +use super::{Lower, Reject, Val, R}; use crate::compiler::{Node, NodeKind}; use crate::ir::{Expr, ExprKind, Stmt, Ty, TrapKind}; use std::collections::HashSet; @@ -66,11 +66,22 @@ 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 || super::marked(items.iter().copied(), super::sh::string_eq).next().is_some()) { + if !self.held.is_empty() && super::ls::withdrawn_come_back(items.iter().any(|f| matches!(f.kind, NodeKind::FnDecl | NodeKind::ConstDecl) && 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); } } + /// `global(name)` for the eager pass: a constant nothing analyzed reaches is lazy like a body (`analyzed`, `ls::const_plan`). + pub(super) fn eager_const(&mut self, name: &str) { + let (nerr, n): (usize, &Node) = (self.errors.len(), self.const_nodes[name]); + let _ = self.global(name); + let spelled = n.extra_type.trim().is_empty() || self.zig_spelled(n.extra_type.trim()); + if !self.analyzed.contains(name) && self.errors[nerr..].iter().all(|e| super::ls::const_plan(e.construct.as_bytes(), e.detail.as_bytes(), spelled) == super::ls::UNRESOLVED) { + let dropped = self.withdraw(nerr); self.held.extend(dropped); + self.globals.retain(|_, v| !matches!(v, Val::Poison)); // recovery mode: a reached use must report a withdrawn failure again + } + } + /// A refused signature type Zig resolves only when the fn is analyzed, as `layout` failures are /// (specs/tri/t27b/lazy_sig_plan.t27): `anytype` on a parameter, `[*]T` with T a type Zig reads here. pub(super) fn lazy_sig(&self, t: &str, param: bool) -> bool { @@ -101,12 +112,6 @@ impl<'a> Lower<'a> { /// reaches can make one, but the callee's parameter types are unknown /// here, so the call is refused rather than lowered. pub(super) fn unresolved_call(&mut self, c: &Node) -> R { - self.reject( - "ExprCall(unresolved fn)", - format!( - "call to `{}`, a fn no test reaches whose signature names a struct t27b cannot lay out", - c.name - ), - ) + self.reject("ExprCall(unresolved fn)", format!("call to `{}`, a fn no test reaches whose signature names a struct t27b cannot lay out", c.name)) } } diff --git a/cli/t27b/tests/tail.rs b/cli/t27b/tests/tail.rs index b4ab16b090..9f0ba2838d 100644 --- a/cli/t27b/tests/tail.rs +++ b/cli/t27b/tests/tail.rs @@ -388,10 +388,21 @@ fn a_signature_zig_never_resolves() { assert!(rejected(&src.replace("all: [*]const Issue", "all: [*]const Issue, m: Missing")).contains("type [*]T")); 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)")); + assert!(rejected(&src.replace("fn issue_score(", "fn lazy_local() -> u32 {\n var x : LazyMissing = 0;\n return 1;\n}\n\nfn issue_score(")).contains("type (undeclared)")); // #11053: t27c BLOCKED 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); } +/// #10899, #10903: a constant no analyzed code names is never resolved (conformance/lazy_const.t27; `t27c test-report` 2/2); a test naming the slice (directly or through a fn) or the call-initialised struct, and a constant naming a type, fn or value declared nowhere, keep the refusal. +#[test] +fn a_constant_zig_never_resolves() { + let src = include_str!("../../../specs/tri/t27b/conformance/lazy_const.t27"); + assert_eq!(names_ok(&run(src)).len(), 2); + for (from, to, want) in [("== 0x1100", "== 0x1100); assert(LAZY_ALLOWED.len == 2", "ConstDecl(slice) at"), ("== 0x1100", "== 0x1100); assert(lazy_allowed_count() == 2", "ConstDecl(slice) at"), ("== 0x1100", "== 0x1100); assert(lazy_bram_end() == 0x100000", "ConstDecl at"), ("[][]const u8", "[]LazyMissing", "type (undeclared)"), ("lazy_region_new(0, 0x100000)", "lazy_region_nowhere(0, 0x100000)", "ExprCall(undeclared fn)"), ("= LAZY_CSR;", "= LAZY_NOWHERE;", "ExprIdentifier(undeclared)")] { + assert!(common::lower_src(&src.replace(from, to)).unwrap_err().join("\n").contains(want), "{}", to); + } +} + // ------------------------------------------------ f32 @log, @log2 and @log10 /// #7822: of an f32 they call libm.t27's ports of compiler_rt `logf`, `log2f` and `log10f`, folded or not diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 4e02858cb5..731d30d0e3 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": 128, + "max_not_pass": 127, "entries": [ {"path": "specs/a/b_c.t27", "t27b": "pass"}, {"path": "specs/a_b/c.t27", "t27b": "pass"}, @@ -536,7 +536,7 @@ {"path": "specs/jobs/suite_ledger_steward.t27", "t27b": "pass"}, {"path": "specs/jobs/t27b_ledger_steward.t27", "t27b": "pass"}, {"path": "specs/lsp/protocol.t27", "t27b": "pass"}, - {"path": "specs/lsp/schema.t27", "t27b": "blocked", "blocker": "type mismatch", "reason": "unimplemented"}, + {"path": "specs/lsp/schema.t27", "t27b": "pass"}, {"path": "specs/math/constants.t27", "t27b": "pass"}, {"path": "specs/math/gf_competitive.t27", "t27b": "pass"}, {"path": "specs/math/property_test_template.t27", "t27b": "pass"}, diff --git a/gen/rust/tri/t27b/ast_shape.rs b/gen/rust/tri/t27b/ast_shape.rs index 711a4d0aef..568a463d58 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: 14 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. @@ -236,11 +236,28 @@ pub fn names_in(nodes: &[u8], text: &[u8], from: usize, to: usize, name: &[u8]) return false; } +pub fn mentioned_in(nodes: &[u8], text: &[u8], from: usize, to: usize, name: &[u8]) -> bool { + let mut j: usize = from; + while (j < to) { + if ((names(nodes, text, j, name) || field_word(nodes, text, j, F_SIZE, name)) || field_word(nodes, text, j, F_TYPE, name)) { + return true; + } + if (field_word(nodes, text, j, F_FIELD, name) || field_word(nodes, text, j, F_RET, name)) { + return true; + } + j = (j + 1); + } + return false; +} + pub fn reach_named(nodes: &[u8], text: &[u8], from: usize, to: usize, marks: &mut [u8]) -> () { let mut g: usize = 0; while (g < count(nodes)) { - if (((kind(nodes, g) == K_FN_DECL) && (marks[(g) as usize] == 0)) && (hi(nodes, g, F_NAME) > lo(nodes, g, F_NAME))) { - if names_in(nodes, text, from, to, &text[(lo(nodes, g, F_NAME)) as usize..(hi(nodes, g, F_NAME)) as usize]) { + if ((marks[(g) as usize] == 0) && (hi(nodes, g, F_NAME) > lo(nodes, g, F_NAME))) { + if ((kind(nodes, g) == K_FN_DECL) && names_in(nodes, text, from, to, &text[(lo(nodes, g, F_NAME)) as usize..(hi(nodes, g, F_NAME)) as usize])) { + marks[(g) as usize] = MARK; + } + if (((kind(nodes, g) == K_CONST_DECL) && !(is_hw_const(nodes, text, g, 0))) && mentioned_in(nodes, text, from, to, &text[(lo(nodes, g, F_NAME)) as usize..(hi(nodes, g, F_NAME)) as usize])) { marks[(g) as usize] = MARK; } } @@ -269,7 +286,7 @@ pub fn analyzed(nodes: &[u8], text: &[u8], marks: &mut [u8]) -> () { let mut r: usize = 0; while (r < count(nodes)) { let k: u8 = kind(nodes, r); - if ((((k != K_FN_DECL) && (k != K_BENCH_BLOCK)) && (k != K_STMT_EXPR)) && !(is_hw_const(nodes, text, r, 0))) { + if ((((k != K_FN_DECL) && (k != K_CONST_DECL)) && (k != K_BENCH_BLOCK)) && (k != K_STMT_EXPR)) { reach_named(nodes, text, r, end(nodes, r), marks); } r = end(nodes, r); @@ -279,9 +296,9 @@ pub fn analyzed(nodes: &[u8], text: &[u8], marks: &mut [u8]) -> () { again = false; let mut f: usize = 0; while (f < count(nodes)) { - if ((kind(nodes, f) == K_FN_DECL) && (marks[(f) as usize] == MARK)) { + if (((kind(nodes, f) == K_FN_DECL) || (kind(nodes, f) == K_CONST_DECL)) && (marks[(f) as usize] == MARK)) { marks[(f) as usize] = DONE; - reach_named(nodes, text, (f + 1), end(nodes, f), marks); + reach_named(nodes, text, f, end(nodes, f), marks); again = true; } f = end(nodes, f); @@ -327,12 +344,16 @@ pub const B_DOT: u8 = 46; pub const FIELDS: usize = 8; +pub const F_FIELD: usize = 3; + pub const F_KIND: usize = 5; pub const F_NAME: usize = 0; pub const F_OP: usize = 6; +pub const F_RET: usize = 7; + pub const F_SIZE: usize = 4; pub const F_TYPE: usize = 2; @@ -343,6 +364,8 @@ pub const K_BENCH_BLOCK: u8 = 9; pub const K_CONST_DECL: u8 = 2; +pub const K_ENUM_DECL: u8 = 3; + pub const K_EXPR_ARRAY_LITERAL: u8 = 22; pub const K_EXPR_BINARY: u8 = 16; @@ -385,6 +408,14 @@ pub const NO_NODE: i64 = -1; pub const REC: usize = 72; +pub fn bounded(s: &[u8], i: usize, e: usize) -> bool { + return (((i == 0) || !(word(s[((i - 1)) as usize]))) && ((e >= s.len()) || !(word(s[(e) as usize])))); +} + +pub fn char_start(c: u8) -> bool { + return ((c < 128) || (c >= 192)); +} + pub fn children(nodes: &[u8], i: usize) -> usize { let mut n: usize = 0; let mut c: usize = (i + 1); @@ -413,6 +444,10 @@ pub fn field_is(nodes: &[u8], text: &[u8], i: usize, f: usize, w: &[u8]) -> bool return (((b - a) == w.len()) && has_at(text, a, b, w)); } +pub fn field_word(nodes: &[u8], text: &[u8], i: usize, f: usize, name: &[u8]) -> bool { + return word_in(&text[(lo(nodes, i, f)) as usize..(hi(nodes, i, f)) as usize], name); +} + pub fn has_at(s: &[u8], lo: usize, hi: usize, w: &[u8]) -> bool { if ((hi - lo) < w.len()) { return false; @@ -443,6 +478,15 @@ pub fn lo(nodes: &[u8], i: usize, f: usize) -> usize { return u32_at(nodes, (((i * REC) + 8) + (f * 8))); } +pub fn names(nodes: &[u8], text: &[u8], i: usize, name: &[u8]) -> bool { + let a: usize = lo(nodes, i, F_NAME); + let b: usize = hi(nodes, i, F_NAME); + if !(has_at(text, a, b, name)) { + return false; + } + return (((b - a) == name.len()) || (text[((a + name.len())) as usize] == B_DOT)); +} + pub fn node(nodes: &mut [u8], i: usize, k: u8, e: usize, a: usize, b: usize) -> () { nodes[((i * REC)) as usize] = k; put32(nodes, ((i * REC) + 4), e); @@ -475,6 +519,28 @@ pub fn u32_at(nodes: &[u8], at: usize) -> usize { return (((b0 + (b1 * 256)) + (b2 * 65536)) + (b3 * 16777216)); } +pub fn word_in(text: &[u8], name: &[u8]) -> bool { + let mut i: usize = 0; + while (i <= text.len()) { + if (name.len() == 0) { + if (((i == text.len()) || char_start(text[(i) as usize])) && bounded(text, i, i)) { + return true; + } + i = (i + 1); + } else { + if has_at(text, i, text.len(), name) { + if bounded(text, i, (i + name.len())) { + return true; + } + i = (i + name.len()); + } else { + i = (i + 1); + } + } + } + return false; +} + pub const B_BANG: u8 = 33; pub const B_CR: u8 = 13; @@ -491,6 +557,8 @@ pub const B_STAR: u8 = 42; pub const B_TAB: u8 = 9; +pub const B_UNDERSCORE: u8 = 95; + pub const NONE: i64 = -1; pub const SHAPE_ANON_STRUCT: u8 = 7; @@ -513,6 +581,14 @@ pub const SHAPE_TUPLE: u8 = 3; pub const SHAPE_UNDECLARED: u8 = 9; +pub fn alpha(c: u8) -> bool { + return (((c >= 65) && (c <= 90)) || ((c >= 97) && (c <= 122))); +} + +pub fn digit(c: u8) -> bool { + return ((c >= 48) && (c <= 57)); +} + pub fn last_of(s: &[u8], lo: usize, hi: usize, b: u8) -> i64 { let mut i: usize = hi; while (i > lo) { @@ -591,6 +667,10 @@ pub fn trim_lo(s: &[u8], lo: usize, hi: usize) -> usize { return i; } +pub fn word(c: u8) -> bool { + return ((alpha(c) || digit(c)) || (c == B_UNDERSCORE)); +} + pub fn ws_at(s: &[u8], i: usize, hi: usize) -> usize { let c: u8 = s[(i) as usize]; if (((c >= B_TAB) && (c <= B_CR)) || (c == B_SPACE)) { diff --git a/gen/rust/tri/t27b/lazy_sig_plan.rs b/gen/rust/tri/t27b/lazy_sig_plan.rs index 980ab18d85..77a8f0267e 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: 16 test(s), 0 invariant(s), 0 function(s) with no body. +// NOT LOWERED BY THIS BACKEND: 19 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. @@ -170,6 +170,9 @@ pub fn body_plan(construct: &[u8], detail: &[u8]) -> u8 { return KEEP; } } + if begins(construct, b"type (undeclared") { + return KEEP; + } if (begins(construct, b"StmtBreak") || begins(construct, b"StmtContinue")) { return KEEP; } @@ -197,6 +200,16 @@ pub fn body_plan(construct: &[u8], detail: &[u8]) -> u8 { return UNRESOLVED; } +pub fn const_plan(construct: &[u8], detail: &[u8], spelled: bool) -> u8 { + if !(spelled) { + return KEEP; + } + if (begins(construct, b"ConstDecl") && ends_with(detail, "has no value")) { + return KEEP; + } + return body_plan(construct, detail); +} + 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/ast_shape.t27 b/specs/tri/t27b/ast_shape.t27 index 180e342b94..e77ad5a217 100644 --- a/specs/tri/t27b/ast_shape.t27 +++ b/specs/tri/t27b/ast_shape.t27 @@ -231,12 +231,26 @@ pub fn names_in(nodes: []const u8, text: []const u8, from: usize, to: usize, nam return false; } -/// Marks every unmarked top-level fn with a name that a node from `from` to `to` carries. +/// Does a node from `from` to `to` mention `name`: carry it, or a path through it (`X.len`), as its name, or hold it +/// as a word in its size, type, field or return-type text (array sizes and elements and annotations are kept as text)? +pub fn mentioned_in(nodes: []const u8, text: []const u8, from: usize, to: usize, name: []const u8) -> bool { + var j : usize = from; + while (j < to) { + if (names(nodes, text, j, name) or field_word(nodes, text, j, F_SIZE, name) or field_word(nodes, text, j, F_TYPE, name)) { return true; } + if (field_word(nodes, text, j, F_FIELD, name) or field_word(nodes, text, j, F_RET, name)) { return true; } + j = j + 1; + } + return false; +} + +/// Marks every unmarked top-level fn with a name that a node from `from` to `to` carries, and every unmarked +/// top-level const that one of them mentions. pub fn reach_named(nodes: []const u8, text: []const u8, from: usize, to: usize, marks: []u8) -> void { var g : usize = 0; while (g < count(nodes)) { - if (kind(nodes, g) == K_FN_DECL and marks[g] == 0 and hi(nodes, g, F_NAME) > lo(nodes, g, F_NAME)) { - if (names_in(nodes, text, from, to, text[lo(nodes, g, F_NAME) .. hi(nodes, g, F_NAME)])) { marks[g] = MARK; } + if (marks[g] == 0 and hi(nodes, g, F_NAME) > lo(nodes, g, F_NAME)) { + if (kind(nodes, g) == K_FN_DECL and names_in(nodes, text, from, to, text[lo(nodes, g, F_NAME) .. hi(nodes, g, F_NAME)])) { marks[g] = MARK; } + if (kind(nodes, g) == K_CONST_DECL and !is_hw_const(nodes, text, g, 0) and mentioned_in(nodes, text, from, to, text[lo(nodes, g, F_NAME) .. hi(nodes, g, F_NAME)])) { marks[g] = MARK; } } g = end(nodes, g); } @@ -261,18 +275,21 @@ pub fn string_eq(nodes: []const u8, text: []const u8, marks: []u8) -> void { } } -/// 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 -/// fn, carries its name. `pub` does not make a fn a root, nor does `main`. A bench is not a root: t27c -/// emits it as `fn bench_() void`, which nothing calls. Nor is a top-level statement, which t27c does -/// not emit at all. Nor is a const only the hardware reads (verilog_plan.t27's is_hw_const: an instance of an -/// extern module, or a net built on one), which no test reaches (#8796). A fn with no name is never reached. +/// What Zig's lazy analysis may reach in `zig test` (`analyzed_fns`): the roots are every top-level item that is +/// neither a fn nor a const (tests; invariants, which t27c emits as `comptime` blocks; struct and enum +/// declarations), and a fn or a const is reached when a root, or a reached fn or const, names it: any node under +/// it carries the fn's name, or mentions the const (`mentioned_in`). A constant is lazy like a fn (#10899: a +/// `pub const` nothing analyzed names is never resolved, whatever it is initialised with), so it is no root; +/// nor is a var, which has the same kind. `pub` makes nothing a root, nor does `main`. A bench is not a +/// root: t27c emits it as `fn bench_() void`, which nothing calls. Nor is a top-level statement, which +/// t27c does not emit at all. Nor is a const only the hardware reads (verilog_plan.t27's is_hw_const: an instance of +/// an extern module, or a net built on one) ever reached, whoever names it: the CPU path never lowers one (#8796). An +/// item with no name is never reached. pub fn analyzed(nodes: []const u8, text: []const u8, marks: []u8) -> void { var r : usize = 0; while (r < count(nodes)) { const k : u8 = kind(nodes, r); - if (k != K_FN_DECL and k != K_BENCH_BLOCK and k != K_STMT_EXPR and !is_hw_const(nodes, text, r, 0)) { reach_named(nodes, text, r, end(nodes, r), marks); } + if (k != K_FN_DECL and k != K_CONST_DECL and k != K_BENCH_BLOCK and k != K_STMT_EXPR) { reach_named(nodes, text, r, end(nodes, r), marks); } r = end(nodes, r); } var again : bool = true; @@ -280,9 +297,9 @@ pub fn analyzed(nodes: []const u8, text: []const u8, marks: []u8) -> void { again = false; var f : usize = 0; while (f < count(nodes)) { - if (kind(nodes, f) == K_FN_DECL and marks[f] == MARK) { + if ((kind(nodes, f) == K_FN_DECL or kind(nodes, f) == K_CONST_DECL) and marks[f] == MARK) { marks[f] = DONE; - reach_named(nodes, text, f + 1, end(nodes, f), marks); + reach_named(nodes, text, f, end(nodes, f), marks); again = true; } f = end(nodes, f); @@ -683,7 +700,7 @@ test strength_reduction_reaches_muls_from_top_level_statements_of_fn_bodies { assert ast_scan::mask(m[0 .. 23], 23) == 516; } -test analyzed_fns_are_named_by_a_root_or_by_a_reached_body { +test analyzed_items_are_named_by_a_root_or_by_a_reached_item { // 0 Test { 1 StmtExpr { 2 Call f } }, 3 Fn f { 4 Module body { 5 StmtExpr { 6 Call g } } }, 7 Fn g { 8 Ident h }, // 9 Fn h, 10 Fn x { 11 Ident f }, 12 Bench { 13 Call x }, 14 StmtExpr { 15 Call x }, 16 Fn g { 17 Ident u8 }, // 18 Fn ``, 19 Const u8 { 20 Ident h } @@ -713,35 +730,30 @@ test analyzed_fns_are_named_by_a_root_or_by_a_reached_body { const ns : []const u8 = buf[0 .. 1512]; ast_scan::clear(m[0 .. 21], 21); analyzed(ns, SHAPE_TEXT, m[0 .. 21]); - assert ast_scan::mask(m[0 .. 21], 21) == 66184; - // A bench is no root: f, and g through it, are gone; h stays through the constant + // f by the test, g and h through f, the second g with the first, and the const u8 through the second g's body + assert ast_scan::mask(m[0 .. 21], 21) == 590472; + // A bench is no root, and nor is a const: f, and g through it, are gone, and the const that names h does not reach it node(buf[0 .. 1512], 0, K_BENCH_BLOCK, 3, 0, 0); ast_scan::clear(m[0 .. 21], 21); analyzed(ns, SHAPE_TEXT, m[0 .. 21]); - assert ast_scan::mask(m[0 .. 21], 21) == 512; - // A const that calls a fn with an empty body (an instance of an extern module, verilog_plan.t27) is no root - node(buf[0 .. 1512], 20, K_EXPR_CALL, 21, 45, 46); - ast_scan::clear(m[0 .. 21], 21); - analyzed(ns, SHAPE_TEXT, m[0 .. 21]); assert ast_scan::mask(m[0 .. 21], 21) == 0; - node(buf[0 .. 1512], 20, K_EXPR_IDENTIFIER, 21, 45, 46); - // A root's own name counts: a constant named x reaches fn x, whose body names f - node(buf[0 .. 1512], 19, K_CONST_DECL, 21, 50, 51); + // A root's own name counts: an enum named x reaches fn x, whose body names f + node(buf[0 .. 1512], 19, K_ENUM_DECL, 21, 50, 51); + node(buf[0 .. 1512], 0, K_TEST_BLOCK, 3, 0, 0); ast_scan::clear(m[0 .. 21], 21); analyzed(ns, SHAPE_TEXT, m[0 .. 21]); assert ast_scan::mask(m[0 .. 21], 21) == 67208; node(buf[0 .. 1512], 19, K_CONST_DECL, 21, 58, 60); - node(buf[0 .. 1512], 0, K_TEST_BLOCK, 3, 0, 0); // An invariant is a root; a top-level statement is not, but any other kind is node(buf[0 .. 1512], 12, K_INVARIANT_BLOCK, 14, 0, 0); ast_scan::clear(m[0 .. 21], 21); analyzed(ns, SHAPE_TEXT, m[0 .. 21]); - assert ast_scan::mask(m[0 .. 21], 21) == 67208; + assert ast_scan::mask(m[0 .. 21], 21) == 591496; node(buf[0 .. 1512], 12, K_BENCH_BLOCK, 14, 0, 0); node(buf[0 .. 1512], 14, K_STMT_LOCAL, 16, 0, 0); ast_scan::clear(m[0 .. 21], 21); analyzed(ns, SHAPE_TEXT, m[0 .. 21]); - assert ast_scan::mask(m[0 .. 21], 21) == 67208; + assert ast_scan::mask(m[0 .. 21], 21) == 591496; node(buf[0 .. 1512], 14, K_STMT_EXPR, 16, 0, 0); // The second g reaches what its body names once g is reached node(buf[0 .. 1512], 17, K_EXPR_IDENTIFIER, 18, 50, 51); @@ -755,6 +767,70 @@ test analyzed_fns_are_named_by_a_root_or_by_a_reached_body { ast_scan::clear(m[0 .. 21], 21); analyzed(ns, SHAPE_TEXT, m[0 .. 21]); assert ast_scan::mask(m[0 .. 21], 21) == 0; + node(buf[0 .. 1512], 19, K_CONST_DECL, 21, 58, 60); + node(buf[0 .. 1512], 0, K_TEST_BLOCK, 3, 0, 0); +} + +test a_const_is_reached_by_a_name_a_path_or_a_type_text { + // The tree of the test above, a Test root and the second g { 17 Ident } reaching the const u8 { 20 Ident h }. + var buf : [1512]u8 = undefined; + var m : [21]u8 = undefined; + node(buf[0 .. 1512], 0, K_TEST_BLOCK, 3, 0, 0); + node(buf[0 .. 1512], 1, K_STMT_EXPR, 3, 0, 0); + node(buf[0 .. 1512], 2, K_EXPR_CALL, 3, 43, 44); + node(buf[0 .. 1512], 3, K_FN_DECL, 7, 43, 44); + node(buf[0 .. 1512], 4, K_MODULE, 7, 33, 37); + node(buf[0 .. 1512], 5, K_STMT_EXPR, 7, 0, 0); + node(buf[0 .. 1512], 6, K_EXPR_CALL, 7, 44, 45); + node(buf[0 .. 1512], 7, K_FN_DECL, 9, 44, 45); + node(buf[0 .. 1512], 8, K_EXPR_IDENTIFIER, 9, 45, 46); + node(buf[0 .. 1512], 9, K_FN_DECL, 10, 45, 46); + node(buf[0 .. 1512], 10, K_FN_DECL, 12, 52, 53); + node(buf[0 .. 1512], 11, K_EXPR_IDENTIFIER, 12, 43, 44); + node(buf[0 .. 1512], 12, K_BENCH_BLOCK, 14, 0, 0); + node(buf[0 .. 1512], 13, K_EXPR_CALL, 14, 50, 51); + node(buf[0 .. 1512], 14, K_STMT_EXPR, 16, 0, 0); + node(buf[0 .. 1512], 15, K_EXPR_CALL, 16, 50, 51); + node(buf[0 .. 1512], 16, K_FN_DECL, 18, 44, 45); + node(buf[0 .. 1512], 17, K_EXPR_IDENTIFIER, 18, 58, 60); + node(buf[0 .. 1512], 18, K_FN_DECL, 19, 0, 0); + node(buf[0 .. 1512], 19, K_CONST_DECL, 21, 58, 60); + node(buf[0 .. 1512], 20, K_EXPR_IDENTIFIER, 21, 45, 46); + const ns : []const u8 = buf[0 .. 1512]; + // A path through the const's name reaches it: a const x, reached by `x.y` in the second g (the fn is named y) + node(buf[0 .. 1512], 19, K_CONST_DECL, 21, 50, 51); + node(buf[0 .. 1512], 17, K_EXPR_IDENTIFIER, 18, 50, 53); + ast_scan::clear(m[0 .. 21], 21); + analyzed(ns, SHAPE_TEXT, m[0 .. 21]); + assert ast_scan::mask(m[0 .. 21], 21) == 590472; + // The same text in a type, not a name: reached too + node(buf[0 .. 1512], 17, K_EXPR_IDENTIFIER, 18, 0, 0); + set(buf[0 .. 1512], 17, F_TYPE, 50, 53); + ast_scan::clear(m[0 .. 21], 21); + analyzed(ns, SHAPE_TEXT, m[0 .. 21]); + assert ast_scan::mask(m[0 .. 21], 21) == 590472; + // Nothing names the const x any more: it is out + set(buf[0 .. 1512], 17, F_TYPE, 0, 0); + node(buf[0 .. 1512], 17, K_EXPR_IDENTIFIER, 18, 45, 46); + ast_scan::clear(m[0 .. 21], 21); + analyzed(ns, SHAPE_TEXT, m[0 .. 21]); + assert ast_scan::mask(m[0 .. 21], 21) == 66184; + // A const that calls a fn with an empty body (an instance of an extern module, verilog_plan.t27) is never reached + node(buf[0 .. 1512], 17, K_EXPR_IDENTIFIER, 18, 58, 60); + node(buf[0 .. 1512], 19, K_CONST_DECL, 21, 58, 60); + node(buf[0 .. 1512], 20, K_EXPR_CALL, 21, 45, 46); + ast_scan::clear(m[0 .. 21], 21); + analyzed(ns, SHAPE_TEXT, m[0 .. 21]); + assert ast_scan::mask(m[0 .. 21], 21) == 66184; + node(buf[0 .. 1512], 20, K_EXPR_IDENTIFIER, 21, 45, 46); + // A const reached by a const: a named by the const u8, which the second g reaches + node(buf[0 .. 1512], 17, K_EXPR_IDENTIFIER, 18, 58, 60); + node(buf[0 .. 1512], 19, K_CONST_DECL, 21, 58, 60); + node(buf[0 .. 1512], 18, K_CONST_DECL, 19, 48, 49); + node(buf[0 .. 1512], 20, K_EXPR_IDENTIFIER, 21, 48, 49); + ast_scan::clear(m[0 .. 21], 21); + analyzed(ns, SHAPE_TEXT, m[0 .. 21]); + assert ast_scan::mask(m[0 .. 21], 21) == 852616; } test a_comparison_with_a_string_literal_is_marked { diff --git a/specs/tri/t27b/conformance/lazy_const.t27 b/specs/tri/t27b/conformance/lazy_const.t27 new file mode 100644 index 0000000000..ef90aaf41c --- /dev/null +++ b/specs/tri/t27b/conformance/lazy_const.t27 @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/tri/t27b/conformance/lazy_const.t27 -- module constants no test reaches, as the reference runs them (t27o epic +; #10879, #10899 and #10903). Every test here passes `t27c test-report`; t27b must give the same verdict on each, or +; refuse the file. The plan is specs/tri/t27b/lazy_sig_plan.t27 (`const_plan`), the reach is ast_shape.t27's `analyzed`. +; Zig resolves a module-level constant only when something analyzed names it. `LAZY_ALLOWED` is a slice written as +; the array literal t27o writes (catalog_gate.t27's `ALLOWED`; Zig: "type '[][]const u8' does not support array +; initialization syntax" once it is read), `LAZY_BRAM` and `LAZY_CSR` are struct constants initialised by a call +; (addrmap.t27's `REGION_BRAM`; t27b cannot call a fn at compile time), and `LAZY_BOTH` names the second one. Only a +; fn no test calls names a constant, so none of them is analyzed and the file compiles. +; Not here (the reference cannot compile it, or t27b cannot yet): a test that names `LAZY_ALLOWED`, directly or through +; `lazy_allowed_count`, which makes Zig read the slice; a test that names `LAZY_BRAM`, which Zig evaluates at compile +; time and passes, so t27b refuses it by name (`ConstDecl`, a counted remainder: the reached form needs a compile-time +; call); a constant whose type or initializer names something declared nowhere, which AstGen refuses in every file. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module T27bConformanceLazyConst; + +struct LazyRegion { + base: u64, + size: u64, +} + +fn lazy_region_new(base: u64, size: u64) -> LazyRegion { + return LazyRegion{ base: base, size: size }; +} + +fn lazy_region_end(r: LazyRegion) -> u64 { + return r.base + r.size; +} + +fn lazy_region_overlaps(a: LazyRegion, b: LazyRegion) -> bool { + return a.base < lazy_region_end(b) and b.base < lazy_region_end(a); +} + +pub const LAZY_ALLOWED : [][]const u8 = ["alpha", "beta"]; + +pub const LAZY_BRAM : LazyRegion = lazy_region_new(0, 0x100000); + +pub const LAZY_CSR : LazyRegion = lazy_region_new(0x40000000, 0x1000); + +pub const LAZY_BOTH : LazyRegion = LAZY_CSR; + +fn lazy_allowed_count() -> usize { + return LAZY_ALLOWED.len; +} + +fn lazy_bram_end() -> u64 { + return lazy_region_end(LAZY_BRAM); +} + +test a_region_ends_where_its_size_runs_out { + const r = lazy_region_new(0x1000, 0x100); + assert(lazy_region_end(r) == 0x1100); +} + +test overlapping_regions_are_found { + const a = lazy_region_new(0x1000, 0x100); + const b = lazy_region_new(0x1050, 0x100); + const c = lazy_region_new(0x2000, 0x100); + assert(lazy_region_overlaps(a, b)); + assert(!lazy_region_overlaps(a, c)); +} diff --git a/specs/tri/t27b/conformance/lazy_signature.t27 b/specs/tri/t27b/conformance/lazy_signature.t27 index 5850caaf66..7890c5c712 100644 --- a/specs/tri/t27b/conformance/lazy_signature.t27 +++ b/specs/tri/t27b/conformance/lazy_signature.t27 @@ -9,8 +9,8 @@ ; mirrors are trios-scarab-types SR-00 and SR-02 (`Term_fmt(self, f: anytype)`) and trios-cli gh.t27 ; (`GhClient_list_agent_issues(..) -> ?[*]AgentIssue`). A mutant of those three bodies survives by ; construction: no test reaches them, which is the point of the spec. -; Not here (the reference cannot compile it): a test that calls `format_into`, which makes Zig look up -; `write_str` on the argument's type, and `[*]Missing`, an undeclared name AstGen refuses in every fn. +; Not here (the reference cannot compile it): a test that calls `format_into` (Zig looks up `write_str` on the argument's +; type), `[*]Missing` or a local of a type declared nowhere (#11053): AstGen refuses an undeclared name in every fn. ; phi^2 + 1/phi^2 = 3 | TRINITY module T27bConformanceLazySignature; diff --git a/specs/tri/t27b/lazy_sig_plan.t27 b/specs/tri/t27b/lazy_sig_plan.t27 index cffe0ecd2f..9b52081b77 100644 --- a/specs/tri/t27b/lazy_sig_plan.t27 +++ b/specs/tri/t27b/lazy_sig_plan.t27 @@ -27,6 +27,16 @@ ; 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). +; -- +; The constant (t27o epic #10879, #10899, #10903). A module-level `const` is as lazy as a fn: Zig resolves it only when +; something analyzed names it, so `pub const ALLOWED : [][]const u8 = ["gfternary"];` (Zig: "type '[][]const u8' does +; not support array initialization syntax" the moment it is read) and `pub const REGION : Region = Region_new(..);` +; (t27b cannot call a fn at compile time) compile while nothing reaches them, and the reference passes the file. ast_shape.t27's `analyzed` now +; marks the constants a test, an invariant or a reached fn or constant mentions, and a constant outside it that t27b +; refuses is judged by `const_plan`: the body rule, but an annotation Zig cannot read in this file keeps the refusal. The +; refusal comes back, with a body's, when AstGen refuses the file anyway (`withdrawn_come_back`). A constant a test +; reaches keeps it (pinned by conformance/lazy_const.t27: t27c passes the call-initialised struct constant, which Zig +; evaluates at compile time, and refuses the slice; t27b refuses both, the first a counted remainder). ; ASCII only (L3) ; phi^2 + 1/phi^2 = 3 | TRINITY @@ -174,7 +184,7 @@ pub fn zig_builtin(name: []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 builtin Zig has not (`@stdout`); a char prong, a char or string literal Zig cannot read; a struct field set twice; `_ = p;` of a +/// `else` prongs; a type declared nowhere (`var x : Missing = 0;`); 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 { @@ -183,6 +193,7 @@ pub fn body_plan(construct: []const u8, detail: []const u8) -> u8 { while (e < construct.len and construct[e] != CLOSE_PAREN) { e = e + 1; } if (!zig_builtin(construct[9 .. e])) { return KEEP; } } + if (begins(construct, "type (undeclared")) { 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; } @@ -194,6 +205,16 @@ pub fn body_plan(construct: []const u8, detail: []const u8) -> u8 { return UNRESOLVED; } +/// The decision for one refusal t27b recorded while lowering a module constant nothing analyzed reaches (`analyzed`). +/// The constant is as lazy as a body, so `body_plan` decides; `spelled` is whether its annotation, if it has one, is a +/// type Zig reads in this file: AstGen resolves the type of a declaration whether or not it is analyzed, so a type +/// declared nowhere (`pub const X : Missing = ..;`) keeps the refusal, as does a constant with no value, which does not parse. +pub fn const_plan(construct: []const u8, detail: []const u8, spelled: bool) -> u8 { + if (!spelled) { return KEEP; } + if (begins(construct, "ConstDecl") and ends_with(detail, "has no value")) { return KEEP; } + return body_plan(construct, detail); +} + /// 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` @@ -328,7 +349,11 @@ test sema_level_refusals_in_an_unreached_body_are_withdrawn { } test astgen_level_refusals_in_an_unreached_body_keep_their_refusal { - // Scope and syntax rules AstGen applies to every fn. + // Scope and syntax rules AstGen applies to every fn. A local of a type declared nowhere is one (#11053): `fn f() { var x : Missing = 0; }` + // is "use of undeclared identifier 'Missing'" whether or not a test calls f. + assert(body_plan("type (undeclared)", "`Missing`") == KEEP); + assert(const_plan("type (undeclared)", "`Missing`", true) == KEEP); + assert(body_plan("type []T", "`[]u8`") == UNRESOLVED and body_plan("type [N]T", "") == UNRESOLVED); assert(body_plan("StmtBreak", "outside a loop") == KEEP); assert(body_plan("StmtContinue", "outside a loop") == KEEP); assert(body_plan("StmtLocal(shadows module var)", "") == KEEP); @@ -355,6 +380,38 @@ test astgen_level_refusals_in_an_unreached_body_keep_their_refusal { assert(body_plan("ExprArrayLiteral(text form)", "") == KEEP); } +test sema_level_refusals_in_a_constant_nothing_reaches_are_withdrawn { + // catalog_gate.t27's `ALLOWED : [][]const u8`, addrmap.t27's `REGION_BRAM : MemRegion = MemRegion_new(..)`, + // capflags.t27's `FULL_CAPABILITIES : CapFlags = CapFlags_all()`. + assert(const_plan("ConstDecl(slice)", "`ALLOWED` is a module-level slice", true) == UNRESOLVED); + assert(const_plan("ConstDecl(slice)", "slice in a module-level constant", true) == UNRESOLVED); + assert(const_plan("ConstDecl", "`MemRegion` constant is not a compile-time value", true) == UNRESOLVED); + assert(const_plan("ConstDecl", "struct or array constant is not a compile-time value", true) == UNRESOLVED); + assert(const_plan("ConstDecl", "enum field is not a compile-time value", true) == UNRESOLVED); + assert(const_plan("ConstDecl", "struct field is not a compile-time value", true) == UNRESOLVED); + assert(const_plan("ConstDecl", "`X` is not a compile-time integer or bool", true) == UNRESOLVED); + assert(const_plan("ExprArrayLiteral", "array literal with no result type", true) == UNRESOLVED); + assert(const_plan("type mismatch", "expected u8, found u32", true) == UNRESOLVED); +} + +test an_annotation_zig_cannot_read_keeps_a_constants_refusal { + // `pub const X : Missing = ..;` and `[]Missing`: AstGen refuses the type whatever reaches the constant. + assert(const_plan("ConstDecl(slice)", "`X` is a module-level slice", false) == KEEP); + assert(const_plan("ConstDecl", "`Missing` constant is not a compile-time value", false) == KEEP); + assert(const_plan("type (undeclared)", "`Missing`", false) == KEEP); +} + +test a_constant_with_no_value_or_a_spelling_astgen_refuses_keeps_its_refusal { + // The same KEEP classes as a body's: they are AstGen's, whatever reaches the constant. + assert(const_plan("ConstDecl", "`X` has no value", true) == KEEP); + assert(const_plan("ExprLiteral(char escape)", "", true) == KEEP); + assert(const_plan("ExprLiteral(string literal)", "", true) == KEEP); + assert(const_plan("ExprArrayLiteral(text form)", "`[4]`: elements kept as text", true) == KEEP); + assert(const_plan("type (tuple)", "`(u32, u32)`", true) == KEEP); + assert(const_plan("ExprStructLit", "field `x` initialised twice", true) == KEEP); + assert(const_plan("ExprCall(@noSuch)", "", true) == 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));