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 new file mode 100644 index 0000000000..fa31b7ffad --- /dev/null +++ b/bootstrap/gen/rust/compiler/use_splice.rs @@ -0,0 +1,548 @@ +// Generated from .t27 spec +// DO NOT EDIT — generated by t27c +// 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. + +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 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; + } + let mut s: usize = e; + while ((s > lo) && (p[((s - 1)) as usize] != B_SLASH)) { + s -= 1; + } + 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)); +} + +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 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)) { + 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_member(t: &[u8], i: usize) -> bool { + if ((i > 0) && ((t[((i - 1)) as usize] == B_DOT) || (t[((i - 1)) as usize] == B_AT))) { + 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 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)))); + 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 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 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; + 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 = 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)) && has_line(names, text, (j + 2), word_end(text, (j + 2)))) { + i = (j + 2); + j = word_end(text, i); + bare = true; + } + 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) { + 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..465083d941 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 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 { 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,20 @@ 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 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))); } 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 +725,17 @@ 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)) { + 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)); 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,15 +787,21 @@ 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| 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(|(_, 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())); + } + 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(), names.as_bytes(), ren.as_bytes(), b, n)); } 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..af308bb301 100644 --- a/bootstrap/tests/unresolved_use.rs +++ b/bootstrap/tests/unresolved_use.rs @@ -81,3 +81,41 @@ 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: 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"); + 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) = 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) = 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) = 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 new file mode 100644 index 0000000000..7e4edbcdd8 --- /dev/null +++ b/docs/now/2026-10-19-use-splice-reads-a-name-in-its-own-module.md @@ -0,0 +1,25 @@ +# 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. +- 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. +- 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. diff --git a/specs/compiler/use_splice.t27 b/specs/compiler/use_splice.t27 new file mode 100644 index 0000000000..1a9759a521 --- /dev/null +++ b/specs/compiler/use_splice.t27 @@ -0,0 +1,599 @@ +// 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), 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 +; 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 (`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::` 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 +; 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; +} + +/// 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; } + 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; +} + +/// 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; +} + +/// 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 { + 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 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 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); + 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, 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 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, names: []const u8, renames: []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 = 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) and has_line(names, text, j + 2, word_end(text, j + 2))) { + 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) { + 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 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); + assert m == want.len; + assert same_span(out[0 .. 256], 0, m, want, 0, want.len); +} + +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); + assert m == want.len; + assert same_span(out[0 .. 256], 0, m, want, 0, want.len); +} + +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); + assert m == want.len; + assert same_span(out[0 .. 256], 0, m, want, 0, want.len); +} + +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); + assert m == want.len; + assert same_span(out[0 .. 128], 0, m, want, 0, want.len); +} + +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); + 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; +} + +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, "same\nPHI\n", "", 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\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", "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; +}