using+pyvm: fix inlining seam and brace guard; stabilize MiniVm.collect_prints unknown-skip; harden MiniVmPrints int/binop scan; add seam-combiner diagnostics (default OFF); green self-contained + progress on using-mixed

This commit is contained in:
Selfhosting Dev
2025-09-22 03:45:36 +09:00
parent c8063c9e41
commit c9a0333c38
6 changed files with 1229 additions and 123 deletions

View File

@ -20,44 +20,13 @@ static box MiniVm {
}
_str_to_int(s) { return new MiniVmScan()._str_to_int(s) }
_int_to_str(n) { return new MiniVmScan()._int_to_str(n) }
read_digits(json, pos) { return new MiniVmScan().read_digits(json, pos) }
read_digits(json, pos) { return new MiniJson().read_digits_from(json, pos) }
// Read a JSON string starting at position pos (at opening quote); returns the decoded string
read_json_string(json, pos) {
// Expect opening quote
local i = pos
local out = ""
local n = json.length()
if json.substring(i, i+1) == "\"" { i = i + 1 } else { return "" }
loop (i < n) {
local ch = json.substring(i, i+1)
if ch == "\"" { i = i + 1 break }
if ch == "\\" {
// handle simple escapes for \ and "
local nx = json.substring(i+1, i+2)
if nx == "\"" { out = out + "\"" i = i + 2 continue }
if nx == "\\" { out = out + "\\" i = i + 2 continue }
// Unknown escape: skip backslash and take next as-is
i = i + 1
continue
}
out = out + ch
i = i + 1
}
return out
}
read_json_string(json, pos) { return new MiniJson().read_quoted_from(json, pos) }
// helper: find needle from position pos
index_of_from(hay, needle, pos) { return new MiniVmScan().index_of_from(hay, needle, pos) }
// helper: next non-whitespace character index from pos
next_non_ws(json, pos) {
local i = pos
local n = json.length()
loop (i < n) {
local ch = json.substring(i, i+1)
if ch != " " && ch != "\n" && ch != "\r" && ch != "\t" { return i }
i = i + 1
}
return -1
}
next_non_ws(json, pos) { return new MiniJson().next_non_ws(json, pos) }
// ——— Helpers (as box methods) ———
// Minimal: Print(BinaryOp) with operator "+"; supports string+string and int+int
@ -168,17 +137,15 @@ static box MiniVm {
if vpos < 0 { return null }
vpos = vpos + k_val.length()
if ty == "int" || ty == "i64" || ty == "integer" {
// read digits directly
local digits = read_digits(json, vpos)
// read digits via MiniJson
local digits = new MiniJson().read_digits_from(json, vpos)
return digits
}
if ty == "string" {
// Find opening and closing quotes (no escape handling in MVP)
// read quoted via MiniJson
local i = index_of_from(json, "\"", vpos)
if i < 0 { return null }
local j = index_of_from(json, "\"", i+1)
if j < 0 { return null }
return json.substring(i+1, j)
return new MiniJson().read_quoted_from(json, i)
}
// Other types not supported yet
return null
@ -287,6 +254,33 @@ static box MiniVm {
}
// (reserved) helper for future robust binop scan
run(json) {
// entry: attempt minimal quick shapes first, then broader routes
// Quick path: Program-level Print of a single Literal string/int
if json.indexOf("\"kind\":\"Program\"") >= 0 && json.indexOf("\"kind\":\"Print\"") >= 0 {
// Literal string
if json.indexOf("\"expression\":{\"kind\":\"Literal\",\"value\":{\"type\":\"string\"") >= 0 {
local ks = "\"expression\":{\"kind\":\"Literal\",\"value\":{\"type\":\"string\",\"value\":\""
local ps = json.indexOf(ks)
if ps >= 0 {
local si = ps + ks.length()
local sj = json.indexOf("\"", si)
if sj >= 0 { print(json.substring(si, sj)) return 0 }
}
}
// Literal int
if json.indexOf("\"expression\":{\"kind\":\"Literal\",\"value\":{\"type\":\"int\"") >= 0 {
local ki = "\"expression\":{\"kind\":\"Literal\",\"value\":{\"type\":\"int\",\"value\":"
local pi = json.indexOf(ki)
if pi >= 0 {
local ii = pi + ki.length()
// digits until closing brace
local ie = json.indexOf("}", ii)
if ie < 0 { ie = ii }
local d = json.substring(ii, ie)
if d { print(d) return 0 }
}
}
}
// Single-purpose fast path for smoke: if BinaryOp '+' exists, try expression-bounded extractor first.
if json.indexOf("\"BinaryOp\"") >= 0 && json.indexOf("\"operator\":\"+\"") >= 0 {
// Bind to first Print and extract value×2 within expression bounds
@ -534,6 +528,222 @@ static box MiniVm {
}
return 0
}
// Pure helper: collect minimal print outputs (literals only) into an array
collect_prints(json) {
// Ported from self-contained smoke (Hardened minimal scanner)
local out = new ArrayBox()
local pos = 0
local guard = 0
// DEV trace: flip to 1 for one-run diagnosis; keep 0 for normal
local trace = 0
local k_print = "\"kind\":\"Print\""
loop (true) {
guard = guard + 1
if guard > 200 { break }
local p = index_of_from(json, k_print, pos)
if p < 0 { break }
// bound current Print slice to [this, next)
local obj_start = p
local next_p = index_of_from(json, k_print, p + k_print.length())
local obj_end = json.length()
if next_p > 0 { obj_end = next_p }
if trace == 1 { print("[collect][p] "+p) print("[collect][slice_end] "+obj_end) }
if trace == 1 {
local k_expr = "\"expression\":{"
local epos_dbg = index_of_from(json, k_expr, obj_start)
print("[scan][expr] "+epos_dbg)
local fc_dbg = index_of_from(json, "\"kind\":\"FunctionCall\"", obj_start)
print("[scan][fc] "+fc_dbg)
local bo_dbg = index_of_from(json, "\"kind\":\"BinaryOp\"", obj_start)
print("[scan][bo] "+bo_dbg)
local cp_dbg = index_of_from(json, "\"kind\":\"Compare\"", obj_start)
print("[scan][cp] "+cp_dbg)
local ts_dbg = index_of_from(json, "\"type\":\"string\"", obj_start)
print("[scan][ts] "+ts_dbg)
local ti_dbg = index_of_from(json, "\"type\":\"int\"", obj_start)
print("[scan][ti] "+ti_dbg)
// positions for tight patterns used by branches
local ks_pat = "\"type\":\"string\",\"value\":\""
print("[scan][ks] "+index_of_from(json, ks_pat, obj_start))
local ki_pat = "\"type\":\"int\",\"value\":"
print("[scan][ki] "+index_of_from(json, ki_pat, obj_start))
}
// 1) FunctionCall echo/itoa (single literal or empty args)
{
// Limit search within Print.expression object for stability
local k_expr = "\"expression\":{"
local epos = index_of_from(json, k_expr, obj_start)
if epos > 0 { if epos < obj_end {
local expr_start = index_of_from(json, "{", epos)
if expr_start > 0 { if expr_start < obj_end {
local expr_end = new MiniVmScan().find_balanced_object_end(json, expr_start)
if expr_end > 0 { if expr_end <= obj_end {
if trace == 1 { print("[collect][expr] "+expr_start+","+expr_end) }
local k_fc = "\"kind\":\"FunctionCall\""
local fcp = index_of_from(json, k_fc, expr_start)
if fcp > 0 { if fcp < expr_end {
local kn = "\"name\":\""
local np = index_of_from(json, kn, fcp)
if np > 0 { if np < obj_end {
local ni = np + kn.length()
local nj = index_of_from(json, "\"", ni)
if nj > 0 { if nj <= expr_end {
local fname = json.substring(ni, nj)
local ka = "\"arguments\":["
local ap = index_of_from(json, ka, nj)
if ap > 0 { if ap < expr_end {
// detect empty args [] quickly: no type token inside balanced array
local arr_start = index_of_from(json, "[", ap)
local arr_end = new MiniVmScan().find_balanced_array_end(json, arr_start)
if arr_start >= 0 { if arr_end >= 0 { if arr_end <= expr_end {
local kt = "\"type\":\""
local atpos = index_of_from(json, kt, arr_start)
if atpos < 0 || atpos >= arr_end {
if fname == "echo" { out.push("") pos = obj_end + 1 continue }
if fname == "itoa" { out.push("0") pos = obj_end + 1 continue }
}
}}}
// string arg
local ks = "\"type\":\"string\",\"value\":\""
local ps = index_of_from(json, ks, ap)
if ps > 0 { if ps < expr_end {
local si = ps + ks.length()
local sj = index_of_from(json, "\"", si)
if sj > 0 { if sj <= expr_end {
local sval = json.substring(si, sj)
if fname == "echo" { out.push(sval) pos = obj_end + 1 continue }
}}
}}
// int arg
local ki = "\"type\":\"int\",\"value\":"
local pi = index_of_from(json, ki, ap)
if pi > 0 { if pi < expr_end {
local ival = read_digits(json, pi + ki.length())
if ival != "" { if fname == "itoa" { out.push(ival) pos = obj_end + 1 continue } else { if fname == "echo" { out.push(ival) pos = obj_end + 1 continue } } }
}}
}}
}}
}}
}}}
}}
}}
}
// 2) BinaryOp(int '+' int)
{
local k_expr = "\"expression\":{"
local epos = index_of_from(json, k_expr, obj_start)
if epos > 0 { if epos < obj_end {
local k_bo = "\"kind\":\"BinaryOp\""
local bpos = index_of_from(json, k_bo, epos)
if bpos > 0 { if bpos < obj_end {
if index_of_from(json, "\"operator\":\"+\"", bpos) > 0 {
local k_l = "\"left\":{\"kind\":\"Literal\",\"value\":{\"type\":\"int\",\"value\":"
local k_r = "\"right\":{\"kind\":\"Literal\",\"value\":{\"type\":\"int\",\"value\":"
local lp = index_of_from(json, k_l, bpos)
if lp > 0 { if lp < obj_end {
local ld = read_digits(json, lp + k_l.length())
if ld != "" {
local rp = index_of_from(json, k_r, lp + k_l.length())
if rp > 0 { if rp < obj_end {
local rd = read_digits(json, rp + k_r.length())
if rd != "" { if trace == 1 { print("[hit][bo-typed] "+ld+"+"+rd) } out.push(_int_to_str(_str_to_int(ld) + _str_to_int(rd))) pos = p + k_print.length() continue }
}}
}
}}
// fallback: two successive 'value' digits within expression bounds
local k_v = "\"value\":"
local v1 = index_of_from(json, k_v, epos)
if v1 > 0 { if v1 < obj_end {
local d1 = new MiniJson().read_digits_from(json, v1 + k_v.length())
if d1 != "" {
local v2 = index_of_from(json, k_v, v1 + k_v.length())
if v2 > 0 { if v2 < obj_end {
local d2 = new MiniJson().read_digits_from(json, v2 + k_v.length())
if d2 != "" { if trace == 1 { print("[hit][bo-fallback] "+d1+"+"+d2) } out.push(_int_to_str(_str_to_int(d1) + _str_to_int(d2))) pos = p + k_print.length() continue }
}}
}
}}
}
}}
}}
}
// 3) Compare(lhs/rhs ints)
{
local k_cp = "\"kind\":\"Compare\""
local cpos = index_of_from(json, k_cp, obj_start)
if cpos > 0 { if cpos < obj_end {
local k_op = "\"operation\":\""
local opos = index_of_from(json, k_op, cpos)
if opos > 0 { if opos < obj_end {
local oi = opos + k_op.length()
local oj = index_of_from(json, "\"", oi)
if oj > 0 { if oj <= obj_end {
local op = json.substring(oi, oj)
local k_v = "\"value\":"
local lhs_v = index_of_from(json, k_v, oj)
if lhs_v > 0 { if lhs_v < obj_end {
local la = read_digits(json, lhs_v + k_v.length())
if la != "" {
local rhs_v = index_of_from(json, k_v, lhs_v + k_v.length())
if rhs_v > 0 { if rhs_v < obj_end {
local rb = read_digits(json, rhs_v + k_v.length())
if rb != "" {
local ai = _str_to_int(la)
local bi = _str_to_int(rb)
local res = 0
if op == "<" { if ai < bi { res = 1 } }
if op == "==" { if ai == bi { res = 1 } }
if op == "<=" { if ai <= bi { res = 1 } }
if op == ">" { if ai > bi { res = 1 } }
if op == ">=" { if ai >= bi { res = 1 } }
if op == "!=" { if ai != bi { res = 1 } }
out.push(_int_to_str(res))
pos = p + k_print.length()
continue
}
}}
}
}}
}}
}}
}}
}
// (FunctionCall branch moved earlier)
// 4) Literal string
{
local ks = "\"type\":\"string\",\"value\":\""
local ps = index_of_from(json, ks, obj_start)
if ps > 0 { if ps < obj_end {
local si = ps + ks.length()
local sj = index_of_from(json, "\"", si)
if sj > 0 { if sj <= obj_end {
if trace == 1 { print("[hit][str]") }
out.push(json.substring(si, sj)) pos = p + k_print.length() continue
}}
}}
}
// 5) Literal int
{
local ki = "\"type\":\"int\",\"value\":"
local pi = index_of_from(json, ki, obj_start)
if pi > 0 { if pi < obj_end {
local digits = read_digits(json, pi + ki.length())
if digits != "" { if trace == 1 { print("[hit][i-lit] "+digits) } out.push(digits) pos = p + k_print.length() continue }
}}
}
// Unknown: skip this Print object entirely to avoid stalls and mis-detection
// Use coarse slice end (next Print position) when available; fallback to k_print-length step
pos = obj_end + 1
if pos <= p { pos = p + k_print.length() }
}
return out
}
}
// Program entry: prefer argv[0] JSON, fallback to embedded sample

View File

@ -1,8 +1,12 @@
using selfhost.vm.scan as MiniVmScan
using selfhost.vm.binop as MiniVmBinOp
using selfhost.vm.compare as MiniVmCompare
// Use the JSON adapter facade for cursor ops (next_non_ws, digits)
using selfhost.vm.json as MiniJsonLoader
static box MiniVmPrints {
// dev trace flag (0=OFF)
_trace_enabled() { return 0 }
// literal string within Print
try_print_string_value_at(json, end, print_pos) {
local scan = new MiniVmScan()
@ -26,21 +30,14 @@ static box MiniVmPrints {
if obj_start <= 0 || obj_start >= end { return -1 }
local obj_end = scan.find_balanced_object_end(json, obj_start)
if obj_end <= 0 || obj_end > end { return -1 }
local k_kind = "\"kind\":\"Literal\""
local kpos = scan.index_of_from(json, k_kind, obj_start)
if kpos <= 0 || kpos >= obj_end { return -1 }
local k_type = "\"type\":\""
local tpos = scan.index_of_from(json, k_type, kpos)
// robust: look for explicit int type within expression object
local k_tint = "\"type\":\"int\""
local tpos = scan.index_of_from(json, k_tint, obj_start)
if tpos <= 0 || tpos >= obj_end { return -1 }
tpos = tpos + k_type.length()
local t_end = scan.index_of_from(json, "\"", tpos)
if t_end <= 0 || t_end > obj_end { return -1 }
local ty = json.substring(tpos, t_end)
if (ty != "int" && ty != "i64" && ty != "integer") { return -1 }
local k_val2 = "\"value\":"
local v2 = scan.index_of_from(json, k_val2, t_end)
local v2 = scan.index_of_from(json, k_val2, tpos)
if v2 <= 0 || v2 >= obj_end { return -1 }
local digits = scan.read_digits(json, v2 + k_val2.length())
local digits = new MiniVmScan().read_digits(json, v2 + k_val2.length())
if digits == "" { return -1 }
print(digits)
return obj_end + 1
@ -66,7 +63,7 @@ static box MiniVmPrints {
local arr_end = scan.find_balanced_array_end(json, arr_start)
if arr_start <= 0 || arr_end <= 0 || arr_end > end { return -1 }
// handle empty args []
local nn = new MiniJson().next_non_ws(json, arr_start+1)
local nn = new MiniJsonLoader().next_non_ws(json, arr_start+1)
if nn > 0 && nn <= arr_end {
if json.substring(nn, nn+1) == "]" {
if fname == "echo" { print("") return arr_end + 1 }
@ -115,6 +112,7 @@ static box MiniVmPrints {
local pos = start
local printed = 0
local guard = 0
local trace = _trace_enabled()
loop (true) {
guard = guard + 1
if guard > 200 { break }
@ -125,6 +123,11 @@ static box MiniVmPrints {
local p_obj_start = scan.index_of_from(json, "{", p)
local p_obj_end = scan.find_balanced_object_end(json, p_obj_start)
if p_obj_start <= 0 || p_obj_end <= 0 { p_obj_end = p + k_print.length() }
// also compute coarse slice end by next Print marker to guard when object balance is not reliable
local next_p = scan.index_of_from(json, k_print, p + k_print.length())
local p_slice_end = end
if next_p > 0 { p_slice_end = next_p }
// dev trace hook (no-op)
// 1) BinaryOp
local nextp = bin.try_print_binop_sum_any(json, end, p)
if nextp > 0 { printed = printed + 1 pos = p_obj_end + 1 continue }
@ -138,17 +141,26 @@ static box MiniVmPrints {
nextp = cmp.try_print_compare_at(json, end, p)
if nextp > 0 { printed = printed + 1 pos = p_obj_end + 1 continue }
// 3) FunctionCall minimal
nextp = self.try_print_functioncall_at(json, end, p)
nextp = new MiniVmPrints().try_print_functioncall_at(json, end, p)
if nextp > 0 { printed = printed + 1 pos = p_obj_end + 1 continue }
// 4) literal string
nextp = self.try_print_string_value_at(json, end, p)
nextp = new MiniVmPrints().try_print_string_value_at(json, end, p)
if nextp > 0 { printed = printed + 1 pos = p_obj_end + 1 continue }
// 5) literal int via type
nextp = self.try_print_int_value_at(json, end, p)
nextp = new MiniVmPrints().try_print_int_value_at(json, end, p)
if nextp > 0 { printed = printed + 1 pos = p_obj_end + 1 continue }
// Unknown shape: skip forward
pos = p + k_print.length()
if pos <= p { pos = p + 1 }
// 5b) literal int (simple pattern inside current Print object)
{
local ki = "\"type\":\"int\",\"value\":"
local pi = scan.index_of_from(json, ki, p)
if pi > 0 { if pi < p_slice_end {
local digits = new MiniJsonLoader().read_digits_from(json, pi + ki.length())
if digits != "" { print(digits) printed = printed + 1 pos = p_slice_end + 1 continue }
}}
}
// Unknown shape: skip this Print object entirely to avoid stalls
pos = p_obj_end + 1
if pos <= p { pos = p + k_print.length() }
}
return printed
}
@ -178,11 +190,11 @@ static box MiniVmPrints {
if arr_start < 0 { return 0 }
local arr_end = new MiniVmScan().find_balanced_array_end(json, arr_start)
if arr_end < 0 { return 0 }
return self.print_prints_in_slice(json, arr_start, arr_end)
return new MiniVmPrints().print_prints_in_slice(json, arr_start, arr_end)
}
// Print all Print-Literal values in Program.statements (string/int only; MVP)
print_all_print_literals(json) {
return self.print_prints_in_slice(json, 0, json.length())
return new MiniVmPrints().print_prints_in_slice(json, 0, json.length())
}
}

View File

@ -0,0 +1,11 @@
using selfhost.vm.core as MiniVm
using selfhost.vm.prints as MiniVmPrints
static box Main {
main(args) {
local json = "{\"kind\":\"Program\",\"statements\":[{\"kind\":\"Print\",\"expression\":{\"kind\":\"Literal\",\"value\":{\"type\":\"string\",\"value\":\"A\"}}},{\"kind\":\"Print\",\"expression\":{\"kind\":\"FunctionCall\",\"name\":\"echo\",\"arguments\":[{\"kind\":\"Literal\",\"value\":{\"type\":\"string\",\"value\":\"B\"}}]}},{\"kind\":\"Print\",\"expression\":{\"kind\":\"FunctionCall\",\"name\":\"itoa\",\"arguments\":[{\"kind\":\"Literal\",\"value\":{\"type\":\"int\",\"value\":7}}]}},{\"kind\":\"Print\",\"expression\":{\"kind\":\"Compare\",\"operation\":\"<\",\"lhs\":{\"kind\":\"Literal\",\"value\":{\"type\":\"int\",\"value\":1}},\"rhs\":{\"kind\":\"Literal\",\"value\":{\"type\":\"int\",\"value\":2}}}},{\"kind\":\"Print\",\"expression\":{\"kind\":\"BinaryOp\",\"operator\":\"+\",\"left\":{\"kind\":\"Literal\",\"value\":{\"type\":\"int\",\"value\":3}},\"right\":{\"kind\":\"Literal\",\"value\":{\"type\":\"int\",\"value\":4}}}},{\"kind\":\"Print\",\"expression\":{\"kind\":\"Literal\",\"value\":{\"type\":\"int\",\"value\":5}}}]}"
new MiniVmPrints().print_prints_in_slice(json, 0, json.length())
return 0
}
}

View File

@ -1,4 +1,5 @@
use crate::runner::NyashRunner;
use std::collections::HashSet;
/// Strip `using` lines and register modules/aliases into the runtime registry.
/// Returns cleaned source. No-op when `NYASH_ENABLE_USING` is not set.
@ -11,71 +12,516 @@ pub fn strip_using_and_register(
if !crate::config::env::enable_using() {
return Ok(code.to_string());
}
let mut out = String::with_capacity(code.len());
let mut used_names: Vec<(String, Option<String>)> = Vec::new();
for line in code.lines() {
let t = line.trim_start();
if t.starts_with("using ") {
crate::cli_v!("[using] stripped line: {}", line);
let rest0 = t.strip_prefix("using ").unwrap().trim();
let rest0 = rest0.strip_suffix(';').unwrap_or(rest0).trim();
let (target, alias) = if let Some(pos) = rest0.find(" as ") {
(rest0[..pos].trim().to_string(), Some(rest0[pos + 4..].trim().to_string()))
} else {
(rest0.to_string(), None)
};
let is_path = target.starts_with('"')
|| target.starts_with("./")
|| target.starts_with('/')
|| target.ends_with(".nyash");
if is_path {
let path = target.trim_matches('"').to_string();
let name = alias.clone().unwrap_or_else(|| {
std::path::Path::new(&path)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("module")
.to_string()
});
used_names.push((name, Some(path)));
} else {
used_names.push((target, alias));
// Optional external combiner (default OFF): NYASH_USING_COMBINER=1
if std::env::var("NYASH_USING_COMBINER").ok().as_deref() == Some("1") {
let fix_braces = std::env::var("NYASH_RESOLVE_FIX_BRACES").ok().as_deref() == Some("1");
let dedup_box = std::env::var("NYASH_RESOLVE_DEDUP_BOX").ok().as_deref() == Some("1");
let dedup_fn = std::env::var("NYASH_RESOLVE_DEDUP_FN").ok().as_deref() == Some("1");
let seam_dbg = std::env::var("NYASH_RESOLVE_SEAM_DEBUG").ok().as_deref() == Some("1");
let mut cmd = std::process::Command::new("python3");
cmd.arg("tools/using_combine.py")
.arg("--entry").arg(filename);
if fix_braces { cmd.arg("--fix-braces"); }
if dedup_box { cmd.arg("--dedup-box"); }
if dedup_fn { cmd.arg("--dedup-fn"); }
if seam_dbg { cmd.arg("--seam-debug"); }
match cmd.output() {
Ok(out) => {
if out.status.success() {
let combined = String::from_utf8_lossy(&out.stdout).to_string();
return Ok(preexpand_at_local(&combined));
} else {
let err = String::from_utf8_lossy(&out.stderr);
return Err(format!("using combiner failed: {}", err));
}
}
Err(e) => {
return Err(format!("using combiner spawn error: {}", e));
}
}
}
fn strip_and_inline(
runner: &NyashRunner,
code: &str,
filename: &str,
visited: &mut HashSet<String>,
) -> Result<String, String> {
let mut out = String::with_capacity(code.len());
let mut prelude = String::new();
let mut used: Vec<(String, Option<String>)> = Vec::new();
for line in code.lines() {
let t = line.trim_start();
if t.starts_with("using ") {
crate::cli_v!("[using] stripped line: {}", line);
let rest0 = t.strip_prefix("using ").unwrap().trim();
let rest0 = rest0.strip_suffix(';').unwrap_or(rest0).trim();
let (target, alias) = if let Some(pos) = rest0.find(" as ") {
(rest0[..pos].trim().to_string(), Some(rest0[pos + 4..].trim().to_string()))
} else {
(rest0.to_string(), None)
};
let is_path = target.starts_with('"')
|| target.starts_with("./")
|| target.starts_with('/')
|| target.ends_with(".nyash");
if is_path {
let path = target.trim_matches('"').to_string();
let name = alias.clone().unwrap_or_else(|| {
std::path::Path::new(&path)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("module")
.to_string()
});
used.push((name, Some(path)));
} else {
used.push((target, alias));
}
continue;
}
out.push_str(line);
out.push('\n');
}
// Register and inline
let using_ctx = runner.init_using_context();
let strict = std::env::var("NYASH_USING_STRICT").ok().as_deref() == Some("1");
let verbose = crate::config::env::cli_verbose();
let ctx_dir = std::path::Path::new(filename).parent();
let trace = verbose || std::env::var("NYASH_RESOLVE_TRACE").ok().as_deref() == Some("1");
let seam_dbg = std::env::var("NYASH_RESOLVE_SEAM_DEBUG").ok().as_deref() == Some("1");
if trace {
eprintln!(
"[using] ctx: modules={} using_paths={}",
using_ctx.pending_modules.len(),
using_ctx.using_paths.join(":")
);
}
for (ns, alias_opt) in used {
// Two forms:
// - using path "..." [as Alias] → handled earlier (stored as (name, Some(path)))
// - using namespace.with.dots [as Alias] → resolve ns → register alias → inline
let resolved_path = if let Some(alias) = alias_opt {
// alias case: resolve namespace to a concrete path
let mut found: Option<String> = using_ctx
.pending_modules
.iter()
.find(|(n, _)| n == &ns)
.map(|(_, p)| p.clone());
if trace {
if let Some(f) = &found {
eprintln!("[using] hit modules: {} -> {}", ns, f);
} else {
eprintln!("[using] miss modules: {}", ns);
}
}
if found.is_none() {
if let Ok(text) = std::fs::read_to_string("nyash.toml") {
if let Ok(doc) = toml::from_str::<toml::Value>(&text) {
if let Some(mut cur) = doc.get("modules").and_then(|v| v.as_table()) {
let mut segs = ns.split('.').peekable();
let mut hit: Option<String> = None;
while let Some(seg) = segs.next() {
if let Some(next) = cur.get(seg) {
if let Some(t) = next.as_table() {
cur = t;
continue;
}
if segs.peek().is_none() {
if let Some(s) = next.as_str() {
hit = Some(s.to_string());
}
}
}
break;
}
if hit.is_some() {
if trace {
eprintln!("[using] hit nyash.toml: {} -> {}", ns, hit.as_ref().unwrap());
}
found = hit;
}
}
}
}
}
if found.is_none() {
match crate::runner::pipeline::resolve_using_target(
&ns,
false,
&using_ctx.pending_modules,
&using_ctx.using_paths,
&using_ctx.aliases,
ctx_dir,
strict,
verbose,
) {
Ok(v) => {
// Treat unchanged token (namespace) as unresolved
if v == ns {
found = None;
} else {
found = Some(v)
}
}
Err(e) => return Err(format!("using: {}", e)),
}
}
if let Some(value) = found.clone() {
let sb = crate::box_trait::StringBox::new(value.clone());
crate::runtime::modules_registry::set(alias.clone(), Box::new(sb));
let sb2 = crate::box_trait::StringBox::new(value.clone());
crate::runtime::modules_registry::set(ns.clone(), Box::new(sb2));
} else if trace {
eprintln!("[using] still unresolved: {} as {}", ns, alias);
}
found
} else {
// direct namespace without alias
match crate::runner::pipeline::resolve_using_target(
&ns,
false,
&using_ctx.pending_modules,
&using_ctx.using_paths,
&using_ctx.aliases,
ctx_dir,
strict,
verbose,
) {
Ok(value) => {
let sb = crate::box_trait::StringBox::new(value.clone());
crate::runtime::modules_registry::set(ns, Box::new(sb));
Some(value)
}
Err(e) => return Err(format!("using: {}", e)),
}
};
if let Some(path) = resolved_path {
// Resolve relative to current file dir
// Guard: skip obvious namespace tokens (ns.ns without extension)
if (!path.contains('/') && !path.contains('\\')) && !path.ends_with(".nyash") && path.contains('.') {
if verbose {
eprintln!("[using] unresolved '{}' (namespace token, skip inline)", path);
}
continue;
}
let mut p = std::path::PathBuf::from(&path);
if p.is_relative() {
// If the raw relative path exists from CWD, use it.
// Otherwise, try relative to the current file's directory.
if !p.exists() {
if let Some(dir) = std::path::Path::new(filename).parent() {
let cand = dir.join(&p);
if cand.exists() {
p = cand;
}
}
}
}
// normalize to absolute to stabilize de-dup
if let Ok(abs) = std::fs::canonicalize(&p) { p = abs; }
let key = p.to_string_lossy().to_string();
if visited.contains(&key) {
continue;
}
visited.insert(key.clone());
if let Ok(text) = std::fs::read_to_string(&p) {
let inlined = strip_and_inline(runner, &text, &key, visited)?;
prelude.push_str(&inlined);
prelude.push_str("\n");
if seam_dbg {
let tail = inlined.chars().rev().take(120).collect::<String>().chars().rev().collect::<String>();
eprintln!("[using][seam][inlined] {} tail=<<<{}>>>", key, tail.replace('\n', "\\n"));
}
} else if verbose {
eprintln!("[using] warn: could not read {}", p.display());
}
}
}
// Prepend inlined modules so their boxes are defined before use
// Seam guard: collapse consecutive blank lines at the join (prelude || body) to a single blank line
if prelude.is_empty() {
return Ok(out);
}
// Optionally deduplicate repeated static boxes in prelude by name (default OFF)
let mut prelude_text = prelude;
if std::env::var("NYASH_RESOLVE_DEDUP_BOX").ok().as_deref() == Some("1") {
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut out_txt = String::with_capacity(prelude_text.len());
let bytes: Vec<char> = prelude_text.chars().collect();
let mut i = 0usize;
while i < bytes.len() {
// naive scan for "static box "
if i + 12 < bytes.len() && bytes[i..].iter().take(11).collect::<String>() == "static box " {
// read name token
let mut j = i + 11;
let mut name = String::new();
while j < bytes.len() {
let c = bytes[j];
if c.is_alphanumeric() || c == '_' { name.push(c); j += 1; } else { break; }
}
// find opening brace '{'
while j < bytes.len() && bytes[j].is_whitespace() { j += 1; }
if j < bytes.len() && bytes[j] == '{' {
// scan to matching closing brace for this box
let mut k = j;
let mut depth = 0i32;
while k < bytes.len() {
let c = bytes[k];
if c == '{' { depth += 1; }
if c == '}' { depth -= 1; if depth == 0 { k += 1; break; } }
k += 1;
}
// decide
if seen.contains(&name) {
// skip duplicate box
i = k; // drop this block
continue;
} else {
seen.insert(name);
// keep this block as-is
out_txt.push_str(&bytes[i..k].iter().collect::<String>());
i = k;
continue;
}
}
}
// default: copy one char
out_txt.push(bytes[i]);
i += 1;
}
prelude_text = out_txt;
}
// Optional: de-duplicate repeated function definitions inside specific boxes (default OFF)
if std::env::var("NYASH_RESOLVE_DEDUP_FN").ok().as_deref() == Some("1") {
// Currently target MiniVmPrints.print_prints_in_slice only (low risk)
let mut out_txt = String::with_capacity(prelude_text.len());
let bytes: Vec<char> = prelude_text.chars().collect();
let mut i = 0usize;
while i < bytes.len() {
// scan for "static box "
let ahead: String = bytes[i..bytes.len().min(i + 12)].iter().collect();
if ahead.starts_with("static box ") {
// parse box name
let mut j = i + 11; // len("static box ") == 11
let mut name = String::new();
while j < bytes.len() {
let c = bytes[j];
if c.is_ascii_alphanumeric() || c == '_' { name.push(c); j += 1; } else { break; }
}
// skip ws to '{'
while j < bytes.len() && bytes[j].is_whitespace() { j += 1; }
if j < bytes.len() && bytes[j] == '{' {
// find matching closing '}' for the box body
let mut k = j;
let mut depth = 0i32;
let mut in_str = false;
while k < bytes.len() {
let c = bytes[k];
if in_str {
if c == '\\' { k += 2; continue; }
if c == '"' { in_str = false; }
k += 1;
continue;
} else {
if c == '"' { in_str = true; k += 1; continue; }
if c == '{' { depth += 1; }
if c == '}' { depth -= 1; if depth == 0 { k += 1; break; } }
k += 1;
}
}
// write header up to body start '{'
out_txt.push_str(&bytes[i..(j + 1)].iter().collect::<String>());
// process body (limited dedup for MiniVmPrints.print_prints_in_slice)
let body_end = k.saturating_sub(1);
if name == "MiniVmPrints" {
let mut kept = false;
let mut p = j + 1;
while p <= body_end {
// find next line start
let mut ls = p;
if ls > j + 1 {
while ls <= body_end && bytes[ls - 1] != '\n' { ls += 1; }
}
if ls > body_end { break; }
// skip spaces
let mut q = ls;
while q <= body_end && bytes[q].is_whitespace() && bytes[q] != '\n' { q += 1; }
// check for function definition of print_prints_in_slice
let rem: String = bytes[q..(body_end + 1).min(q + 64)].iter().collect();
if rem.starts_with("print_prints_in_slice(") {
// find ')'
let mut r = q;
let mut dp = 0i32;
let mut in_s = false;
while r <= body_end {
let c = bytes[r];
if in_s { if c == '\\' { r += 2; continue; } if c == '"' { in_s = false; } r += 1; continue; }
if c == '"' { in_s = true; r += 1; continue; }
if c == '(' { dp += 1; r += 1; continue; }
if c == ')' { dp -= 1; r += 1; if dp <= 0 { break; } continue; }
r += 1;
}
while r <= body_end && bytes[r].is_whitespace() { r += 1; }
if r <= body_end && bytes[r] == '{' {
// find body end
let mut t = r;
let mut d2 = 0i32;
let mut in_s2 = false;
while t <= body_end {
let c2 = bytes[t];
if in_s2 { if c2 == '\\' { t += 2; continue; } if c2 == '"' { in_s2 = false; } t += 1; continue; }
if c2 == '"' { in_s2 = true; t += 1; continue; }
if c2 == '{' { d2 += 1; }
if c2 == '}' { d2 -= 1; if d2 == 0 { t += 1; break; } }
t += 1;
}
// start-of-line
let mut sol = q;
while sol > j + 1 && bytes[sol - 1] != '\n' { sol -= 1; }
if !kept {
out_txt.push_str(&bytes[sol..t].iter().collect::<String>());
kept = true;
}
p = t;
continue;
}
}
// copy this line
let mut eol = ls;
while eol <= body_end && bytes[eol] != '\n' { eol += 1; }
out_txt.push_str(&bytes[ls..(eol.min(body_end + 1))].iter().collect::<String>());
if eol <= body_end && bytes[eol] == '\n' { out_txt.push('\n'); }
p = eol + 1;
}
} else {
// copy body as-is
out_txt.push_str(&bytes[(j + 1)..=body_end].iter().collect::<String>());
}
// write closing '}'
out_txt.push('}');
i = k;
continue;
}
}
// default: copy one char
out_txt.push(bytes[i]);
i += 1;
}
prelude_text = out_txt;
}
let prelude_clean = prelude_text.trim_end_matches(['\n', '\r']);
if seam_dbg {
let tail = prelude_clean.chars().rev().take(160).collect::<String>().chars().rev().collect::<String>();
let head = out.chars().take(160).collect::<String>();
eprintln!("[using][seam] prelude_tail=<<<{}>>>", tail.replace('\n', "\\n"));
eprintln!("[using][seam] body_head =<<<{}>>>", head.replace('\n', "\\n"));
}
let mut combined = String::with_capacity(prelude_clean.len() + out.len() + 1);
combined.push_str(prelude_clean);
combined.push('\n');
// Optional seam safety: append missing '}' for unmatched '{' in prelude
if std::env::var("NYASH_RESOLVE_FIX_BRACES").ok().as_deref() == Some("1") {
// compute { } delta ignoring strings and comments
let mut delta: i32 = 0;
let mut it = prelude_clean.chars().peekable();
let mut in_str = false;
let mut in_sl = false;
let mut in_ml = false;
while let Some(c) = it.next() {
if in_sl {
if c == '\n' { in_sl = false; }
continue;
}
if in_ml {
if c == '*' {
if let Some('/') = it.peek().copied() {
// consume '/'
it.next();
in_ml = false;
}
}
continue;
}
if in_str {
if c == '\\' { it.next(); continue; }
if c == '"' { in_str = false; }
continue;
}
if c == '"' { in_str = true; continue; }
if c == '/' {
match it.peek().copied() {
Some('/') => { in_sl = true; it.next(); continue; }
Some('*') => { in_ml = true; it.next(); continue; }
_ => {}
}
}
if c == '{' { delta += 1; }
if c == '}' { delta -= 1; }
}
if delta > 0 {
if trace { eprintln!("[using][seam] fix: appending {} '}}' before body", delta); }
for _ in 0..delta { combined.push('}'); combined.push('\n'); }
}
}
combined.push_str(&out);
Ok(combined)
}
let mut visited = HashSet::new();
let combined = strip_and_inline(runner, code, filename, &mut visited)?;
// Dev sugar: always pre-expand @name[:T] = expr at line-head to keep sources readable
Ok(preexpand_at_local(&combined))
}
/// Pre-expand line-head `@name[: Type] = expr` into `local name[: Type] = expr`.
/// Minimal, safe, no semantics change. Applies only at line head (after spaces/tabs).
pub(crate) fn preexpand_at_local(src: &str) -> String {
let mut out = String::with_capacity(src.len());
for line in src.lines() {
let bytes = line.as_bytes();
let mut i = 0;
while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') { i += 1; }
if i < bytes.len() && bytes[i] == b'@' {
// parse identifier
let mut j = i + 1;
// first char [A-Za-z_]
if j < bytes.len() && ((bytes[j] as char).is_ascii_alphabetic() || bytes[j] == b'_') {
j += 1;
while j < bytes.len() {
let c = bytes[j] as char;
if c.is_ascii_alphanumeric() || c == '_' { j += 1; } else { break; }
}
// optional type: spaces ':' spaces ident
let mut k = j;
while k < bytes.len() && (bytes[k] == b' ' || bytes[k] == b'\t') { k += 1; }
if k < bytes.len() && bytes[k] == b':' {
k += 1;
while k < bytes.len() && (bytes[k] == b' ' || bytes[k] == b'\t') { k += 1; }
// simple type ident
if k < bytes.len() && ((bytes[k] as char).is_ascii_alphabetic() || bytes[k] == b'_') {
k += 1;
while k < bytes.len() {
let c = bytes[k] as char;
if c.is_ascii_alphanumeric() || c == '_' { k += 1; } else { break; }
}
}
}
// consume spaces to '='
let mut eqp = k;
while eqp < bytes.len() && (bytes[eqp] == b' ' || bytes[eqp] == b'\t') { eqp += 1; }
if eqp < bytes.len() && bytes[eqp] == b'=' {
// build transformed line: prefix + 'local ' + rest from after '@' up to '=' + ' =' + remainder
out.push_str(&line[..i]);
out.push_str("local ");
out.push_str(&line[i + 1..eqp]);
out.push_str(" =");
out.push_str(&line[eqp + 1..]);
out.push('\n');
continue;
}
}
continue;
}
out.push_str(line);
out.push('\n');
}
// Register modules with resolver (aliases/modules/paths)
let using_ctx = runner.init_using_context();
let strict = std::env::var("NYASH_USING_STRICT").ok().as_deref() == Some("1");
let verbose = crate::config::env::cli_verbose();
let ctx_dir = std::path::Path::new(filename).parent();
for (ns_or_alias, alias_or_path) in used_names {
if let Some(path) = alias_or_path {
let sb = crate::box_trait::StringBox::new(path);
crate::runtime::modules_registry::set(ns_or_alias, Box::new(sb));
} else {
match crate::runner::pipeline::resolve_using_target(
&ns_or_alias,
false,
&using_ctx.pending_modules,
&using_ctx.using_paths,
&using_ctx.aliases,
ctx_dir,
strict,
verbose,
) {
Ok(value) => {
let sb = crate::box_trait::StringBox::new(value);
crate::runtime::modules_registry::set(ns_or_alias, Box::new(sb));
}
Err(e) => {
return Err(format!("using: {}", e));
}
}
}
}
Ok(out)
out
}

View File

@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR=$(cd "$(dirname "$0")/../../../.." && pwd)
echo "[smoke] collect_prints using + mixed order ..." >&2
pushd "$ROOT_DIR" >/dev/null
cargo build --release -q
export NYASH_ENABLE_USING=1
export NYASH_VM_USE_PY=1
# seam safety valve for inlining (default-OFF elsewhere)
export NYASH_RESOLVE_FIX_BRACES=1
# keep dedup OFF for stability (resolver dedup is dev-only)
unset NYASH_RESOLVE_DEDUP_BOX || true
unset NYASH_RESOLVE_DEDUP_FN || true
# parser seam guard (default-OFF): ensure 'static box' at top-level is not mistaken for initializer
export NYASH_PARSER_STATIC_INIT_STRICT=1
BIN=./target/release/nyash
APP=apps/selfhost-vm/collect_mixed_using_smoke.nyash
out=$("$BIN" --backend vm "$APP")
expected=$'A\nB\n7\n1\n7\n5'
if [[ "$out" != "$expected" ]]; then
echo "[smoke] FAIL: unexpected output" >&2
echo "--- got ---" >&2
printf '%s\n' "$out" >&2
echo "--- exp ---" >&2
printf '%s\n' "$expected" >&2
exit 1
fi
echo "[smoke] OK: collect_prints using + mixed order" >&2
popd >/dev/null

390
tools/using_combine.py Normal file
View File

@ -0,0 +1,390 @@
#!/usr/bin/env python3
import sys, os, argparse
def read_text(path: str) -> str:
with open(path, 'r', encoding='utf-8') as f:
return f.read()
def load_toml_modules(toml_path: str):
modules = {}
using_paths = []
try:
txt = read_text(toml_path)
except Exception:
return modules, using_paths
section = None
for line in txt.splitlines():
s = line.strip()
if not s:
continue
if s.startswith('[') and s.endswith(']'):
section = s[1:-1].strip()
continue
if section == 'modules':
# dotted key: a.b.c = "path"
if '=' in s:
k, v = s.split('=', 1)
key = k.strip()
val = v.strip().strip('"')
if key and val:
modules[key] = val
if section == 'using':
# paths = ["apps", "lib", "."]
if s.startswith('paths') and '=' in s:
_, arr = s.split('=', 1)
arr = arr.strip()
if arr.startswith('[') and arr.endswith(']'):
body = arr[1:-1]
parts = [p.strip().strip('"') for p in body.split(',') if p.strip()]
using_paths = [p for p in parts if p]
return modules, using_paths
def brace_delta_ignoring_strings(text: str) -> int:
i = 0
n = len(text)
delta = 0
in_str = False
in_sl = False
in_ml = False
while i < n:
ch = text[i]
# single-line comment
if in_sl:
if ch == '\n':
in_sl = False
i += 1
continue
# multi-line comment
if in_ml:
if ch == '*' and i + 1 < n and text[i + 1] == '/':
in_ml = False
i += 2
continue
i += 1
continue
# string
if in_str:
if ch == '\\':
i += 2
continue
if ch == '"':
in_str = False
i += 1
continue
i += 1
continue
# enter states
if ch == '"':
in_str = True
i += 1
continue
if ch == '/' and i + 1 < n and text[i + 1] == '/':
in_sl = True
i += 2
continue
if ch == '/' and i + 1 < n and text[i + 1] == '*':
in_ml = True
i += 2
continue
# count braces
if ch == '{':
delta += 1
elif ch == '}':
delta -= 1
i += 1
return delta
def find_balanced(text: str, start: int, open_ch: str, close_ch: str) -> int:
i = start
n = len(text)
if i >= n or text[i] != open_ch:
return -1
depth = 0
while i < n:
ch = text[i]
if ch == '"':
i += 1
while i < n:
c = text[i]
if c == '\\':
i += 2
continue
if c == '"':
i += 1
break
i += 1
continue
if ch == open_ch:
depth += 1
if ch == close_ch:
depth -= 1
if depth == 0:
return i
i += 1
return -1
def dedup_boxes(text: str) -> str:
seen = set()
out = []
i = 0
n = len(text)
tok = 'static box '
while i < n:
if text.startswith(tok, i):
j = i + len(tok)
# read identifier
name = []
while j < n and (text[j] == '_' or text[j].isalnum()):
name.append(text[j]); j += 1
# skip ws to '{'
while j < n and text[j].isspace():
j += 1
if j < n and text[j] == '{':
end = find_balanced(text, j, '{', '}')
if end < 0:
end = j
block = text[i:end+1]
nm = ''.join(name)
if nm in seen:
i = end + 1
continue
seen.add(nm)
out.append(block)
i = end + 1
continue
# default: copy one char
out.append(text[i])
i += 1
return ''.join(out)
def dedup_fn_prints_in_slice(text: str) -> str:
# limited: within static box MiniVmPrints, keep only first definition of print_prints_in_slice
out = []
i = 0
n = len(text)
tok = 'static box '
while i < n:
if text.startswith(tok, i):
j = i + len(tok)
name = []
while j < n and (text[j] == '_' or text[j].isalnum()):
name.append(text[j]); j += 1
while j < n and text[j].isspace():
j += 1
if j < n and text[j] == '{':
end = find_balanced(text, j, '{', '}')
if end < 0:
end = j
header = text[i:j+1]
body = text[j+1:end]
nm = ''.join(name)
if nm == 'MiniVmPrints':
kept = False
body_out = []
p = 0
m = len(body)
while p < m:
# find line start
ls = p
if ls > 0:
while ls < m and body[ls-1] != '\n':
ls += 1
if ls >= m:
break
# skip ws
q = ls
while q < m and body[q].isspace() and body[q] != '\n':
q += 1
rem = body[q:q+64]
if rem.startswith('print_prints_in_slice('):
# find def body
r = q
depth = 0
in_s = False
while r < m:
c = body[r]
if in_s:
if c == '\\':
r += 2; continue
if c == '"':
in_s = False
r += 1; continue
if c == '"':
in_s = True; r += 1; continue
if c == '(':
depth += 1; r += 1; continue
if c == ')':
depth -= 1; r += 1
if depth <= 0:
break
continue
r += 1
while r < m and body[r].isspace():
r += 1
if r < m and body[r] == '{':
t = r
d2 = 0
in_s2 = False
while t < m:
c2 = body[t]
if in_s2:
if c2 == '\\':
t += 2; continue
if c2 == '"':
in_s2 = False
t += 1; continue
if c2 == '"':
in_s2 = True; t += 1; continue
if c2 == '{':
d2 += 1
if c2 == '}':
d2 -= 1
if d2 == 0:
t += 1; break
t += 1
# start-of-line for pretty include
sol = q
while sol > 0 and body[sol-1] != '\n':
sol -= 1
if not kept:
body_out.append(body[sol:t])
kept = True
p = t
continue
# default copy this line
eol = ls
while eol < m and body[eol] != '\n':
eol += 1
body_out.append(body[ls:eol+1])
p = eol + 1
new_body = ''.join(body_out)
out.append(header)
out.append(new_body)
out.append('}')
i = end + 1
continue
# non-target box
out.append(text[i:end+1])
i = end + 1
continue
out.append(text[i]); i += 1
return ''.join(out)
def combine(entry: str, fix_braces: bool, dedup_box: bool, dedup_fn: bool, seam_debug: bool) -> str:
repo_root = os.getcwd()
modules, using_paths = load_toml_modules(os.path.join(repo_root, 'nyash.toml'))
visited = set()
def resolve_ns(ns: str) -> str | None:
if ns in modules:
return modules[ns]
# try using paths as base dirs
for base in using_paths or []:
cand = os.path.join(base, *(ns.split('.'))) + '.nyash'
if os.path.exists(cand):
return cand
return None
def strip_and_inline(path: str) -> str:
abspath = path
if not os.path.isabs(abspath):
abspath = os.path.abspath(path)
key = os.path.normpath(abspath)
if key in visited:
return ''
visited.add(key)
code = read_text(abspath)
out_lines = []
used = [] # list[(target, alias_or_path)] ; alias_or_path: None|alias|path
for line in code.splitlines():
t = line.lstrip()
if t.startswith('using '):
rest = t[len('using '):].strip()
if rest.endswith(';'):
rest = rest[:-1].strip()
if ' as ' in rest:
tgt, alias = rest.split(' as ', 1)
tgt = tgt.strip(); alias = alias.strip()
else:
tgt, alias = rest, None
# path vs ns
is_path = tgt.startswith('"') or tgt.startswith('./') or tgt.startswith('/') or tgt.endswith('.nyash')
if is_path:
path_tgt = tgt.strip('"')
name = alias or os.path.splitext(os.path.basename(path_tgt))[0]
used.append((name, path_tgt))
else:
used.append((tgt, alias))
continue
out_lines.append(line)
out = '\n'.join(out_lines) + '\n'
prelude = []
for ns, alias in used:
path_tgt = None
if alias is not None and (ns.startswith('/') or ns.startswith('./') or ns.endswith('.nyash')):
path_tgt = ns
else:
if alias is None:
# direct namespace
path_tgt = resolve_ns(ns)
else:
# alias ns
path_tgt = resolve_ns(ns)
if not path_tgt:
continue
# resolve relative to current file
if not os.path.isabs(path_tgt):
cand = os.path.join(os.path.dirname(abspath), path_tgt)
if os.path.exists(cand):
path_tgt = cand
if not os.path.exists(path_tgt):
continue
inlined = strip_and_inline(path_tgt)
if inlined:
prelude.append(inlined)
prelude_text = ''.join(prelude)
# seam debug
if seam_debug:
tail = prelude_text[-160:].replace('\n', '\\n')
head = out[:160].replace('\n', '\\n')
sys.stderr.write(f"[using][seam] prelude_tail=<<<{tail}>>>\n")
sys.stderr.write(f"[using][seam] body_head =<<<{head}>>>\n")
# seam join
prelude_clean = prelude_text.rstrip('\n\r')
combined = prelude_clean + '\n' + out
if fix_braces:
delta = brace_delta_ignoring_strings(prelude_clean)
if delta > 0:
sys.stderr.write(f"[using][seam] fix: appending {delta} '}}' before body\n")
combined = prelude_clean + '\n' + ('}\n' * delta) + out
# dedups
if dedup_box:
combined = dedup_boxes(combined)
if dedup_fn:
combined = dedup_fn_prints_in_slice(combined)
return combined
return strip_and_inline(entry)
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--entry', required=True)
ap.add_argument('--fix-braces', action='store_true')
ap.add_argument('--dedup-box', action='store_true')
ap.add_argument('--dedup-fn', action='store_true')
ap.add_argument('--seam-debug', action='store_true')
args = ap.parse_args()
text = combine(args.entry, args.fix_braces, args.dedup_box, args.dedup_fn, args.seam_debug)
sys.stdout.write(text)
if __name__ == '__main__':
main()