diff --git a/apps/selfhost-vm/boxes/mini_vm_core.nyash b/apps/selfhost-vm/boxes/mini_vm_core.nyash index 60b2383a..503efbfa 100644 --- a/apps/selfhost-vm/boxes/mini_vm_core.nyash +++ b/apps/selfhost-vm/boxes/mini_vm_core.nyash @@ -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 diff --git a/apps/selfhost-vm/boxes/mini_vm_prints.nyash b/apps/selfhost-vm/boxes/mini_vm_prints.nyash index aaad4e3c..72f4899f 100644 --- a/apps/selfhost-vm/boxes/mini_vm_prints.nyash +++ b/apps/selfhost-vm/boxes/mini_vm_prints.nyash @@ -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()) } } diff --git a/apps/selfhost-vm/collect_mixed_using_smoke.nyash b/apps/selfhost-vm/collect_mixed_using_smoke.nyash new file mode 100644 index 00000000..f5b9274e --- /dev/null +++ b/apps/selfhost-vm/collect_mixed_using_smoke.nyash @@ -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 + } +} diff --git a/src/runner/modes/common_util/resolve.rs b/src/runner/modes/common_util/resolve.rs index ff341bec..6ed95880 100644 --- a/src/runner/modes/common_util/resolve.rs +++ b/src/runner/modes/common_util/resolve.rs @@ -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)> = 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, + ) -> Result { + let mut out = String::with_capacity(code.len()); + let mut prelude = String::new(); + let mut used: Vec<(String, Option)> = 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 = 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::(&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 = 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::().chars().rev().collect::(); + 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 = std::collections::HashSet::new(); + let mut out_txt = String::with_capacity(prelude_text.len()); + let bytes: Vec = 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::() == "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::()); + 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 = 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::()); + // 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::()); + 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::()); + 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::()); + } + // 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::().chars().rev().collect::(); + let head = out.chars().take(160).collect::(); + 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 } diff --git a/tools/test/smoke/selfhost/collect_prints_using_mixed.sh b/tools/test/smoke/selfhost/collect_prints_using_mixed.sh new file mode 100644 index 00000000..4c2748dc --- /dev/null +++ b/tools/test/smoke/selfhost/collect_prints_using_mixed.sh @@ -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 diff --git a/tools/using_combine.py b/tools/using_combine.py new file mode 100644 index 00000000..4dc92c46 --- /dev/null +++ b/tools/using_combine.py @@ -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()