From 48fe539e2a84ffc84ce4181752ccb7940f542dc7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 15:27:37 +0700 Subject: [PATCH 1/5] t27c: the use splice reads a name inside a pulled declaration in that declaration's module; the rule is specs/compiler/use_splice.t27 (Closes #10556) Two imported modules that each declare one helper name made the splice drop both, so a pulled fn called a helper the generated file never declared. A name found inside a pulled declaration now resolves against the module whose text names it (its own declaration, else the one its direct imports make) when the whole closure cannot answer it; both copies are spliced under __ and the uses are renamed. A qualified reference the importer writes is read in the module it names. A bare name the importer writes stays UNRESOLVED. The rule, the old all_agree/normalised/without_comment and alias_target are specs/compiler/use_splice.t27, lowered by t27c gen-rust to bootstrap/gen/rust/compiler/use_splice.rs and mounted by #[path]; the hand Rust in use_resolve.rs shrinks. Co-Authored-By: Claude Sonnet 5.5 --- bootstrap/gen/rust/compiler/use_splice.rs | 504 ++++++++++++++++++ bootstrap/src/use_resolve.rs | 226 ++------ bootstrap/tests/unresolved_use.rs | 32 ++ ...e-splice-reads-a-name-in-its-own-module.md | 17 + specs/compiler/use_splice.t27 | 496 +++++++++++++++++ 5 files changed, 1087 insertions(+), 188 deletions(-) create mode 100644 bootstrap/gen/rust/compiler/use_splice.rs create mode 100644 docs/now/2026-10-19-use-splice-reads-a-name-in-its-own-module.md create mode 100644 specs/compiler/use_splice.t27 diff --git a/bootstrap/gen/rust/compiler/use_splice.rs b/bootstrap/gen/rust/compiler/use_splice.rs new file mode 100644 index 0000000000..10443736ca --- /dev/null +++ b/bootstrap/gen/rust/compiler/use_splice.rs @@ -0,0 +1,504 @@ +// Generated from .t27 spec +// DO NOT EDIT — generated by t27c +// NOT LOWERED BY THIS BACKEND: 18 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. + +pub const B_NL: u8 = 10; + +pub const B_DQUOTE: u8 = 34; + +pub const B_SQUOTE: u8 = 39; + +pub const B_EQ: u8 = 61; + +pub const B_AT: u8 = 64; + +pub const B_BSLASH: u8 = 92; + +pub const ALIAS_MAX: usize = 1024; + +pub const NO: i32 = -1; + +pub fn put(out: &mut [u8], cap: usize, w: usize, b: u8) -> usize { + if (w < cap) { + out[(w) as usize] = b; + } + return (w + 1); +} + +pub fn line_end(t: &[u8], s: usize) -> usize { + let mut i: usize = s; + while ((i < t.len()) && (t[(i) as usize] != B_NL)) { + i += 1; + } + return i; +} + +pub fn same_span(a: &[u8], alo: usize, ahi: usize, b: &[u8], blo: usize, bhi: usize) -> bool { + if ((ahi - alo) != (bhi - blo)) { + return false; + } + let mut k: usize = 0; + while (k < (ahi - alo)) { + if (a[((alo + k)) as usize] != b[((blo + k)) as usize]) { + return false; + } + k += 1; + } + return true; +} + +pub fn code_end(t: &[u8], s: usize, e: usize) -> usize { + let mut q: u8 = 0; + let mut i: usize = s; + while (i < e) { + let c: u8 = t[(i) as usize]; + if (q != 0) { + if (c == B_BSLASH) { + i += 1; + } else { + if (c == q) { + q = 0; + } + } + } else { + if ((c == B_DQUOTE) || (c == B_SQUOTE)) { + q = c; + } else { + if (((c == B_SLASH) && ((i + 1) < e)) && (t[((i + 1)) as usize] == B_SLASH)) { + return i; + } + } + } + i += 1; + } + return e; +} + +pub fn norm_line(t: &[u8], p: usize) -> i64 { + let mut s: usize = p; + while (s < t.len()) { + let e: usize = line_end(t, s); + let c: usize = code_end(t, s, e); + let lo: usize = trim_lo(t, s, c); + let hi: usize = trim_hi(t, lo, c); + if (lo < hi) { + return pack(lo, hi); + } + s = (e + 1); + } + return -(1); +} + +pub fn lo_of(p: i64) -> usize { + return ((p / HALF) as _); +} + +pub fn hi_of(p: i64) -> usize { + return ((p - ((p / HALF) * HALF)) as _); +} + +pub fn same_decl(a: &[u8], b: &[u8]) -> bool { + let mut pa: usize = 0; + let mut pb: usize = 0; + let mut more: bool = true; + while more { + let la: i64 = norm_line(a, pa); + let lb: i64 = norm_line(b, pb); + if ((la < 0) || (lb < 0)) { + return ((la < 0) && (lb < 0)); + } + if !(same_span(a, lo_of(la), hi_of(la), b, lo_of(lb), hi_of(lb))) { + return false; + } + pa = (line_end(a, lo_of(la)) + 1); + pb = (line_end(b, lo_of(lb)) + 1); + } + return true; +} + +pub fn alias_class(text: &[u8], name: &[u8], stem: &[u8]) -> u8 { + let mut j: [u8; 1024] = [0; 1024]; + let mut n: usize = 0; + let mut line: i64 = norm_line(text, 0); + while (line >= 0) { + let lo: usize = lo_of(line); + let mut hi: usize = hi_of(line); + if (((n + (hi - lo)) + 1) > ALIAS_MAX) { + return 0; + } + if (n > 0) { + j[(n) as usize] = B_SPACE; + n += 1; + } + let mut k: usize = lo; + while (k < hi) { + j[(n) as usize] = text[(k) as usize]; + n += 1; + k += 1; + } + line = norm_line(text, (line_end(text, lo) + 1)); + } + let mut eq: usize = 0; + while ((eq < n) && (j[(eq) as usize] != B_EQ)) { + eq += 1; + } + if (eq >= n) { + return 0; + } + let lo: usize = trim_lo(&j[(0) as usize..(n) as usize], (eq + 1), n); + let mut hi: usize = trim_hi(&j[(0) as usize..(n) as usize], lo, n); + while ((hi > lo) && (j[((hi - 1)) as usize] == B_SEMI)) { + hi -= 1; + } + hi = trim_hi(&j[(0) as usize..(n) as usize], lo, hi); + let mut c: usize = lo; + while (((c + 1) < hi) && !(((j[(c) as usize] == B_COLON) && (j[((c + 1)) as usize] == B_COLON)))) { + c += 1; + } + if ((c + 1) >= hi) { + return 0; + } + let mh: usize = trim_hi(&j[(0) as usize..(n) as usize], lo, c); + let tl: usize = trim_lo(&j[(0) as usize..(n) as usize], (c + 2), hi); + let th: usize = trim_hi(&j[(0) as usize..(n) as usize], tl, hi); + if (!(same_span(&j[(0) as usize..(n) as usize], tl, th, name, 0, name.len())) || (mh <= lo)) { + return 0; + } + let mut m: usize = lo; + while (m < mh) { + if (!(word(j[(m) as usize])) && (j[(m) as usize] != B_MINUS)) { + return 0; + } + m += 1; + } + if same_span(&j[(0) as usize..(n) as usize], lo, mh, stem, 0, stem.len()) { + return 1; + } + return 2; +} + +pub fn has_line(list: &[u8], t: &[u8], lo: usize, hi: usize) -> bool { + let mut s: usize = 0; + while (s < list.len()) { + let e: usize = line_end(list, s); + if same_span(list, s, e, t, lo, hi) { + return true; + } + s = (e + 1); + } + return false; +} + +pub fn names_module(p: &[u8], lo: usize, hi: usize, q: &[u8]) -> bool { + let mut e: usize = hi; + if (((e - lo) >= 4) && starts(p, (e - 4), e, ".t27")) { + e -= 4; + } + let mut s: usize = e; + while ((s > lo) && (p[((s - 1)) as usize] != B_SLASH)) { + s -= 1; + } + let n: usize = (e - s); + if ((q.len() <= n) || !(same_span(q, 0, n, p, s, e))) { + return false; + } + return ((q[(n) as usize] == B_COLON) || (q[(n) as usize] == B_DOT)); +} + +pub fn scope_index(cands: &[u8], ctx: &[u8], imports: &[u8]) -> i32 { + if (ctx.len() == 0) { + return NO; + } + let mut k: i32 = 0; + let mut hit: i32 = NO; + let mut hits: u32 = 0; + let mut s: usize = 0; + while (s < cands.len()) { + let e: usize = line_end(cands, s); + if (ctx[0] == B_EQ) { + if names_module(cands, s, e, &ctx[(1) as usize..(ctx.len()) as usize]) { + return k; + } + } else { + if same_span(cands, s, e, ctx, 0, ctx.len()) { + return k; + } + if has_line(imports, cands, s, e) { + hit = k; + hits += 1; + } + } + k += 1; + s = (e + 1); + } + if (hits == 1) { + return hit; + } + return NO; +} + +pub fn put_flat(origin: &[u8], name: &[u8], out: &mut [u8], cap: usize, at: usize) -> usize { + let mut n: usize = origin.len(); + if ((n >= 4) && starts(origin, (n - 4), n, ".t27")) { + n -= 4; + } + let mut w: usize = at; + let mut i: usize = 0; + while (i < n) { + if word(origin[(i) as usize]) { + w = put(out, cap, w, origin[(i) as usize]); + } else { + w = put(out, cap, w, B_UNDERSCORE); + } + i += 1; + } + w = put(out, cap, w, B_UNDERSCORE); + w = put(out, cap, w, B_UNDERSCORE); + i = 0; + while (i < name.len()) { + w = put(out, cap, w, name[(i) as usize]); + i += 1; + } + return w; +} + +pub fn flat_name(origin: &[u8], name: &[u8], out: &mut [u8], cap: usize) -> usize { + return put_flat(origin, name, out, cap, 0); +} + +pub fn skip_blank(t: &[u8], p: usize) -> usize { + let mut i: usize = p; + while ((i < t.len()) && ((t[(i) as usize] == B_SPACE) || (t[(i) as usize] == B_TAB))) { + i += 1; + } + return i; +} + +pub fn after_decl_keyword(t: &[u8], p: usize) -> bool { + let mut e: usize = p; + while ((e > 0) && (ws_before(t, 0, e) > 0)) { + e -= 1; + } + let mut s: usize = e; + while ((s > 0) && word(t[((s - 1)) as usize])) { + s -= 1; + } + return ((starts(t, s, e, "const") && ((e - s) == 5)) || (starts(t, s, e, "type") && ((e - s) == 4))); +} + +pub fn is_use(t: &[u8], i: usize, j: usize) -> bool { + if ((i > 0) && ((t[((i - 1)) as usize] == B_DOT) || (t[((i - 1)) as usize] == B_AT))) { + return false; + } + if (((i > 1) && (t[((i - 1)) as usize] == B_COLON)) && (t[((i - 2)) as usize] == B_COLON)) { + return false; + } + let k: usize = skip_blank(t, j); + if (k >= t.len()) { + return true; + } + let single_colon: bool = ((t[(k) as usize] == B_COLON) && !((((k + 1) < t.len()) && (t[((k + 1)) as usize] == B_COLON)))); + let single_eq: bool = ((t[(k) as usize] == B_EQ) && !((((k + 1) < t.len()) && (t[((k + 1)) as usize] == B_EQ)))); + if (single_colon || single_eq) { + return after_decl_keyword(t, i); + } + return true; +} + +pub fn rename_ident(text: &[u8], name: &[u8], origin: &[u8], out: &mut [u8], cap: usize) -> usize { + let mut w: usize = 0; + let mut i: usize = 0; + let mut bol: bool = true; + while (i < text.len()) { + let c: u8 = text[(i) as usize]; + if (c == B_NL) { + w = put(out, cap, w, c); + bol = true; + i += 1; + } else { + if (bol && ((c == B_SPACE) || (c == B_TAB))) { + w = put(out, cap, w, c); + i += 1; + } else { + if ((bol && (c == B_SEMI)) || (((c == B_SLASH) && ((i + 1) < text.len())) && (text[((i + 1)) as usize] == B_SLASH))) { + while ((i < text.len()) && (text[(i) as usize] != B_NL)) { + w = put(out, cap, w, text[(i) as usize]); + i += 1; + } + } else { + if ((c == B_DQUOTE) || (c == B_SQUOTE)) { + bol = false; + w = put(out, cap, w, c); + i += 1; + while (((i < text.len()) && (text[(i) as usize] != c)) && (text[(i) as usize] != B_NL)) { + if ((text[(i) as usize] == B_BSLASH) && ((i + 1) < text.len())) { + w = put(out, cap, w, text[(i) as usize]); + i += 1; + } + w = put(out, cap, w, text[(i) as usize]); + i += 1; + } + if ((i < text.len()) && (text[(i) as usize] == c)) { + w = put(out, cap, w, c); + i += 1; + } + } else { + if word(c) { + bol = false; + let mut j: usize = i; + while ((j < text.len()) && word(text[(j) as usize])) { + j += 1; + } + if (same_span(text, i, j, name, 0, name.len()) && is_use(text, i, j)) { + w = put_flat(origin, name, out, cap, w); + } else { + let mut k: usize = i; + while (k < j) { + w = put(out, cap, w, text[(k) as usize]); + k += 1; + } + } + i = j; + } else { + bol = false; + w = put(out, cap, w, c); + i += 1; + } + } + } + } + } + } + return w; +} + +pub const B_COLON: u8 = 58; + +pub const B_CR: u8 = 13; + +pub const B_DOT: u8 = 46; + +pub const B_MINUS: u8 = 45; + +pub const B_SEMI: u8 = 59; + +pub const B_SLASH: u8 = 47; + +pub const B_SPACE: u8 = 32; + +pub const B_TAB: u8 = 9; + +pub const B_UNDERSCORE: u8 = 95; + +pub const HALF: i64 = 4294967296; + +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 pack(lo: usize, hi: usize) -> i64 { + let a: i64 = (lo as _); + let b: i64 = (hi as _); + return ((a * HALF) + b); +} + +pub fn same(s: &[u8], w: &'static str) -> bool { + if (s.len() != w.len()) { + return false; + } + let mut k: usize = 0; + while (k < w.len()) { + if (s[(k) as usize] != w.as_bytes()[(k) as usize]) { + return false; + } + k = (k + 1); + } + return true; +} + +pub fn starts(s: &[u8], lo: usize, hi: usize, w: &'static str) -> bool { + if ((hi - lo) < w.len()) { + return false; + } + return same(&s[(lo) as usize..((lo + w.len())) as usize], w); +} + +pub fn trim_hi(s: &[u8], lo: usize, hi: usize) -> usize { + let mut j: usize = hi; + while (j > lo) { + let n: usize = ws_before(s, lo, j); + if (n == 0) { + return j; + } + j = (j - n); + } + return j; +} + +pub fn trim_lo(s: &[u8], lo: usize, hi: usize) -> usize { + let mut i: usize = lo; + while (i < hi) { + let n: usize = ws_at(s, i, hi); + if (n == 0) { + return i; + } + i = (i + n); + } + 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)) { + return 1; + } + if (c == 194) { + if (((i + 1) < hi) && ((s[((i + 1)) as usize] == 133) || (s[((i + 1)) as usize] == 160))) { + return 2; + } + return 0; + } + if ((i + 2) >= hi) { + return 0; + } + let b1: u8 = s[((i + 1)) as usize]; + let b2: u8 = s[((i + 2)) as usize]; + if (((c == 225) && (b1 == 154)) && (b2 == 128)) { + return 3; + } + if (((c == 226) && (b1 == 128)) && (((((b2 >= 128) && (b2 <= 138)) || (b2 == 168)) || (b2 == 169)) || (b2 == 175))) { + return 3; + } + if (((c == 226) && (b1 == 129)) && (b2 == 159)) { + return 3; + } + if (((c == 227) && (b1 == 128)) && (b2 == 128)) { + return 3; + } + return 0; +} + +pub fn ws_before(s: &[u8], lo: usize, end: usize) -> usize { + let c: u8 = s[((end - 1)) as usize]; + if (((c >= B_TAB) && (c <= B_CR)) || (c == B_SPACE)) { + return 1; + } + if (((end - lo) >= 2) && (ws_at(s, (end - 2), end) == 2)) { + return 2; + } + if (((end - lo) >= 3) && (ws_at(s, (end - 3), end) == 3)) { + return 3; + } + return 0; +} + diff --git a/bootstrap/src/use_resolve.rs b/bootstrap/src/use_resolve.rs index c598b56ebe..7fbafb0204 100644 --- a/bootstrap/src/use_resolve.rs +++ b/bootstrap/src/use_resolve.rs @@ -28,6 +28,7 @@ use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; +#[path = "../gen/rust/compiler/use_splice.rs"] #[allow(dead_code, unused_parens, unused_mut, unused_variables, non_snake_case, unused_assignments, clippy::all)] mod use_splice; // specs/compiler/use_splice.t27, by `t27c gen-rust` /// One top-level declaration lifted out of a spec, with the module it came from. #[derive(Clone)] @@ -471,72 +472,7 @@ fn identifiers(text: &str) -> HashSet { /// `math/constants.t27` against `math/sacred_physics.t27` is a real conflict /// and a silent pick would be exactly the mistake this guard was built for. fn all_agree(candidates: &[&Decl]) -> bool { - let first = normalised(&candidates[0].text); - candidates.iter().all(|d| normalised(&d.text) == first) -} - -/// Drop a line comment that is not inside a string literal. -/// -/// `base/types.t27` writes `pub const ONE : i8 = 1; // Trit = +1` and -/// `base/ops.t27` writes `pub const ONE : i8 = 1;`. Those are the same -/// declaration; only one of them is annotated. Comparing raw text calls that a -/// conflict and refuses the import. -fn without_comment(line: &str) -> &str { - let b = line.as_bytes(); - let mut in_str: Option = None; - let mut i = 0; - while i < b.len() { - match in_str { - Some(q) => { - if b[i] == b'\\' { - i += 1; - } else if b[i] == q { - in_str = None; - } - } - None => { - if b[i] == b'"' || b[i] == b'\'' { - in_str = Some(b[i]); - } else if b[i] == b'/' && i + 1 < b.len() && b[i + 1] == b'/' { - return &line[..i]; - } - } - } - i += 1; - } - line -} - -/// A declaration reduced to what it states: no comments, no blank lines, no -/// indentation. -fn normalised(text: &str) -> Vec { - text.lines() - .map(|l| without_comment(l).trim().to_string()) - .filter(|l| !l.is_empty()) - .collect() -} - -/// If this declaration is nothing but `= ::;`, the module it -/// names. -/// -/// `specs/math/sacred_physics.t27` declares -/// `const PHI : f64 = constants::PHI;` -- a re-export. It is not a second -/// opinion about PHI, it is a pointer to the first one. -fn alias_target(d: &Decl, name: &str) -> Option { - let joined = normalised(&d.text).join(" "); - let rhs = joined.split_once('=')?.1.trim().trim_end_matches(';').trim(); - let (module, target) = rhs.split_once("::")?; - let module = module.trim(); - if target.trim() != name || module.is_empty() { - return None; - } - if !module - .chars() - .all(|c| c.is_alphanumeric() || c == '_' || c == '-') - { - return None; - } - Some(module.to_string()) + candidates.iter().all(|d| use_splice::same_decl(d.text.as_bytes(), candidates[0].text.as_bytes())) } /// Which candidate to splice when they are not all the same declaration. @@ -550,29 +486,13 @@ fn alias_target(d: &Decl, name: &str) -> Option { /// flat output has no `constants` namespace. The target is what carries the /// value. fn alias_resolved<'a>(candidates: &[&'a Decl], name: &str) -> Option<&'a Decl> { - let mut real: Vec<&Decl> = Vec::new(); - let mut aliases: Vec<(&Decl, String)> = Vec::new(); - for d in candidates { - match alias_target(d, name) { - Some(m) => aliases.push((d, m.to_string())), - None => real.push(d), - } - } - if real.len() != 1 || aliases.is_empty() { + let class = |d: &Decl, stem: &str| use_splice::alias_class(d.text.as_bytes(), name.as_bytes(), stem.as_bytes()); + let real: Vec<&&Decl> = candidates.iter().filter(|d| class(d, "") == 0).collect(); + if real.len() != 1 || real.len() == candidates.len() { return None; } - let target = real[0]; - let target_module = target - .origin - .rsplit('/') - .next() - .unwrap_or(&target.origin) - .trim_end_matches(".t27"); - if aliases.iter().all(|(_, m)| m == target_module) { - Some(target) - } else { - None - } + let stem = real[0].origin.rsplit('/').next().unwrap_or(&real[0].origin).trim_end_matches(".t27"); + candidates.iter().all(|d| class(d, stem) != 2).then_some(*real[0]) } /// The names `resolve` refused to splice, as it explained them. /// @@ -628,7 +548,7 @@ fn flatten_qualified( available.get(name).map_or(false, |cands| { cands .iter() - .any(|d| normalised(&d.text) == normalised(&ld.text)) + .any(|d| use_splice::same_decl(d.text.as_bytes(), ld.text.as_bytes())) }) }) .unwrap_or(false) @@ -686,6 +606,12 @@ fn consumed_use(line: &str, specs_root: &Path, pulled: &HashSet) -> Opti Some(format!("{}use {}{{{}}}; // {}", indent, prefix, kept.join(", "), note)) } +fn renamed(text: &str, name: &str, origin: &str) -> String { + let n = use_splice::rename_ident(text.as_bytes(), name.as_bytes(), origin.as_bytes(), &mut [], 0); + let mut out = vec![0u8; n]; + use_splice::rename_ident(text.as_bytes(), name.as_bytes(), origin.as_bytes(), &mut out, n); + String::from_utf8(out).unwrap_or_default() +} pub fn resolve(input_path: &Path, source: &str) -> String { let specs_root = match find_specs_root(input_path) { Some(r) => r, @@ -697,6 +623,7 @@ pub fn resolve(input_path: &Path, source: &str) -> String { let mut seen: HashSet = HashSet::new(); let mut queue: Vec = use_targets(source, &specs_root); let mut available: HashMap> = HashMap::new(); + let mut imports: HashMap = HashMap::new(); while let Some(dep) = queue.pop() { let canonical = dep.canonicalize().unwrap_or_else(|_| dep.clone()); if !seen.insert(canonical) { @@ -721,6 +648,7 @@ pub fn resolve(input_path: &Path, source: &str) -> String { .unwrap_or(&dep) .to_string_lossy() .to_string(); + imports.insert(origin.clone(), use_targets(&text, &specs_root).iter().map(|p| p.strip_prefix(&specs_root).unwrap_or(p).to_string_lossy().to_string()).collect::>().join("\n")); for d in split_decls(&text, &origin) { available.entry(d.name.clone()).or_default().push(d); } @@ -751,19 +679,21 @@ pub fn resolve(input_path: &Path, source: &str) -> String { let mut pulled: Vec = Vec::new(); let mut pulled_names: HashSet = HashSet::new(); let mut ambiguous: Vec<(String, Vec)> = Vec::new(); - let mut frontier = identifiers(source); - for (_, name, _) in &qualified { - frontier.insert(name.clone()); + let mut scoped: HashMap<(String, String), String> = HashMap::new(); + let bare = qualified.iter().fold(source.to_string(), |s, (q, n, _)| s.replace(q.as_str(), &q[..q.len() - n.len()])); + let mut frontier: HashSet<(String, String)> = identifiers(&bare).into_iter().map(|n| (n, String::new())).collect(); + for (q, name, _) in &qualified { + frontier.insert((name.clone(), format!("={}", q))); } while !frontier.is_empty() { - let mut next: HashSet = HashSet::new(); + let mut next: HashSet<(String, String)> = HashSet::new(); // The frontier is walked in HashSet order, which Rust randomises per // process. Sorting it here was tried and REMOVED: with the `distinct` // sort below in place and this one gone, 0 of the 492 importing specs // are non-deterministic over four runs each. A guard whose removal // changes nothing measurable is decoration, and the comment justifying // it would outlive the reason for it. - for name in frontier { + for (name, ctx) in frontier { if local.contains(&name) || pulled_names.contains(&name) { continue; } @@ -796,11 +726,20 @@ pub fn resolve(input_path: &Path, source: &str) -> String { let mut origins: Vec = distinct.iter().map(|d| d.origin.clone()).collect(); origins.sort(); + let at = use_splice::scope_index(origins.join("\n").as_bytes(), ctx.as_bytes(), imports.get(&ctx).map_or("", String::as_str).as_bytes()); + if let Some(d) = usize::try_from(at).ok().and_then(|i| distinct.get(i)) { + for c in [&ctx, &d.origin] { scoped.insert((c.clone(), name.clone()), d.origin.clone()); } + if pulled_names.insert(format!("{}::{}", d.origin, name)) { + next.extend(identifiers(&d.text).into_iter().map(|n| (n, d.origin.clone()))); + pulled.push((*d).clone()); + } + continue; + } ambiguous.push((name.clone(), origins)); continue; } }; - next.extend(identifiers(&decl.text)); + next.extend(identifiers(&decl.text).into_iter().map(|n| (n, decl.origin.clone()))); pulled_names.insert(name); pulled.push(decl); } @@ -852,16 +791,18 @@ pub fn resolve(input_path: &Path, source: &str) -> String { // local one; otherwise the qualifier stays, the call binds to no local // function, and the arity check is skipped by non-match rather than run // against a namesake. Non-call spellings keep the old flattening. + let flat = |q: &String, n: &String| scoped.get(&(format!("={}", q), n.clone())).map(|o| renamed(n, n, o)); let mut rewrites: Vec<&(String, String, bool)> = qualified .iter() - .filter(|(_, name, is_call)| { - flatten_qualified(name, *is_call, &pulled_names, &local, &local_decls, &available) + .filter(|(q, name, is_call)| { + flat(q, name).is_some() || flatten_qualified(name, *is_call, &pulled_names, &local, &local_decls, &available) }) .collect(); rewrites.sort_by(|a, b| b.0.len().cmp(&a.0.len())); for (qual, name, _) in rewrites { - out = out.replace(qual.as_str(), name.as_str()); + out = out.replace(qual.as_str(), &flat(qual, name).unwrap_or_else(|| name.clone())); } + scoped.iter().for_each(|((c, x), o)| pulled.iter_mut().filter(|d| d.origin == *c).for_each(|d| d.text = renamed(&d.text, x, o))); if !out.ends_with('\n') { out.push('\n'); } @@ -1133,63 +1074,6 @@ mod tests { } } -#[cfg(test)] -mod agree_tests { - use super::*; - - fn d(origin: &str, text: &str) -> Decl { - Decl { name: "X".into(), text: text.into(), origin: origin.into() } - } - - #[test] - fn identical_declarations_agree() { - let a = d("types", "pub const Trit = enum(i8) {\n neg = -1,\n};"); - let b = d("ops", "pub const Trit = enum(i8) {\n neg = -1,\n};"); - assert!(all_agree(&[&a, &b])); - } - - #[test] - fn indentation_does_not_make_two_declarations_disagree() { - // The corpus writes two conventions -- column 0 in one file, indented - // under `module M;` in another. Raw text comparison calls these - // different and keeps refusing an import that has no ambiguity in it. - let a = d("types", "pub const T = enum(i8) {\n neg = -1,\n};"); - let b = d("ops", " pub const T = enum(i8) {\n neg = -1,\n };"); - assert!(all_agree(&[&a, &b])); - } - - #[test] - fn a_different_value_disagrees() { - // PHI is 1.618... in math/constants.t27 and something else in - // math/sacred_physics.t27. Twenty (spec, name) pairs are this shape and - // every one must stay unresolved: a silent pick here is the mistake the - // refusal was built to prevent. - let a = d("constants", "pub const PHI : f64 = 1.618033988749895;"); - let b = d("sacred_physics", "pub const PHI : f64 = 1.6180339887;"); - assert!(!all_agree(&[&a, &b])); - } - - #[test] - fn a_missing_line_disagrees() { - let a = d("x", "pub const E = enum(i8) {\n a = 1,\n b = 2,\n};"); - let b = d("y", "pub const E = enum(i8) {\n a = 1,\n};"); - assert!(!all_agree(&[&a, &b])); - } - - #[test] - fn blank_lines_are_not_content() { - let a = d("x", "pub const A = 1;"); - let b = d("y", "\npub const A = 1;\n\n"); - assert!(all_agree(&[&a, &b])); - } - - #[test] - fn a_single_candidate_agrees_with_itself() { - let a = d("x", "pub const A = 1;"); - assert!(all_agree(&[&a])); - } -} - #[cfg(test)] mod notes_tests { use super::*; @@ -1230,40 +1114,6 @@ mod alias_tests { Decl { name: "X".into(), text: text.into(), origin: origin.into() } } - #[test] - fn a_trailing_comment_is_not_part_of_a_declaration() { - // base/types.t27 annotates `pub const ONE : i8 = 1;` and base/ops.t27 - // does not. Same declaration, one of them documented. - let a = d("base/ops.t27", "pub const ONE : i8 = 1;"); - let b = d("base/types.t27", "pub const ONE : i8 = 1; // Trit = +1"); - assert!(all_agree(&[&a, &b])); - } - - #[test] - fn a_slash_inside_a_string_does_not_start_a_comment() { - assert_eq!(without_comment(r#"const U = "http://x"; // note"#), r#"const U = "http://x"; "#); - } - - #[test] - fn an_alias_names_its_target_module() { - let a = d("math/sacred_physics.t27", "const PHI : f64 = constants::PHI;"); - assert_eq!(alias_target(&a, "PHI").as_deref(), Some("constants")); - } - - #[test] - fn a_value_is_not_an_alias() { - let a = d("math/constants.t27", "const PHI : f64 = 1.618033988749895;"); - assert!(alias_target(&a, "PHI").is_none()); - } - - #[test] - fn an_alias_to_a_different_name_is_not_an_alias_for_this_one() { - // `const PHI = constants::PHI_INV;` is a re-export of something else, - // and splicing the PHI definition for it would be wrong. - let a = d("m/x.t27", "const PHI : f64 = constants::PHI_INV;"); - assert!(alias_target(&a, "PHI").is_none()); - } - #[test] fn the_target_is_spliced_and_the_alias_is_not() { // Splicing the alias would emit `constants::PHI` into a flat file with diff --git a/bootstrap/tests/unresolved_use.rs b/bootstrap/tests/unresolved_use.rs index 0a66e868f6..4119449920 100644 --- a/bootstrap/tests/unresolved_use.rs +++ b/bootstrap/tests/unresolved_use.rs @@ -81,3 +81,35 @@ fn a_use_resolves_however_the_spec_path_is_spelled() { assert!(stdout.contains("const K"), "{spelled}: the spliced constant is in the output:\n{stdout}"); } } + +/// #10556: modules a and b each declare a helper `ok`; `gen` of an importer, whose body is `body`. +fn two_helpers(tag: &str, body: &str) -> (bool, String, String) { + let root = scratch(tag).join("specs/r"); + std::fs::create_dir_all(&root).expect("dir"); + let a = "module a;\npub fn ok(n: u32) -> bool {\n return n > 0;\n}\npub fn admit(n: u32) -> bool {\n return ok(n);\n}\n"; + let b = "module b;\npub fn ok(k: u32, f: bool) -> bool {\n return k > 0 or f;\n}\npub fn fine(k: u32) -> bool {\n return ok(k, false);\n}\n"; + for (n, t) in [("a", a), ("b", b), ("imp", &format!("module imp;\nuse r::a;\nuse r::b;\n\npub fn s(n: u32) -> bool {{\n return {body};\n}}\n"))] { + std::fs::write(root.join(format!("{n}.t27")), t).expect("write"); + } + gen(&root.join("imp.t27")) +} + +#[test] +fn a_helper_two_modules_declare_stays_with_the_module_that_means_it() { + let (ok, out, err) = two_helpers("scope", "admit(n) and b::fine(n)"); + assert!(ok && !err.contains("UNRESOLVED") && !out.contains("UNRESOLVED"), "{err}"); + for want in ["fn r_a__ok(", "fn r_b__ok(", "return r_a__ok(n);", "return r_b__ok(k, false);"] { assert!(out.contains(want), "{want}:\n{out}"); } +} + +#[test] +fn a_qualified_helper_takes_the_flat_name_of_the_module_it_names() { + let (_, out, err) = two_helpers("qualified", "b::ok(n, true)"); + assert!(!err.contains("UNRESOLVED") && out.contains("return r_b__ok(n, true);") && !out.contains("r_a__ok"), "{err}\n{out}"); +} + +#[test] +fn a_name_the_importer_writes_that_two_modules_declare_is_still_refused() { + let (ok, out, err) = two_helpers("refused", "ok(n, true)"); + assert!(ok && err.contains("UNRESOLVED ok: declared in r/a.t27 and r/b.t27"), "{err}"); + assert!(!out.contains("r_a__ok") && !out.contains("fn ok("), "{out}"); +} diff --git a/docs/now/2026-10-19-use-splice-reads-a-name-in-its-own-module.md b/docs/now/2026-10-19-use-splice-reads-a-name-in-its-own-module.md new file mode 100644 index 0000000000..42959f0ccd --- /dev/null +++ b/docs/now/2026-10-19-use-splice-reads-a-name-in-its-own-module.md @@ -0,0 +1,17 @@ +# NOW -- the `use` splice reads a name inside a pulled declaration in that declaration's module (2026-10-19) + +## specs/compiler/use_splice.t27, bootstrap/src/use_resolve.rs (Closes #10556, blocks epic #10879) + +- Before: two imported modules that declare one helper name made the splice refuse both, even when the name + sat inside a pulled function and could only mean the helper next to it. `t27c gen` exited 0 and the + generated file called a function it never declared (`tri t27o --check` could not pass `compiler.rs` or + `main.rs`: `tri::crypto::sha256` and `tri::t27b::type_text` both declare `same`). +- Now: a name the whole closure cannot answer is read in the scope of the module whose text names it (the + module's own declaration, else the one its direct imports make). A qualified reference the importer writes + (`beta::name_ok`) is read in beta. Both copies are spliced under `__` and the uses inside + the module's texts are renamed. A bare name the importer writes stays UNRESOLVED. +- The rule is `specs/compiler/use_splice.t27`; `t27c gen-rust` writes `bootstrap/gen/rust/compiler/use_splice.rs`, + which use_resolve.rs mounts by `#[path]`. `all_agree` and `alias_target` moved there too, so the hand Rust only + shrinks. +- Trap, not fixed here: `identifiers` reads `;` prose lines, so a comment that names the ambiguous name in the + importer (the header of the issue's `importer.t27` does) still prints UNRESOLVED, though the output is right. diff --git a/specs/compiler/use_splice.t27 b/specs/compiler/use_splice.t27 new file mode 100644 index 0000000000..e8deef3d08 --- /dev/null +++ b/specs/compiler/use_splice.t27 @@ -0,0 +1,496 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/compiler/use_splice.t27 -- how the `use` splice reads a name that two imported modules declare +; (Closes #10556), and the two agreement rules it already had (`all_agree`, `alias_resolved`). +; -- +; The splice (bootstrap/src/use_resolve.rs) pulls into the importer every declaration the importer names, +; then every declaration the pulled text names. It keys the declarations by bare name. When two imported +; modules declare one name differently it refused both, and that is right for a name the IMPORTER wrote: +; the choice is the author's. It is wrong for a name found INSIDE a pulled declaration. `admit` in +; service.t27 calls `name_ok`; the only `name_ok` its author can mean is the one in service.t27, and +; refusing both left the generated file calling a function it never declared (`t27c gen` still exited 0). +; -- +; The rules, each pinned by a test below: +; R1 (scope). A name found inside a pulled declaration of module O is read in O's scope: the declaration O +; makes, else the one declaration its direct imports make. `scope_index` answers; -1 is "no answer". +; R2 (only on ambiguity). The scope is consulted only for a name the whole closure cannot answer (one +; declaration, declarations that agree, or an alias). A name with one answer anywhere keeps it, so a +; spec that resolved yesterday generates the same bytes today. +; R3 (flat names). The output is one namespace, so two scoped copies of one name cannot both be called +; `name_ok`. A scoped copy is named `__` (`flat_name`) and every use of the name in the +; texts of the module that means it is rewritten (`rename_ident`). A qualified reference the importer +; writes (`beta::name_ok`) is read in beta's scope, so it takes the same flat name. +; R4 (the author's choice stays refused). A bare name the importer writes, declared differently by two +; imported modules, is still UNRESOLVED: no scope applies to it. +; -- +; `rename_ident` is a token rewrite, not a parse. It leaves comments, `;` prose lines, string and character +; literals, `.name` and `::name` (a member or a qualified path), `@name`, and a name in a declaration +; position (`name: T`, `name = v` inside a struct or an enum) alone; a declaration head after `const` or +; `type` is renamed. The Zig the specs lower to forbids a local or a parameter that shadows a top-level +; declaration, so inside a module's own texts a bare use of its declared name can only mean the declaration. +; -- +; Also here: `same_decl` (the old `all_agree`/`normalised`/`without_comment`) and `alias_class` (the old +; `alias_target`), moved from use_resolve.rs so the hand Rust only shrinks. Their behaviour is the old one, +; line for line. +; `t27c gen-rust` lowers this spec to bootstrap/gen/rust/compiler/use_splice.rs, which use_resolve.rs +; mounts by #[path]. Never hand-edit the generated copy. +; ASCII only (L3) +; phi^2 + 1/phi^2 = 3 | TRINITY + +module UseSplice; + +use tri::t27b::type_text; + +pub const B_NL : u8 = 10; +pub const B_DQUOTE : u8 = 34; +pub const B_SQUOTE : u8 = 39; +pub const B_EQ : u8 = 61; +pub const B_AT : u8 = 64; +pub const B_BSLASH : u8 = 92; + +/// A declaration longer than this once normalised and joined is no alias: `const X : T = module::X;` is a line. +pub const ALIAS_MAX : usize = 1024; + +/// No such text, or no answer. +pub const NO : i32 = -1; + +/// Appends one byte; the count goes on past `cap` so the caller learns the size it needs. +fn put(out: []u8, cap: usize, w: usize, b: u8) -> usize { + if (w < cap) { out[w] = b; } + return w + 1; +} + +/// The end of the line that starts at s: its newline, or t.len. +fn line_end(t: []const u8, s: usize) -> usize { + var i : usize = s; + while (i < t.len and t[i] != B_NL) { i += 1; } + return i; +} + +/// Are a[alo .. ahi] and b[blo .. bhi] the same bytes? +pub fn same_span(a: []const u8, alo: usize, ahi: usize, b: []const u8, blo: usize, bhi: usize) -> bool { + if (ahi - alo != bhi - blo) { return false; } + var k : usize = 0; + while (k < ahi - alo) { + if (a[alo + k] != b[blo + k]) { return false; } + k += 1; + } + return true; +} + +/// Where the code of the line t[s .. e] ends: at its first `//` outside a quote, else at e. A quote opens at +/// a double or single quote byte and a backslash inside it skips the next byte. +fn code_end(t: []const u8, s: usize, e: usize) -> usize { + var q : u8 = 0; + var i : usize = s; + while (i < e) { + const c : u8 = t[i]; + if (q != 0) { + if (c == B_BSLASH) { + i += 1; + } else if (c == q) { + q = 0; + } + } else if (c == B_DQUOTE or c == B_SQUOTE) { + q = c; + } else if (c == type_text::B_SLASH and i + 1 < e and t[i + 1] == type_text::B_SLASH) { + return i; + } + i += 1; + } + return e; +} + +/// The next line of t at or after the line start p that has code left once its comment and its outer +/// whitespace are gone, as its two offsets packed in one i64; -1 when there is none. +fn norm_line(t: []const u8, p: usize) -> i64 { + var s : usize = p; + while (s < t.len) { + const e : usize = line_end(t, s); + const c : usize = code_end(t, s, e); + const lo : usize = type_text::trim_lo(t, s, c); + const hi : usize = type_text::trim_hi(t, lo, c); + if (lo < hi) { return type_text::pack(lo, hi); } + s = e + 1; + } + return -1; +} + +fn lo_of(p: i64) -> usize { + return @intCast(@divTrunc(p, type_text::HALF)); +} + +fn hi_of(p: i64) -> usize { + return @intCast(p - @divTrunc(p, type_text::HALF) * type_text::HALF); +} + +/// Do these two declarations say the same thing? Line by line with each line trimmed, comments dropped and +/// blank lines skipped, because the corpus writes two indentation conventions and annotates one copy of a +/// declaration and not the other (`pub const ONE : i8 = 1; // Trit = +1`). Two declarations that agree +/// offer no choice, so a refusal between them costs the import for nothing. +pub fn same_decl(a: []const u8, b: []const u8) -> bool { + var pa : usize = 0; + var pb : usize = 0; + var more : bool = true; + while (more) { + const la : i64 = norm_line(a, pa); + const lb : i64 = norm_line(b, pb); + if (la < 0 or lb < 0) { return la < 0 and lb < 0; } + if (!same_span(a, lo_of(la), hi_of(la), b, lo_of(lb), hi_of(lb))) { return false; } + pa = line_end(a, lo_of(la)) + 1; + pb = line_end(b, lo_of(lb)) + 1; + } + return true; +} + +/// Is this declaration (the text of the declaration called `name`) an alias, `const PHI : f64 = constants::PHI;`? +/// 0 when it is not one, 1 when it points at the module `stem`, 2 when it points at another module. An alias +/// is a pointer to the first definition, not a second opinion about it; its own text (`constants::PHI`) has +/// no meaning in the flat output, so the splice takes the target. +pub fn alias_class(text: []const u8, name: []const u8, stem: []const u8) -> u8 { + var j : [1024]u8 = [0] ** 1024; + var n : usize = 0; + var line : i64 = norm_line(text, 0); + while (line >= 0) { + const lo : usize = lo_of(line); + const hi : usize = hi_of(line); + if (n + (hi - lo) + 1 > ALIAS_MAX) { return 0; } + if (n > 0) { + j[n] = type_text::B_SPACE; + n += 1; + } + var k : usize = lo; + while (k < hi) { + j[n] = text[k]; + n += 1; + k += 1; + } + line = norm_line(text, line_end(text, lo) + 1); + } + var eq : usize = 0; + while (eq < n and j[eq] != B_EQ) { eq += 1; } + if (eq >= n) { return 0; } + const lo : usize = type_text::trim_lo(j[0 .. n], eq + 1, n); + var hi : usize = type_text::trim_hi(j[0 .. n], lo, n); + while (hi > lo and j[hi - 1] == type_text::B_SEMI) { hi -= 1; } + hi = type_text::trim_hi(j[0 .. n], lo, hi); + var c : usize = lo; + while (c + 1 < hi and !(j[c] == type_text::B_COLON and j[c + 1] == type_text::B_COLON)) { c += 1; } + if (c + 1 >= hi) { return 0; } + const mh : usize = type_text::trim_hi(j[0 .. n], lo, c); + const tl : usize = type_text::trim_lo(j[0 .. n], c + 2, hi); + const th : usize = type_text::trim_hi(j[0 .. n], tl, hi); + if (!same_span(j[0 .. n], tl, th, name, 0, name.len) or mh <= lo) { return 0; } + var m : usize = lo; + while (m < mh) { + if (!type_text::word(j[m]) and j[m] != type_text::B_MINUS) { return 0; } + m += 1; + } + if (same_span(j[0 .. n], lo, mh, stem, 0, stem.len)) { return 1; } + return 2; +} + +/// Does `list` (names one to a line) hold a line that is t[lo .. hi]? +fn has_line(list: []const u8, t: []const u8, lo: usize, hi: usize) -> bool { + var s : usize = 0; + while (s < list.len) { + const e : usize = line_end(list, s); + if (same_span(list, s, e, t, lo, hi)) { return true; } + s = e + 1; + } + return false; +} + +/// Does the qualified spelling q (`m::name` or `m.name`) name the module at path p[lo .. hi], whose stem is m? +fn names_module(p: []const u8, lo: usize, hi: usize, q: []const u8) -> bool { + var e : usize = hi; + if (e - lo >= 4 and type_text::starts(p, e - 4, e, ".t27")) { e -= 4; } + var s : usize = e; + while (s > lo and p[s - 1] != type_text::B_SLASH) { s -= 1; } + const n : usize = e - s; + if (q.len <= n or !same_span(q, 0, n, p, s, e)) { return false; } + return q[n] == type_text::B_COLON or q[n] == type_text::B_DOT; +} + +/// R1. `cands` lists, one to a line, the modules that declare a name no one answer covers; `ctx` says whose +/// text names it: "" for the importer's own text, `=m::name` for a qualified reference the importer writes +/// (the module m it names), else the path of the module whose pulled declaration names it; `imports` lists +/// the modules a module `ctx` imports. The index of the line of `cands` that the context means: the module +/// itself, else the one line `imports` also holds; -1 for none or for several, which stays UNRESOLVED. +pub fn scope_index(cands: []const u8, ctx: []const u8, imports: []const u8) -> i32 { + if (ctx.len == 0) { return NO; } + var k : i32 = 0; + var hit : i32 = NO; + var hits : u32 = 0; + var s : usize = 0; + while (s < cands.len) { + const e : usize = line_end(cands, s); + if (ctx[0] == B_EQ) { + if (names_module(cands, s, e, ctx[1 .. ctx.len])) { return k; } + } else { + if (same_span(cands, s, e, ctx, 0, ctx.len)) { return k; } + if (has_line(imports, cands, s, e)) { + hit = k; + hits += 1; + } + } + k += 1; + s = e + 1; + } + if (hits == 1) { return hit; } + return NO; +} + +/// Appends the flat name of `name` as `origin` declares it (R3): the origin path without `.t27`, every byte +/// that is not a letter, a digit or `_` turned into `_`, then `__` and the name. +fn put_flat(origin: []const u8, name: []const u8, out: []u8, cap: usize, at: usize) -> usize { + var n : usize = origin.len; + if (n >= 4 and type_text::starts(origin, n - 4, n, ".t27")) { n -= 4; } + var w : usize = at; + var i : usize = 0; + while (i < n) { + if (type_text::word(origin[i])) { + w = put(out, cap, w, origin[i]); + } else { + w = put(out, cap, w, type_text::B_UNDERSCORE); + } + i += 1; + } + w = put(out, cap, w, type_text::B_UNDERSCORE); + w = put(out, cap, w, type_text::B_UNDERSCORE); + i = 0; + while (i < name.len) { + w = put(out, cap, w, name[i]); + i += 1; + } + return w; +} + +/// R3. The flat name of `name` as `origin` declares it, written to out[0 .. cap]; the result is the length it +/// needs. +pub fn flat_name(origin: []const u8, name: []const u8, out: []u8, cap: usize) -> usize { + return put_flat(origin, name, out, cap, 0); +} + +/// The first byte at or after p that is not a space or a tab; t.len when there is none. +fn skip_blank(t: []const u8, p: usize) -> usize { + var i : usize = p; + while (i < t.len and (t[i] == type_text::B_SPACE or t[i] == type_text::B_TAB)) { i += 1; } + return i; +} + +/// Is the word that ends just before p (blanks and newlines between) `const` or `type`: the keywords after +/// which a name is a declaration head, whatever follows it? +fn after_decl_keyword(t: []const u8, p: usize) -> bool { + var e : usize = p; + while (e > 0 and type_text::ws_before(t, 0, e) > 0) { e -= 1; } + var s : usize = e; + while (s > 0 and type_text::word(t[s - 1])) { s -= 1; } + return type_text::starts(t, s, e, "const") and e - s == 5 or type_text::starts(t, s, e, "type") and e - s == 4; +} + +/// Is the word t[i .. j] a use of the name, as opposed to a member (`.name`, `::name`, `@name`) or a name in a +/// declaration position (`name: T`, `name = v` after anything but `const` or `type`)? +fn is_use(t: []const u8, i: usize, j: usize) -> bool { + if (i > 0 and (t[i - 1] == type_text::B_DOT or t[i - 1] == B_AT)) { return false; } + if (i > 1 and t[i - 1] == type_text::B_COLON and t[i - 2] == type_text::B_COLON) { return false; } + const k : usize = skip_blank(t, j); + if (k >= t.len) { return true; } + const single_colon : bool = t[k] == type_text::B_COLON and !(k + 1 < t.len and t[k + 1] == type_text::B_COLON); + const single_eq : bool = t[k] == B_EQ and !(k + 1 < t.len and t[k + 1] == B_EQ); + if (single_colon or single_eq) { return after_decl_keyword(t, i); } + return true; +} + +/// R3. `text` with every use of `name` renamed to its flat name as `origin` declares it, written to +/// out[0 .. cap]; the result is the length it needs. See the header for what is left alone. +pub fn rename_ident(text: []const u8, name: []const u8, origin: []const u8, out: []u8, cap: usize) -> usize { + var w : usize = 0; + var i : usize = 0; + var bol : bool = true; + while (i < text.len) { + const c : u8 = text[i]; + if (c == B_NL) { + w = put(out, cap, w, c); + bol = true; + i += 1; + } else if (bol and (c == type_text::B_SPACE or c == type_text::B_TAB)) { + w = put(out, cap, w, c); + i += 1; + } else if ((bol and c == type_text::B_SEMI) or (c == type_text::B_SLASH and i + 1 < text.len and text[i + 1] == type_text::B_SLASH)) { + while (i < text.len and text[i] != B_NL) { + w = put(out, cap, w, text[i]); + i += 1; + } + } else if (c == B_DQUOTE or c == B_SQUOTE) { + bol = false; + w = put(out, cap, w, c); + i += 1; + while (i < text.len and text[i] != c and text[i] != B_NL) { + if (text[i] == B_BSLASH and i + 1 < text.len) { + w = put(out, cap, w, text[i]); + i += 1; + } + w = put(out, cap, w, text[i]); + i += 1; + } + if (i < text.len and text[i] == c) { + w = put(out, cap, w, c); + i += 1; + } + } else if (type_text::word(c)) { + bol = false; + var j : usize = i; + while (j < text.len and type_text::word(text[j])) { j += 1; } + if (same_span(text, i, j, name, 0, name.len) and is_use(text, i, j)) { + w = put_flat(origin, name, out, cap, w); + } else { + var k : usize = i; + while (k < j) { + w = put(out, cap, w, text[k]); + k += 1; + } + } + i = j; + } else { + bol = false; + w = put(out, cap, w, c); + i += 1; + } + } + return w; +} + +test same_decl_ignores_indentation_comments_and_blank_lines { + assert same_decl("pub const Trit = enum(i8) {\n neg = -1,\n};", "pub const Trit = enum(i8) {\n neg = -1,\n};"); + assert same_decl("pub const T = enum(i8) {\n neg = -1,\n};", " pub const T = enum(i8) {\n neg = -1,\n };"); + assert same_decl("pub const ONE : i8 = 1;", "pub const ONE : i8 = 1; // Trit = +1"); + assert same_decl("pub const A = 1;", "\npub const A = 1;\n\n"); + assert same_decl("pub const A = 1;", "pub const A = 1;"); + assert same_decl("", ""); +} + +test same_decl_tells_a_real_difference { + assert !same_decl("pub const PHI : f64 = 1.618033988749895;", "pub const PHI : f64 = 1.6180339887;"); + assert !same_decl("pub const E = enum(i8) {\n a = 1,\n b = 2,\n};", "pub const E = enum(i8) {\n a = 1,\n};"); + assert !same_decl("pub const A = 1;", ""); + assert !same_decl("pub const A = 1;", "pub const A = 1;\npub const B = 2;"); +} + +test a_slash_inside_a_string_does_not_start_a_comment { + assert same_decl("const U = \"http://x\"; // note", "const U = \"http://x\";"); + assert !same_decl("const U = \"http://x\";", "const U = \"http://y\";"); + assert !same_decl("const U = \"a\\\"//b\";", "const U = \"a\\\"//c\";"); +} + +test an_alias_names_its_target_module { + assert alias_class("const PHI : f64 = constants::PHI;", "PHI", "constants") == 1; + assert alias_class("pub const PHI : f64 = constants::PHI; // re-export", "PHI", "constants") == 1; + assert alias_class("const PHI : f64 =\n constants::PHI;", "PHI", "constants") == 1; + assert alias_class("const PHI : f64 = constants::PHI;", "PHI", "other") == 2; + assert alias_class("const PHI : f64 = constants::PHI;", "PHI", "") == 2; +} + +test a_value_is_not_an_alias { + assert alias_class("const PHI : f64 = 1.618033988749895;", "PHI", "constants") == 0; + assert alias_class("pub fn f(a: u8) -> u8 {\n return a;\n}", "f", "x") == 0; + assert alias_class("", "PHI", "constants") == 0; +} + +test an_alias_to_a_different_name_is_not_an_alias_for_this_one { + assert alias_class("const PHI : f64 = constants::PHI_INV;", "PHI", "constants") == 0; + assert alias_class("const PHI : f64 = ::PHI;", "PHI", "constants") == 0; + assert alias_class("const PHI : f64 = a b::PHI;", "PHI", "constants") == 0; +} + +test the_scope_of_a_module_is_itself_then_its_imports { + assert scope_index("m/a.t27\nm/b.t27", "m/a.t27", "") == 0; + assert scope_index("m/a.t27\nm/b.t27", "m/b.t27", "") == 1; + assert scope_index("m/a.t27\nm/b.t27", "m/c.t27", "m/b.t27\nm/x.t27") == 1; + assert scope_index("m/a.t27\nm/b.t27", "m/c.t27", "m/x.t27\nm/a.t27") == 0; + assert scope_index("m/a.t27\nm/b.t27", "m/a.t27", "m/b.t27") == 0; +} + +test a_qualified_reference_means_the_module_it_names { + assert scope_index("tri/crypto/sha256.t27\ntri/t27b/txt.t27", "=txt::same", "") == 1; + assert scope_index("tri/crypto/sha256.t27\ntri/t27b/txt.t27", "=sha256.same", "") == 0; + assert scope_index("tri/crypto/sha256.t27\ntri/t27b/txt.t27", "=xt::same", "") == -1; + assert scope_index("tri/crypto/sha256.t27\ntri/t27b/txt.t27", "=txt", "") == -1; + assert scope_index("tri/crypto/sha256.t27\ntri/t27b/txt.t27", "=hex::same", "tri/crypto/sha256.t27") == -1; +} + +test the_importers_own_text_has_no_scope { + assert scope_index("m/a.t27\nm/b.t27", "", "m/a.t27") == -1; + assert scope_index("m/a.t27\nm/b.t27", "", "") == -1; +} + +test a_module_that_imports_both_or_neither_has_no_answer { + assert scope_index("m/a.t27\nm/b.t27", "m/c.t27", "m/a.t27\nm/b.t27") == -1; + assert scope_index("m/a.t27\nm/b.t27", "m/c.t27", "") == -1; + assert scope_index("m/a.t27\nm/b.t27", "m/c.t27", "m/x.t27") == -1; + assert scope_index("", "m/c.t27", "m/c.t27") == -1; +} + +test a_flat_name_carries_the_module_path { + var out : [64]u8 = undefined; + const m : usize = flat_name("t27mcp/service.t27", "name_ok", out[0 .. 64], 64); + assert same_span(out[0 .. 64], 0, m, "t27mcp_service__name_ok", 0, 23); + assert m == 23; + const k : usize = flat_name("port/a-b/x.y.t27", "K", out[0 .. 64], 64); + assert same_span(out[0 .. 64], 0, k, "port_a_b_x_y__K", 0, 15); + assert k == 15; +} + +test a_flat_name_reports_the_size_it_needs { + var out : [4]u8 = undefined; + assert flat_name("m/a.t27", "name_ok", out[0 .. 4], 4) == 12; +} + +test rename_replaces_the_head_and_every_call { + var out : [256]u8 = undefined; + const src : str = "pub fn name_ok(n: u32) -> bool {\n return n > 0;\n}\n\npub fn admit(n: u32) -> u32 {\n if (name_ok(n) == false) { return 3; }\n return name_ok(n) and 1;\n}\n"; + const want : str = "pub fn m_a__name_ok(n: u32) -> bool {\n return n > 0;\n}\n\npub fn admit(n: u32) -> u32 {\n if (m_a__name_ok(n) == false) { return 3; }\n return m_a__name_ok(n) and 1;\n}\n"; + const m : usize = rename_ident(src, "name_ok", "m/a.t27", out[0 .. 256], 256); + assert m == want.len; + assert same_span(out[0 .. 256], 0, m, want, 0, want.len); +} + +test rename_leaves_a_longer_word_a_member_a_comment_and_a_string_alone { + var out : [256]u8 = undefined; + const src : str = "fn f() {\n // name_ok here\n ; name_ok in prose\n const s = \"name_ok\";\n a.name_ok = name_ok_more + my_name_ok + @name_ok(1) + q::name_ok;\n return name_ok;\n}\n"; + const want : str = "fn f() {\n // name_ok here\n ; name_ok in prose\n const s = \"name_ok\";\n a.name_ok = name_ok_more + my_name_ok + @name_ok(1) + q::name_ok;\n return M__name_ok;\n}\n"; + const m : usize = rename_ident(src, "name_ok", "M.t27", out[0 .. 256], 256); + assert m == want.len; + assert same_span(out[0 .. 256], 0, m, want, 0, want.len); +} + +test rename_leaves_a_field_a_parameter_and_a_variant_alone { + var out : [256]u8 = undefined; + const src : str = "pub const S = struct {\n kind: u32,\n};\npub const E = enum(u8) {\n kind = 1,\n};\nfn g(kind: u32) -> u32 { return kind; }\n"; + const want : str = "pub const S = struct {\n kind: u32,\n};\npub const E = enum(u8) {\n kind = 1,\n};\nfn g(kind: u32) -> u32 { return M__kind; }\n"; + const m : usize = rename_ident(src, "kind", "M.t27", out[0 .. 256], 256); + assert m == want.len; + assert same_span(out[0 .. 256], 0, m, want, 0, want.len); +} + +test rename_renames_a_const_head_and_its_uses { + var out : [128]u8 = undefined; + const src : str = "pub const K : u8 = 3;\nconst J = K + 1;\ntype T = K;\n"; + const want : str = "pub const x__K : u8 = 3;\nconst J = x__K + 1;\ntype T = x__K;\n"; + const m : usize = rename_ident(src, "K", "x.t27", out[0 .. 128], 128); + assert m == want.len; + assert same_span(out[0 .. 128], 0, m, want, 0, want.len); +} + +test rename_keeps_a_comparison_and_an_escaped_quote { + var out : [128]u8 = undefined; + const src : str = "fn h() { if (K == 1 or K != 2) { return \"a\\\"K\"; } }\n"; + const want : str = "fn h() { if (z__K == 1 or z__K != 2) { return \"a\\\"K\"; } }\n"; + const m : usize = rename_ident(src, "K", "z.t27", out[0 .. 128], 128); + assert m == want.len; + assert same_span(out[0 .. 128], 0, m, want, 0, want.len); +} + +test rename_reports_the_size_it_needs { + var out : [8]u8 = undefined; + assert rename_ident("a(K) + K", "K", "m.t27", out[0 .. 8], 8) == 14; +} From c8080b8159910aa6a3501b8e9b0ece5bdf09c5c1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 15:50:15 +0700 Subject: [PATCH 2/5] t27c: a path a pulled declaration writes loses its module; one rewrite pass in use_splice.t27 renames scoped helpers and strips imported paths (Closes #10556) compound_binop, pulled from operator_text into a converted compiler.rs, kept `type_text::same`; the flat file declared no `type_text`, so `tri t27o --check` stopped on it (BLOCKED, undeclared identifier). The importer's own paths were always flattened; pulled ones were not. use_splice.rewrite now strips `stem::` in a pulled text when the stem is a module that text's module imports, and applies the scoped renames in the same pass. rename_ident is folded into it. Co-Authored-By: Claude Sonnet 5.5 --- bootstrap/gen/rust/compiler/use_splice.rs | 82 ++++++--- bootstrap/src/use_resolve.rs | 25 ++- bootstrap/tests/unresolved_use.rs | 28 +-- ...e-splice-reads-a-name-in-its-own-module.md | 4 + specs/compiler/use_splice.t27 | 160 ++++++++++++++---- 5 files changed, 221 insertions(+), 78 deletions(-) diff --git a/bootstrap/gen/rust/compiler/use_splice.rs b/bootstrap/gen/rust/compiler/use_splice.rs index 10443736ca..59bdcaed19 100644 --- a/bootstrap/gen/rust/compiler/use_splice.rs +++ b/bootstrap/gen/rust/compiler/use_splice.rs @@ -1,6 +1,6 @@ // Generated from .t27 spec // DO NOT EDIT — generated by t27c -// NOT LOWERED BY THIS BACKEND: 18 test(s), 0 invariant(s), 0 function(s) with no body. +// NOT LOWERED BY THIS BACKEND: 22 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. @@ -191,7 +191,7 @@ pub fn has_line(list: &[u8], t: &[u8], lo: usize, hi: usize) -> bool { return false; } -pub fn names_module(p: &[u8], lo: usize, hi: usize, q: &[u8]) -> bool { +pub fn stem_of(p: &[u8], lo: usize, hi: usize) -> i64 { let mut e: usize = hi; if (((e - lo) >= 4) && starts(p, (e - 4), e, ".t27")) { e -= 4; @@ -200,8 +200,13 @@ pub fn names_module(p: &[u8], lo: usize, hi: usize, q: &[u8]) -> bool { while ((s > lo) && (p[((s - 1)) as usize] != B_SLASH)) { s -= 1; } - let n: usize = (e - s); - if ((q.len() <= n) || !(same_span(q, 0, n, p, s, e))) { + return pack(s, e); +} + +pub fn names_module(p: &[u8], lo: usize, hi: usize, q: &[u8]) -> bool { + let st: i64 = stem_of(p, lo, hi); + let n: usize = (hi_of(st) - lo_of(st)); + if ((q.len() <= n) || !(same_span(q, 0, n, p, lo_of(st), hi_of(st)))) { return false; } return ((q[(n) as usize] == B_COLON) || (q[(n) as usize] == B_DOT)); @@ -276,6 +281,14 @@ pub fn skip_blank(t: &[u8], p: usize) -> usize { return i; } +pub fn word_end(t: &[u8], i: usize) -> usize { + let mut j: usize = i; + while ((j < t.len()) && word(t[(j) as usize])) { + j += 1; + } + return j; +} + pub fn after_decl_keyword(t: &[u8], p: usize) -> bool { let mut e: usize = p; while ((e > 0) && (ws_before(t, 0, e) > 0)) { @@ -288,26 +301,53 @@ pub fn after_decl_keyword(t: &[u8], p: usize) -> bool { return ((starts(t, s, e, "const") && ((e - s) == 5)) || (starts(t, s, e, "type") && ((e - s) == 4))); } -pub fn is_use(t: &[u8], i: usize, j: usize) -> bool { +pub fn is_member(t: &[u8], i: usize) -> bool { if ((i > 0) && ((t[((i - 1)) as usize] == B_DOT) || (t[((i - 1)) as usize] == B_AT))) { - return false; - } - if (((i > 1) && (t[((i - 1)) as usize] == B_COLON)) && (t[((i - 2)) as usize] == B_COLON)) { - return false; + return true; } + return (((i > 1) && (t[((i - 1)) as usize] == B_COLON)) && (t[((i - 2)) as usize] == B_COLON)); +} + +pub fn in_declaration(t: &[u8], i: usize, j: usize) -> bool { let k: usize = skip_blank(t, j); if (k >= t.len()) { - return true; + return false; } let single_colon: bool = ((t[(k) as usize] == B_COLON) && !((((k + 1) < t.len()) && (t[((k + 1)) as usize] == B_COLON)))); let single_eq: bool = ((t[(k) as usize] == B_EQ) && !((((k + 1) < t.len()) && (t[((k + 1)) as usize] == B_EQ)))); - if (single_colon || single_eq) { - return after_decl_keyword(t, i); + return ((single_colon || single_eq) && !(after_decl_keyword(t, i))); +} + +pub fn is_stem(list: &[u8], t: &[u8], i: usize, j: usize) -> bool { + let mut s: usize = 0; + while (s < list.len()) { + let e: usize = line_end(list, s); + let st: i64 = stem_of(list, s, e); + if same_span(list, lo_of(st), hi_of(st), t, i, j) { + return true; + } + s = (e + 1); } - return true; + return false; +} + +pub fn rename_of(renames: &[u8], t: &[u8], i: usize, j: usize) -> i64 { + let mut s: usize = 0; + while (s < renames.len()) { + let e: usize = line_end(renames, s); + let mut tab: usize = s; + while ((tab < e) && (renames[(tab) as usize] != B_TAB)) { + tab += 1; + } + if ((tab < e) && same_span(renames, s, tab, t, i, j)) { + return pack((tab + 1), e); + } + s = (e + 1); + } + return -(1); } -pub fn rename_ident(text: &[u8], name: &[u8], origin: &[u8], out: &mut [u8], cap: usize) -> usize { +pub fn rewrite(text: &[u8], imports: &[u8], renames: &[u8], out: &mut [u8], cap: usize) -> usize { let mut w: usize = 0; let mut i: usize = 0; let mut bol: bool = true; @@ -347,12 +387,16 @@ pub fn rename_ident(text: &[u8], name: &[u8], origin: &[u8], out: &mut [u8], cap } else { if word(c) { bol = false; - let mut j: usize = i; - while ((j < text.len()) && word(text[(j) as usize])) { - j += 1; + let mut j: usize = word_end(text, i); + let mut bare: bool = false; + if (((((((j + 2) < text.len()) && (text[(j) as usize] == B_COLON)) && (text[((j + 1)) as usize] == B_COLON)) && word(text[((j + 2)) as usize])) && !(is_member(text, i))) && is_stem(imports, text, i, j)) { + i = (j + 2); + j = word_end(text, i); + bare = true; } - if (same_span(text, i, j, name, 0, name.len()) && is_use(text, i, j)) { - w = put_flat(origin, name, out, cap, w); + let r: i64 = rename_of(renames, text, i, j); + if (((r >= 0) && (bare || !(is_member(text, i)))) && !(in_declaration(text, i, j))) { + w = put_flat(&renames[(lo_of(r)) as usize..(hi_of(r)) as usize], &text[(i) as usize..(j) as usize], out, cap, w); } else { let mut k: usize = i; while (k < j) { diff --git a/bootstrap/src/use_resolve.rs b/bootstrap/src/use_resolve.rs index 7fbafb0204..1a1a3b4db5 100644 --- a/bootstrap/src/use_resolve.rs +++ b/bootstrap/src/use_resolve.rs @@ -606,10 +606,10 @@ fn consumed_use(line: &str, specs_root: &Path, pulled: &HashSet) -> Opti Some(format!("{}use {}{{{}}}; // {}", indent, prefix, kept.join(", "), note)) } -fn renamed(text: &str, name: &str, origin: &str) -> String { - let n = use_splice::rename_ident(text.as_bytes(), name.as_bytes(), origin.as_bytes(), &mut [], 0); - let mut out = vec![0u8; n]; - use_splice::rename_ident(text.as_bytes(), name.as_bytes(), origin.as_bytes(), &mut out, n); +fn sized(f: impl Fn(&mut [u8], usize) -> usize) -> String { + let mut out = vec![0u8; f(&mut [], 0)]; + let n = out.len(); + f(&mut out, n); String::from_utf8(out).unwrap_or_default() } pub fn resolve(input_path: &Path, source: &str) -> String { @@ -680,8 +680,7 @@ pub fn resolve(input_path: &Path, source: &str) -> String { let mut pulled_names: HashSet = HashSet::new(); let mut ambiguous: Vec<(String, Vec)> = Vec::new(); let mut scoped: HashMap<(String, String), String> = HashMap::new(); - let bare = qualified.iter().fold(source.to_string(), |s, (q, n, _)| s.replace(q.as_str(), &q[..q.len() - n.len()])); - let mut frontier: HashSet<(String, String)> = identifiers(&bare).into_iter().map(|n| (n, String::new())).collect(); + let mut frontier: HashSet<(String, String)> = identifiers(&qualified.iter().fold(source.to_string(), |s, (q, n, _)| s.replace(q.as_str(), &q[..q.len() - n.len()]))).into_iter().map(|n| (n, String::new())).collect(); for (q, name, _) in &qualified { frontier.insert((name.clone(), format!("={}", q))); } @@ -728,11 +727,8 @@ pub fn resolve(input_path: &Path, source: &str) -> String { origins.sort(); let at = use_splice::scope_index(origins.join("\n").as_bytes(), ctx.as_bytes(), imports.get(&ctx).map_or("", String::as_str).as_bytes()); if let Some(d) = usize::try_from(at).ok().and_then(|i| distinct.get(i)) { - for c in [&ctx, &d.origin] { scoped.insert((c.clone(), name.clone()), d.origin.clone()); } - if pulled_names.insert(format!("{}::{}", d.origin, name)) { - next.extend(identifiers(&d.text).into_iter().map(|n| (n, d.origin.clone()))); - pulled.push((*d).clone()); - } + scoped.insert((ctx.clone(), name.clone()), d.origin.clone()); + if pulled_names.insert(format!("{}::{}", d.origin, name)) { next.extend(identifiers(&d.text).into_iter().map(|n| (n, d.origin.clone()))); pulled.push((*d).clone()); } continue; } ambiguous.push((name.clone(), origins)); @@ -791,7 +787,7 @@ pub fn resolve(input_path: &Path, source: &str) -> String { // local one; otherwise the qualifier stays, the call binds to no local // function, and the arity check is skipped by non-match rather than run // against a namesake. Non-call spellings keep the old flattening. - let flat = |q: &String, n: &String| scoped.get(&(format!("={}", q), n.clone())).map(|o| renamed(n, n, o)); + let flat = |q: &String, n: &String| scoped.get(&(format!("={}", q), n.clone())).map(|o| sized(|b, c| use_splice::flat_name(o.as_bytes(), n.as_bytes(), b, c))); let mut rewrites: Vec<&(String, String, bool)> = qualified .iter() .filter(|(q, name, is_call)| { @@ -802,7 +798,10 @@ pub fn resolve(input_path: &Path, source: &str) -> String { for (qual, name, _) in rewrites { out = out.replace(qual.as_str(), &flat(qual, name).unwrap_or_else(|| name.clone())); } - scoped.iter().for_each(|((c, x), o)| pulled.iter_mut().filter(|d| d.origin == *c).for_each(|d| d.text = renamed(&d.text, x, o))); + for d in pulled.iter_mut() { + let ren: String = scoped.iter().filter(|((c, x), o)| *c == d.origin || (*x == d.name && **o == d.origin)).map(|((_, x), o)| format!("{x}\t{o}\n")).collect(); + d.text = sized(|b, n| use_splice::rewrite(d.text.as_bytes(), imports.get(&d.origin).map_or("", String::as_str).as_bytes(), ren.as_bytes(), b, n)); + } if !out.ends_with('\n') { out.push('\n'); } diff --git a/bootstrap/tests/unresolved_use.rs b/bootstrap/tests/unresolved_use.rs index 4119449920..af308bb301 100644 --- a/bootstrap/tests/unresolved_use.rs +++ b/bootstrap/tests/unresolved_use.rs @@ -82,34 +82,40 @@ fn a_use_resolves_however_the_spec_path_is_spelled() { } } -/// #10556: modules a and b each declare a helper `ok`; `gen` of an importer, whose body is `body`. -fn two_helpers(tag: &str, body: &str) -> (bool, String, String) { +/// #10556: write specs/r/.t27 for each file and gen the last one. +fn gen_tree(tag: &str, files: &[(&str, &str)]) -> (bool, String, String) { let root = scratch(tag).join("specs/r"); std::fs::create_dir_all(&root).expect("dir"); - let a = "module a;\npub fn ok(n: u32) -> bool {\n return n > 0;\n}\npub fn admit(n: u32) -> bool {\n return ok(n);\n}\n"; - let b = "module b;\npub fn ok(k: u32, f: bool) -> bool {\n return k > 0 or f;\n}\npub fn fine(k: u32) -> bool {\n return ok(k, false);\n}\n"; - for (n, t) in [("a", a), ("b", b), ("imp", &format!("module imp;\nuse r::a;\nuse r::b;\n\npub fn s(n: u32) -> bool {{\n return {body};\n}}\n"))] { - std::fs::write(root.join(format!("{n}.t27")), t).expect("write"); - } - gen(&root.join("imp.t27")) + for (n, t) in files { std::fs::write(root.join(format!("{n}.t27")), t).expect("write"); } + gen(&root.join(format!("{}.t27", files.last().expect("files").0))) } +const A: &str = "module a;\npub fn ok(n: u32) -> bool {\n return n > 0;\n}\npub fn admit(n: u32) -> bool {\n return ok(n);\n}\n"; +const B: &str = "module b;\npub fn ok(k: u32, f: bool) -> bool {\n return k > 0 or f;\n}\npub fn fine(k: u32) -> bool {\n return ok(k, false);\n}\n"; +fn imp(body: &str) -> String { format!("module imp;\nuse r::a;\nuse r::b;\n\npub fn s(n: u32) -> bool {{\n return {body};\n}}\n") } #[test] fn a_helper_two_modules_declare_stays_with_the_module_that_means_it() { - let (ok, out, err) = two_helpers("scope", "admit(n) and b::fine(n)"); + let (ok, out, err) = gen_tree("scope", &[("a", A), ("b", B), ("imp", &imp("admit(n) and b::fine(n)"))]); assert!(ok && !err.contains("UNRESOLVED") && !out.contains("UNRESOLVED"), "{err}"); for want in ["fn r_a__ok(", "fn r_b__ok(", "return r_a__ok(n);", "return r_b__ok(k, false);"] { assert!(out.contains(want), "{want}:\n{out}"); } } #[test] fn a_qualified_helper_takes_the_flat_name_of_the_module_it_names() { - let (_, out, err) = two_helpers("qualified", "b::ok(n, true)"); + let (_, out, err) = gen_tree("qualified", &[("a", A), ("b", B), ("imp", &imp("b::ok(n, true)"))]); assert!(!err.contains("UNRESOLVED") && out.contains("return r_b__ok(n, true);") && !out.contains("r_a__ok"), "{err}\n{out}"); } #[test] fn a_name_the_importer_writes_that_two_modules_declare_is_still_refused() { - let (ok, out, err) = two_helpers("refused", "ok(n, true)"); + let (ok, out, err) = gen_tree("refused", &[("a", A), ("b", B), ("imp", &imp("ok(n, true)"))]); assert!(ok && err.contains("UNRESOLVED ok: declared in r/a.t27 and r/b.t27"), "{err}"); assert!(!out.contains("r_a__ok") && !out.contains("fn ok("), "{out}"); } + +#[test] +fn a_path_in_a_pulled_declaration_loses_its_module() { + let c = "module c;\nuse r::a;\npub fn f(n: u32) -> bool {\n return a::admit(n);\n}\n"; + let (ok, out, err) = gen_tree("path", &[("a", A), ("c", c), ("imp", "module imp;\nuse r::c;\n\npub fn s(n: u32) -> bool {\n return c::f(n);\n}\n")]); + assert!(ok && out.contains("return admit(n);") && !out.contains("a.admit("), "{err}\n{out}"); +} diff --git a/docs/now/2026-10-19-use-splice-reads-a-name-in-its-own-module.md b/docs/now/2026-10-19-use-splice-reads-a-name-in-its-own-module.md index 42959f0ccd..b348bb907b 100644 --- a/docs/now/2026-10-19-use-splice-reads-a-name-in-its-own-module.md +++ b/docs/now/2026-10-19-use-splice-reads-a-name-in-its-own-module.md @@ -10,6 +10,10 @@ module's own declaration, else the one its direct imports make). A qualified reference the importer writes (`beta::name_ok`) is read in beta. Both copies are spliced under `__` and the uses inside the module's texts are renamed. A bare name the importer writes stays UNRESOLVED. +- Also: a path a pulled declaration writes (`type_text::same` in `compound_binop`, pulled from operator_text) loses + its module when the declaration's module imports it. The importer's own paths always did; pulled ones kept the + qualifier and the file declared no `type_text`. This, not only the clash, is what stopped `tri t27o --check` + on `bootstrap/src/compiler.rs`. - The rule is `specs/compiler/use_splice.t27`; `t27c gen-rust` writes `bootstrap/gen/rust/compiler/use_splice.rs`, which use_resolve.rs mounts by `#[path]`. `all_agree` and `alias_target` moved there too, so the hand Rust only shrinks. diff --git a/specs/compiler/use_splice.t27 b/specs/compiler/use_splice.t27 index e8deef3d08..823bb8ca07 100644 --- a/specs/compiler/use_splice.t27 +++ b/specs/compiler/use_splice.t27 @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 ; specs/compiler/use_splice.t27 -- how the `use` splice reads a name that two imported modules declare -; (Closes #10556), and the two agreement rules it already had (`all_agree`, `alias_resolved`). +; (Closes #10556), a path a pulled declaration writes, and the two agreement rules it already had +; (`all_agree`, `alias_resolved`). ; -- ; The splice (bootstrap/src/use_resolve.rs) pulls into the importer every declaration the importer names, ; then every declaration the pulled text names. It keys the declarations by bare name. When two imported @@ -17,12 +18,16 @@ ; spec that resolved yesterday generates the same bytes today. ; R3 (flat names). The output is one namespace, so two scoped copies of one name cannot both be called ; `name_ok`. A scoped copy is named `__` (`flat_name`) and every use of the name in the -; texts of the module that means it is rewritten (`rename_ident`). A qualified reference the importer -; writes (`beta::name_ok`) is read in beta's scope, so it takes the same flat name. +; texts of the module that means it is rewritten (`rewrite`). A qualified reference the importer writes +; (`beta::name_ok`) is read in beta's scope, so it takes the same flat name. ; R4 (the author's choice stays refused). A bare name the importer writes, declared differently by two ; imported modules, is still UNRESOLVED: no scope applies to it. +; R5 (paths in a pulled text). The flat output has no namespace, so `type_text::same` in a pulled +; declaration of a module that imports type_text loses its `type_text::`, as the importer's own +; qualified references always did (#5574, W588). The declaration it names is spliced beside the text by +; the usual rules; before, the file called `type_text.same` and declared no `type_text`. ; -- -; `rename_ident` is a token rewrite, not a parse. It leaves comments, `;` prose lines, string and character +; `rewrite` is a token rewrite, not a parse. It leaves comments, `;` prose lines, string and character ; literals, `.name` and `::name` (a member or a qualified path), `@name`, and a name in a declaration ; position (`name: T`, `name = v` inside a struct or an enum) alone; a declaration head after `const` or ; `type` is renamed. The Zig the specs lower to forbids a local or a parameter that shadows a top-level @@ -200,14 +205,20 @@ fn has_line(list: []const u8, t: []const u8, lo: usize, hi: usize) -> bool { return false; } -/// Does the qualified spelling q (`m::name` or `m.name`) name the module at path p[lo .. hi], whose stem is m? -fn names_module(p: []const u8, lo: usize, hi: usize, q: []const u8) -> bool { +/// The stem of the module path p[lo .. hi], packed: the bytes between its last `/` and its `.t27`. +fn stem_of(p: []const u8, lo: usize, hi: usize) -> i64 { var e : usize = hi; if (e - lo >= 4 and type_text::starts(p, e - 4, e, ".t27")) { e -= 4; } var s : usize = e; while (s > lo and p[s - 1] != type_text::B_SLASH) { s -= 1; } - const n : usize = e - s; - if (q.len <= n or !same_span(q, 0, n, p, s, e)) { return false; } + return type_text::pack(s, e); +} + +/// Does the qualified spelling q (`m::name` or `m.name`) name the module at path p[lo .. hi], whose stem is m? +fn names_module(p: []const u8, lo: usize, hi: usize, q: []const u8) -> bool { + const st : i64 = stem_of(p, lo, hi); + const n : usize = hi_of(st) - lo_of(st); + if (q.len <= n or !same_span(q, 0, n, p, lo_of(st), hi_of(st))) { return false; } return q[n] == type_text::B_COLON or q[n] == type_text::B_DOT; } @@ -278,6 +289,13 @@ fn skip_blank(t: []const u8, p: usize) -> usize { return i; } +/// The end of the word that starts at t[i]. +fn word_end(t: []const u8, i: usize) -> usize { + var j : usize = i; + while (j < t.len and type_text::word(t[j])) { j += 1; } + return j; +} + /// Is the word that ends just before p (blanks and newlines between) `const` or `type`: the keywords after /// which a name is a declaration head, whatever follows it? fn after_decl_keyword(t: []const u8, p: usize) -> bool { @@ -288,22 +306,54 @@ fn after_decl_keyword(t: []const u8, p: usize) -> bool { return type_text::starts(t, s, e, "const") and e - s == 5 or type_text::starts(t, s, e, "type") and e - s == 4; } -/// Is the word t[i .. j] a use of the name, as opposed to a member (`.name`, `::name`, `@name`) or a name in a -/// declaration position (`name: T`, `name = v` after anything but `const` or `type`)? -fn is_use(t: []const u8, i: usize, j: usize) -> bool { - if (i > 0 and (t[i - 1] == type_text::B_DOT or t[i - 1] == B_AT)) { return false; } - if (i > 1 and t[i - 1] == type_text::B_COLON and t[i - 2] == type_text::B_COLON) { return false; } +/// Is the word at t[i] a member or a path part (`.name`, `::name`, `@name`) rather than a name of its own? +fn is_member(t: []const u8, i: usize) -> bool { + if (i > 0 and (t[i - 1] == type_text::B_DOT or t[i - 1] == B_AT)) { return true; } + return i > 1 and t[i - 1] == type_text::B_COLON and t[i - 2] == type_text::B_COLON; +} + +/// Is the word t[i .. j] in a declaration position (`name: T`, `name = v`) unless it follows `const` or `type`? +fn in_declaration(t: []const u8, i: usize, j: usize) -> bool { const k : usize = skip_blank(t, j); - if (k >= t.len) { return true; } + if (k >= t.len) { return false; } const single_colon : bool = t[k] == type_text::B_COLON and !(k + 1 < t.len and t[k + 1] == type_text::B_COLON); const single_eq : bool = t[k] == B_EQ and !(k + 1 < t.len and t[k + 1] == B_EQ); - if (single_colon or single_eq) { return after_decl_keyword(t, i); } - return true; + return (single_colon or single_eq) and !after_decl_keyword(t, i); +} + +/// Is the word t[i .. j] the stem of a module that `list` (paths, one to a line) names? +fn is_stem(list: []const u8, t: []const u8, i: usize, j: usize) -> bool { + var s : usize = 0; + while (s < list.len) { + const e : usize = line_end(list, s); + const st : i64 = stem_of(list, s, e); + if (same_span(list, lo_of(st), hi_of(st), t, i, j)) { return true; } + s = e + 1; + } + return false; +} + +/// The origin that `renames` (`name TAB origin`, one to a line) gives the word t[i .. j], packed; -1 for none. +fn rename_of(renames: []const u8, t: []const u8, i: usize, j: usize) -> i64 { + var s : usize = 0; + while (s < renames.len) { + const e : usize = line_end(renames, s); + var tab : usize = s; + while (tab < e and renames[tab] != type_text::B_TAB) { tab += 1; } + if (tab < e and same_span(renames, s, tab, t, i, j)) { return type_text::pack(tab + 1, e); } + s = e + 1; + } + return -1; } -/// R3. `text` with every use of `name` renamed to its flat name as `origin` declares it, written to -/// out[0 .. cap]; the result is the length it needs. See the header for what is left alone. -pub fn rename_ident(text: []const u8, name: []const u8, origin: []const u8, out: []u8, cap: usize) -> usize { +/// R3, and the qualifier rule. `text` is the text of one pulled declaration of a module whose direct imports +/// are listed in `imports` (paths, one to a line), written to out[0 .. cap]; the result is the length it needs. +/// - A path `stem::name` whose stem is one of those modules loses the stem: the flat output has no namespace, +/// and the declaration `name` means is spliced beside this text (a pulled fn that called `type_text::same` +/// called nothing the file declared). +/// - A word in `renames` (`name TAB origin`, one to a line) becomes its flat name as that origin declares it. +/// Everything else is copied byte for byte, and see the header for what counts as a use. +pub fn rewrite(text: []const u8, imports: []const u8, renames: []const u8, out: []u8, cap: usize) -> usize { var w : usize = 0; var i : usize = 0; var bol : bool = true; @@ -339,10 +389,16 @@ pub fn rename_ident(text: []const u8, name: []const u8, origin: []const u8, out: } } else if (type_text::word(c)) { bol = false; - var j : usize = i; - while (j < text.len and type_text::word(text[j])) { j += 1; } - if (same_span(text, i, j, name, 0, name.len) and is_use(text, i, j)) { - w = put_flat(origin, name, out, cap, w); + var j : usize = word_end(text, i); + var bare : bool = false; + if (j + 2 < text.len and text[j] == type_text::B_COLON and text[j + 1] == type_text::B_COLON and type_text::word(text[j + 2]) and !is_member(text, i) and is_stem(imports, text, i, j)) { + i = j + 2; + j = word_end(text, i); + bare = true; + } + const r : i64 = rename_of(renames, text, i, j); + if (r >= 0 and (bare or !is_member(text, i)) and !in_declaration(text, i, j)) { + w = put_flat(renames[lo_of(r) .. hi_of(r)], text[i .. j], out, cap, w); } else { var k : usize = i; while (k < j) { @@ -445,52 +501,86 @@ test a_flat_name_reports_the_size_it_needs { assert flat_name("m/a.t27", "name_ok", out[0 .. 4], 4) == 12; } -test rename_replaces_the_head_and_every_call { +test rewrite_renames_the_head_and_every_call { var out : [256]u8 = undefined; const src : str = "pub fn name_ok(n: u32) -> bool {\n return n > 0;\n}\n\npub fn admit(n: u32) -> u32 {\n if (name_ok(n) == false) { return 3; }\n return name_ok(n) and 1;\n}\n"; const want : str = "pub fn m_a__name_ok(n: u32) -> bool {\n return n > 0;\n}\n\npub fn admit(n: u32) -> u32 {\n if (m_a__name_ok(n) == false) { return 3; }\n return m_a__name_ok(n) and 1;\n}\n"; - const m : usize = rename_ident(src, "name_ok", "m/a.t27", out[0 .. 256], 256); + const m : usize = rewrite(src, "", "name_ok\tm/a.t27\n", out[0 .. 256], 256); assert m == want.len; assert same_span(out[0 .. 256], 0, m, want, 0, want.len); } -test rename_leaves_a_longer_word_a_member_a_comment_and_a_string_alone { +test rewrite_leaves_a_longer_word_a_member_a_comment_and_a_string_alone { var out : [256]u8 = undefined; const src : str = "fn f() {\n // name_ok here\n ; name_ok in prose\n const s = \"name_ok\";\n a.name_ok = name_ok_more + my_name_ok + @name_ok(1) + q::name_ok;\n return name_ok;\n}\n"; const want : str = "fn f() {\n // name_ok here\n ; name_ok in prose\n const s = \"name_ok\";\n a.name_ok = name_ok_more + my_name_ok + @name_ok(1) + q::name_ok;\n return M__name_ok;\n}\n"; - const m : usize = rename_ident(src, "name_ok", "M.t27", out[0 .. 256], 256); + const m : usize = rewrite(src, "", "name_ok\tM.t27\n", out[0 .. 256], 256); assert m == want.len; assert same_span(out[0 .. 256], 0, m, want, 0, want.len); } -test rename_leaves_a_field_a_parameter_and_a_variant_alone { +test rewrite_leaves_a_field_a_parameter_and_a_variant_alone { var out : [256]u8 = undefined; const src : str = "pub const S = struct {\n kind: u32,\n};\npub const E = enum(u8) {\n kind = 1,\n};\nfn g(kind: u32) -> u32 { return kind; }\n"; const want : str = "pub const S = struct {\n kind: u32,\n};\npub const E = enum(u8) {\n kind = 1,\n};\nfn g(kind: u32) -> u32 { return M__kind; }\n"; - const m : usize = rename_ident(src, "kind", "M.t27", out[0 .. 256], 256); + const m : usize = rewrite(src, "", "kind\tM.t27\n", out[0 .. 256], 256); assert m == want.len; assert same_span(out[0 .. 256], 0, m, want, 0, want.len); } -test rename_renames_a_const_head_and_its_uses { +test rewrite_renames_a_const_head_and_its_uses { var out : [128]u8 = undefined; const src : str = "pub const K : u8 = 3;\nconst J = K + 1;\ntype T = K;\n"; const want : str = "pub const x__K : u8 = 3;\nconst J = x__K + 1;\ntype T = x__K;\n"; - const m : usize = rename_ident(src, "K", "x.t27", out[0 .. 128], 128); + const m : usize = rewrite(src, "", "K\tx.t27\n", out[0 .. 128], 128); assert m == want.len; assert same_span(out[0 .. 128], 0, m, want, 0, want.len); } -test rename_keeps_a_comparison_and_an_escaped_quote { +test rewrite_keeps_a_comparison_and_an_escaped_quote { var out : [128]u8 = undefined; const src : str = "fn h() { if (K == 1 or K != 2) { return \"a\\\"K\"; } }\n"; const want : str = "fn h() { if (z__K == 1 or z__K != 2) { return \"a\\\"K\"; } }\n"; - const m : usize = rename_ident(src, "K", "z.t27", out[0 .. 128], 128); + const m : usize = rewrite(src, "", "K\tz.t27\n", out[0 .. 128], 128); assert m == want.len; assert same_span(out[0 .. 128], 0, m, want, 0, want.len); } -test rename_reports_the_size_it_needs { +test rewrite_reports_the_size_it_needs { var out : [8]u8 = undefined; - assert rename_ident("a(K) + K", "K", "m.t27", out[0 .. 8], 8) == 14; + assert rewrite("a(K) + K", "", "K\tm.t27\n", out[0 .. 8], 8) == 14; +} + +test rewrite_applies_each_rename_of_its_list { + var out : [128]u8 = undefined; + const want : str = "a__x(a__y, b__z)\n"; + const m : usize = rewrite("x(y, z)\n", "", "x\ta.t27\ny\ta.t27\nz\tb.t27\n", out[0 .. 128], 128); + assert m == want.len; + assert same_span(out[0 .. 128], 0, m, want, 0, want.len); +} + +test rewrite_strips_the_qualifier_of_an_import { + var out : [256]u8 = undefined; + const imports : str = "tri/t27b/txt.t27\nmath/constants.t27\n"; + const src : str = "fn f(a: u8) -> bool {\n return txt::same(a, \"x\") and constants::PHI > 0 and other::same(a) and q.txt::same(a) and txt::;\n}\n"; + const want : str = "fn f(a: u8) -> bool {\n return same(a, \"x\") and PHI > 0 and other::same(a) and q.txt::same(a) and txt::;\n}\n"; + const m : usize = rewrite(src, imports, "", out[0 .. 256], 256); + assert m == want.len; + assert same_span(out[0 .. 256], 0, m, want, 0, want.len); +} + +test a_stripped_name_is_renamed_like_a_bare_one { + var out : [128]u8 = undefined; + const want : str = "return a__same(x);\n"; + const m : usize = rewrite("return txt::same(x);\n", "tri/txt.t27\n", "same\ta.t27\n", out[0 .. 128], 128); + assert m == want.len; + assert same_span(out[0 .. 128], 0, m, want, 0, want.len); +} + +test a_text_with_nothing_to_rewrite_comes_back_as_it_was { + var out : [64]u8 = undefined; + const src : str = "pub fn f() -> u8 {\n // txt::same\n return 1;\n}\n"; + const m : usize = rewrite(src, "tri/txt.t27\n", "", out[0 .. 64], 64); + assert m == src.len; + assert same_span(out[0 .. 64], 0, m, src, 0, src.len); } From 52c1c0e4564109317392dc1990efb31a17646600 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 18:05:15 +0700 Subject: [PATCH 3/5] t27c: strip a pulled path only when the splice pulled the name it names; seal use_splice.t27 (Closes #10556) rewrite takes the names the splice pulled and strips `stem::name` in a pulled text only for those, so a path to a type the backends spell themselves (gf16::GF16) is left alone. The seal of specs/compiler/use_splice.t27 is saved with the final spec (23 tests). Co-Authored-By: Claude Sonnet 5.5 --- .trinity/seals/compiler_UseSplice.json | 33 +++++++++++++++ bootstrap/gen/rust/compiler/use_splice.rs | 6 +-- bootstrap/src/use_resolve.rs | 3 +- specs/compiler/use_splice.t27 | 49 ++++++++++++++--------- 4 files changed, 69 insertions(+), 22 deletions(-) create mode 100644 .trinity/seals/compiler_UseSplice.json diff --git a/.trinity/seals/compiler_UseSplice.json b/.trinity/seals/compiler_UseSplice.json new file mode 100644 index 0000000000..e37b3c2cf0 --- /dev/null +++ b/.trinity/seals/compiler_UseSplice.json @@ -0,0 +1,33 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+c8080b815", + "closure": [ + { + "spec": "specs/tri/t27b/type_text.t27", + "spec_hash": "sha256:0c2ea71bc30d0865c2fb2206de7a0499e836bba7da6d1340bb1bef116823f473" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:633cd6987c77cef09985f1049d758d50889dd1e3cf20e03e455f30479df95482", + "gen_hash_rust": "sha256:dff979fde3ecab32314c6dfdf6ac8ab5e6170f8c787b44375f875256ad72cfe2", + "gen_hash_verilog": "sha256:f79922dc6d792d1f1591e0d98ca998de9c4d32961fe93b29de9cd2966242849c", + "gen_hash_zig": "sha256:93ca686b0a6c0d201c140dfd0ac87c419221fec5fee8ca9ae82e1bb2c2e2b3b0", + "module": "UseSplice", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-19T11:04:47Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:2fc45736c0b0936850ac5df3b151c98d96f9615f7ef4c4a9e4bde1a3ec31089f", + "spec_path": "specs/compiler/use_splice.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 23, + "total": 23, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/bootstrap/gen/rust/compiler/use_splice.rs b/bootstrap/gen/rust/compiler/use_splice.rs index 59bdcaed19..fa31b7ffad 100644 --- a/bootstrap/gen/rust/compiler/use_splice.rs +++ b/bootstrap/gen/rust/compiler/use_splice.rs @@ -1,6 +1,6 @@ // Generated from .t27 spec // DO NOT EDIT — generated by t27c -// NOT LOWERED BY THIS BACKEND: 22 test(s), 0 invariant(s), 0 function(s) with no body. +// NOT LOWERED BY THIS BACKEND: 23 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. @@ -347,7 +347,7 @@ pub fn rename_of(renames: &[u8], t: &[u8], i: usize, j: usize) -> i64 { return -(1); } -pub fn rewrite(text: &[u8], imports: &[u8], renames: &[u8], out: &mut [u8], cap: usize) -> usize { +pub fn rewrite(text: &[u8], imports: &[u8], names: &[u8], renames: &[u8], out: &mut [u8], cap: usize) -> usize { let mut w: usize = 0; let mut i: usize = 0; let mut bol: bool = true; @@ -389,7 +389,7 @@ pub fn rewrite(text: &[u8], imports: &[u8], renames: &[u8], out: &mut [u8], cap: bol = false; let mut j: usize = word_end(text, i); let mut bare: bool = false; - if (((((((j + 2) < text.len()) && (text[(j) as usize] == B_COLON)) && (text[((j + 1)) as usize] == B_COLON)) && word(text[((j + 2)) as usize])) && !(is_member(text, i))) && is_stem(imports, text, i, j)) { + if ((((((((j + 2) < text.len()) && (text[(j) as usize] == B_COLON)) && (text[((j + 1)) as usize] == B_COLON)) && word(text[((j + 2)) as usize])) && !(is_member(text, i))) && is_stem(imports, text, i, j)) && has_line(names, text, (j + 2), word_end(text, (j + 2)))) { i = (j + 2); j = word_end(text, i); bare = true; diff --git a/bootstrap/src/use_resolve.rs b/bootstrap/src/use_resolve.rs index 1a1a3b4db5..465083d941 100644 --- a/bootstrap/src/use_resolve.rs +++ b/bootstrap/src/use_resolve.rs @@ -798,9 +798,10 @@ pub fn resolve(input_path: &Path, source: &str) -> String { for (qual, name, _) in rewrites { out = out.replace(qual.as_str(), &flat(qual, name).unwrap_or_else(|| name.clone())); } + let names: String = pulled_names.iter().map(|n| format!("{n}\n")).chain(scoped.keys().map(|(_, n)| format!("{n}\n"))).collect(); for d in pulled.iter_mut() { let ren: String = scoped.iter().filter(|((c, x), o)| *c == d.origin || (*x == d.name && **o == d.origin)).map(|((_, x), o)| format!("{x}\t{o}\n")).collect(); - d.text = sized(|b, n| use_splice::rewrite(d.text.as_bytes(), imports.get(&d.origin).map_or("", String::as_str).as_bytes(), ren.as_bytes(), b, n)); + d.text = sized(|b, n| use_splice::rewrite(d.text.as_bytes(), imports.get(&d.origin).map_or("", String::as_str).as_bytes(), names.as_bytes(), ren.as_bytes(), b, n)); } if !out.ends_with('\n') { out.push('\n'); diff --git a/specs/compiler/use_splice.t27 b/specs/compiler/use_splice.t27 index 823bb8ca07..1a9759a521 100644 --- a/specs/compiler/use_splice.t27 +++ b/specs/compiler/use_splice.t27 @@ -23,9 +23,10 @@ ; R4 (the author's choice stays refused). A bare name the importer writes, declared differently by two ; imported modules, is still UNRESOLVED: no scope applies to it. ; R5 (paths in a pulled text). The flat output has no namespace, so `type_text::same` in a pulled -; declaration of a module that imports type_text loses its `type_text::`, as the importer's own -; qualified references always did (#5574, W588). The declaration it names is spliced beside the text by -; the usual rules; before, the file called `type_text.same` and declared no `type_text`. +; declaration of a module that imports type_text loses its `type_text::` when the splice pulled `same`, as +; the importer's own qualified references always did (#5574, W588). Before, the file called +; `type_text.same` beside a declared `same` and declared no `type_text`. A path to a name the splice did +; not pull (`gf16::GF16`, a type the backends spell themselves) is left as it was. ; -- ; `rewrite` is a token rewrite, not a parse. It leaves comments, `;` prose lines, string and character ; literals, `.name` and `::name` (a member or a qualified path), `@name`, and a name in a declaration @@ -348,12 +349,13 @@ fn rename_of(renames: []const u8, t: []const u8, i: usize, j: usize) -> i64 { /// R3, and the qualifier rule. `text` is the text of one pulled declaration of a module whose direct imports /// are listed in `imports` (paths, one to a line), written to out[0 .. cap]; the result is the length it needs. -/// - A path `stem::name` whose stem is one of those modules loses the stem: the flat output has no namespace, -/// and the declaration `name` means is spliced beside this text (a pulled fn that called `type_text::same` -/// called nothing the file declared). +/// - A path `stem::name` whose stem is one of those modules and whose `name` is one of the declarations the +/// splice pulled (`names`, one to a line) loses the stem: the flat output has no namespace, and the +/// declaration it means is spliced beside this text (a pulled fn that called `type_text::same` called +/// nothing the file declared). A path to a name the splice did not pull is left as it was. /// - A word in `renames` (`name TAB origin`, one to a line) becomes its flat name as that origin declares it. /// Everything else is copied byte for byte, and see the header for what counts as a use. -pub fn rewrite(text: []const u8, imports: []const u8, renames: []const u8, out: []u8, cap: usize) -> usize { +pub fn rewrite(text: []const u8, imports: []const u8, names: []const u8, renames: []const u8, out: []u8, cap: usize) -> usize { var w : usize = 0; var i : usize = 0; var bol : bool = true; @@ -391,7 +393,7 @@ pub fn rewrite(text: []const u8, imports: []const u8, renames: []const u8, out: bol = false; var j : usize = word_end(text, i); var bare : bool = false; - if (j + 2 < text.len and text[j] == type_text::B_COLON and text[j + 1] == type_text::B_COLON and type_text::word(text[j + 2]) and !is_member(text, i) and is_stem(imports, text, i, j)) { + if (j + 2 < text.len and text[j] == type_text::B_COLON and text[j + 1] == type_text::B_COLON and type_text::word(text[j + 2]) and !is_member(text, i) and is_stem(imports, text, i, j) and has_line(names, text, j + 2, word_end(text, j + 2))) { i = j + 2; j = word_end(text, i); bare = true; @@ -505,7 +507,7 @@ test rewrite_renames_the_head_and_every_call { var out : [256]u8 = undefined; const src : str = "pub fn name_ok(n: u32) -> bool {\n return n > 0;\n}\n\npub fn admit(n: u32) -> u32 {\n if (name_ok(n) == false) { return 3; }\n return name_ok(n) and 1;\n}\n"; const want : str = "pub fn m_a__name_ok(n: u32) -> bool {\n return n > 0;\n}\n\npub fn admit(n: u32) -> u32 {\n if (m_a__name_ok(n) == false) { return 3; }\n return m_a__name_ok(n) and 1;\n}\n"; - const m : usize = rewrite(src, "", "name_ok\tm/a.t27\n", out[0 .. 256], 256); + const m : usize = rewrite(src, "", "", "name_ok\tm/a.t27\n", out[0 .. 256], 256); assert m == want.len; assert same_span(out[0 .. 256], 0, m, want, 0, want.len); } @@ -514,7 +516,7 @@ test rewrite_leaves_a_longer_word_a_member_a_comment_and_a_string_alone { var out : [256]u8 = undefined; const src : str = "fn f() {\n // name_ok here\n ; name_ok in prose\n const s = \"name_ok\";\n a.name_ok = name_ok_more + my_name_ok + @name_ok(1) + q::name_ok;\n return name_ok;\n}\n"; const want : str = "fn f() {\n // name_ok here\n ; name_ok in prose\n const s = \"name_ok\";\n a.name_ok = name_ok_more + my_name_ok + @name_ok(1) + q::name_ok;\n return M__name_ok;\n}\n"; - const m : usize = rewrite(src, "", "name_ok\tM.t27\n", out[0 .. 256], 256); + const m : usize = rewrite(src, "", "", "name_ok\tM.t27\n", out[0 .. 256], 256); assert m == want.len; assert same_span(out[0 .. 256], 0, m, want, 0, want.len); } @@ -523,7 +525,7 @@ test rewrite_leaves_a_field_a_parameter_and_a_variant_alone { var out : [256]u8 = undefined; const src : str = "pub const S = struct {\n kind: u32,\n};\npub const E = enum(u8) {\n kind = 1,\n};\nfn g(kind: u32) -> u32 { return kind; }\n"; const want : str = "pub const S = struct {\n kind: u32,\n};\npub const E = enum(u8) {\n kind = 1,\n};\nfn g(kind: u32) -> u32 { return M__kind; }\n"; - const m : usize = rewrite(src, "", "kind\tM.t27\n", out[0 .. 256], 256); + const m : usize = rewrite(src, "", "", "kind\tM.t27\n", out[0 .. 256], 256); assert m == want.len; assert same_span(out[0 .. 256], 0, m, want, 0, want.len); } @@ -532,7 +534,7 @@ test rewrite_renames_a_const_head_and_its_uses { var out : [128]u8 = undefined; const src : str = "pub const K : u8 = 3;\nconst J = K + 1;\ntype T = K;\n"; const want : str = "pub const x__K : u8 = 3;\nconst J = x__K + 1;\ntype T = x__K;\n"; - const m : usize = rewrite(src, "", "K\tx.t27\n", out[0 .. 128], 128); + const m : usize = rewrite(src, "", "", "K\tx.t27\n", out[0 .. 128], 128); assert m == want.len; assert same_span(out[0 .. 128], 0, m, want, 0, want.len); } @@ -541,20 +543,20 @@ test rewrite_keeps_a_comparison_and_an_escaped_quote { var out : [128]u8 = undefined; const src : str = "fn h() { if (K == 1 or K != 2) { return \"a\\\"K\"; } }\n"; const want : str = "fn h() { if (z__K == 1 or z__K != 2) { return \"a\\\"K\"; } }\n"; - const m : usize = rewrite(src, "", "K\tz.t27\n", out[0 .. 128], 128); + const m : usize = rewrite(src, "", "", "K\tz.t27\n", out[0 .. 128], 128); assert m == want.len; assert same_span(out[0 .. 128], 0, m, want, 0, want.len); } test rewrite_reports_the_size_it_needs { var out : [8]u8 = undefined; - assert rewrite("a(K) + K", "", "K\tm.t27\n", out[0 .. 8], 8) == 14; + assert rewrite("a(K) + K", "", "", "K\tm.t27\n", out[0 .. 8], 8) == 14; } test rewrite_applies_each_rename_of_its_list { var out : [128]u8 = undefined; const want : str = "a__x(a__y, b__z)\n"; - const m : usize = rewrite("x(y, z)\n", "", "x\ta.t27\ny\ta.t27\nz\tb.t27\n", out[0 .. 128], 128); + const m : usize = rewrite("x(y, z)\n", "", "", "x\ta.t27\ny\ta.t27\nz\tb.t27\n", out[0 .. 128], 128); assert m == want.len; assert same_span(out[0 .. 128], 0, m, want, 0, want.len); } @@ -564,7 +566,7 @@ test rewrite_strips_the_qualifier_of_an_import { const imports : str = "tri/t27b/txt.t27\nmath/constants.t27\n"; const src : str = "fn f(a: u8) -> bool {\n return txt::same(a, \"x\") and constants::PHI > 0 and other::same(a) and q.txt::same(a) and txt::;\n}\n"; const want : str = "fn f(a: u8) -> bool {\n return same(a, \"x\") and PHI > 0 and other::same(a) and q.txt::same(a) and txt::;\n}\n"; - const m : usize = rewrite(src, imports, "", out[0 .. 256], 256); + const m : usize = rewrite(src, imports, "same\nPHI\n", "", out[0 .. 256], 256); assert m == want.len; assert same_span(out[0 .. 256], 0, m, want, 0, want.len); } @@ -572,7 +574,7 @@ test rewrite_strips_the_qualifier_of_an_import { test a_stripped_name_is_renamed_like_a_bare_one { var out : [128]u8 = undefined; const want : str = "return a__same(x);\n"; - const m : usize = rewrite("return txt::same(x);\n", "tri/txt.t27\n", "same\ta.t27\n", out[0 .. 128], 128); + const m : usize = rewrite("return txt::same(x);\n", "tri/txt.t27\n", "same\n", "same\ta.t27\n", out[0 .. 128], 128); assert m == want.len; assert same_span(out[0 .. 128], 0, m, want, 0, want.len); } @@ -580,7 +582,18 @@ test a_stripped_name_is_renamed_like_a_bare_one { test a_text_with_nothing_to_rewrite_comes_back_as_it_was { var out : [64]u8 = undefined; const src : str = "pub fn f() -> u8 {\n // txt::same\n return 1;\n}\n"; - const m : usize = rewrite(src, "tri/txt.t27\n", "", out[0 .. 64], 64); + const m : usize = rewrite(src, "tri/txt.t27\n", "same\n", "", out[0 .. 64], 64); assert m == src.len; assert same_span(out[0 .. 64], 0, m, src, 0, src.len); } + +test a_path_to_a_name_that_was_not_pulled_is_kept { + var out : [128]u8 = undefined; + const src : str = "return txt::same(x) and txt::other(x);\n"; + const want : str = "return same(x) and txt::other(x);\n"; + const m : usize = rewrite(src, "tri/txt.t27\n", "same\n", "", out[0 .. 128], 128); + assert m == want.len; + assert same_span(out[0 .. 128], 0, m, want, 0, want.len); + const k : usize = rewrite(src, "tri/txt.t27\n", "", "", out[0 .. 128], 128); + assert k == src.len; +} From dc42f8140ec8a636d1de2df932531616110cc566 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 18:21:04 +0700 Subject: [PATCH 4/5] docs: the now note for the use splice reads the measured corpus effect (Closes #10556) Co-Authored-By: Claude Sonnet 5.5 --- .../2026-10-19-use-splice-reads-a-name-in-its-own-module.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/now/2026-10-19-use-splice-reads-a-name-in-its-own-module.md b/docs/now/2026-10-19-use-splice-reads-a-name-in-its-own-module.md index b348bb907b..92544f8d5f 100644 --- a/docs/now/2026-10-19-use-splice-reads-a-name-in-its-own-module.md +++ b/docs/now/2026-10-19-use-splice-reads-a-name-in-its-own-module.md @@ -17,5 +17,9 @@ - The rule is `specs/compiler/use_splice.t27`; `t27c gen-rust` writes `bootstrap/gen/rust/compiler/use_splice.rs`, which use_resolve.rs mounts by `#[path]`. `all_agree` and `alias_target` moved there too, so the hand Rust only shrinks. +- Measured: `gen`, `gen-rust`, `gen-c` and `gen-verilog` over the 757 specs that carry a `use` line, base against this + branch: 20 differ (11 had an UNRESOLVED line, 9 named an undeclared path), the other 737 are byte-identical; none of + the 20 compiled before and none stopped compiling. `seal --verify` is identical for all 756 sealed importers, since a + seal hashes the unspliced source, so only the new spec gets a seal. - Trap, not fixed here: `identifiers` reads `;` prose lines, so a comment that names the ambiguous name in the importer (the header of the issue's `importer.t27` does) still prints UNRESOLVED, though the output is right. From bbcb02d8aa82bc2f2d4e6939fb4f90ab4f0610e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 18:21:29 +0700 Subject: [PATCH 5/5] docs: correct the counts in the use splice now note (Closes #10556) Co-Authored-By: Claude Sonnet 5.5 --- ...026-10-19-use-splice-reads-a-name-in-its-own-module.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/now/2026-10-19-use-splice-reads-a-name-in-its-own-module.md b/docs/now/2026-10-19-use-splice-reads-a-name-in-its-own-module.md index 92544f8d5f..7e4edbcdd8 100644 --- a/docs/now/2026-10-19-use-splice-reads-a-name-in-its-own-module.md +++ b/docs/now/2026-10-19-use-splice-reads-a-name-in-its-own-module.md @@ -17,9 +17,9 @@ - The rule is `specs/compiler/use_splice.t27`; `t27c gen-rust` writes `bootstrap/gen/rust/compiler/use_splice.rs`, which use_resolve.rs mounts by `#[path]`. `all_agree` and `alias_target` moved there too, so the hand Rust only shrinks. -- Measured: `gen`, `gen-rust`, `gen-c` and `gen-verilog` over the 757 specs that carry a `use` line, base against this - branch: 20 differ (11 had an UNRESOLVED line, 9 named an undeclared path), the other 737 are byte-identical; none of - the 20 compiled before and none stopped compiling. `seal --verify` is identical for all 756 sealed importers, since a - seal hashes the unspliced source, so only the new spec gets a seal. +- Measured: `gen`, `gen-rust`, `gen-c` and `gen-verilog` over the 756 specs that carry a `use` line (the new spec + aside), base against this branch: 20 differ (11 had an UNRESOLVED line, 9 named an undeclared path), the other 736 + are byte-identical; none of the 20 compiled before and none stopped compiling. `seal --verify` prints the same on + both for all 756, since a seal hashes the unspliced source, so only the new spec gets a seal. - Trap, not fixed here: `identifiers` reads `;` prose lines, so a comment that names the ambiguous name in the importer (the header of the issue's `importer.t27` does) still prints UNRESOLVED, though the output is right.