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:
@ -20,44 +20,13 @@ static box MiniVm {
|
|||||||
}
|
}
|
||||||
_str_to_int(s) { return new MiniVmScan()._str_to_int(s) }
|
_str_to_int(s) { return new MiniVmScan()._str_to_int(s) }
|
||||||
_int_to_str(n) { return new MiniVmScan()._int_to_str(n) }
|
_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 a JSON string starting at position pos (at opening quote); returns the decoded string
|
||||||
read_json_string(json, pos) {
|
read_json_string(json, pos) { return new MiniJson().read_quoted_from(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
|
|
||||||
}
|
|
||||||
// helper: find needle from position pos
|
// helper: find needle from position pos
|
||||||
index_of_from(hay, needle, pos) { return new MiniVmScan().index_of_from(hay, needle, pos) }
|
index_of_from(hay, needle, pos) { return new MiniVmScan().index_of_from(hay, needle, pos) }
|
||||||
// helper: next non-whitespace character index from pos
|
// helper: next non-whitespace character index from pos
|
||||||
next_non_ws(json, pos) {
|
next_non_ws(json, pos) { return new MiniJson().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
|
|
||||||
}
|
|
||||||
// ——— Helpers (as box methods) ———
|
// ——— Helpers (as box methods) ———
|
||||||
|
|
||||||
// Minimal: Print(BinaryOp) with operator "+"; supports string+string and int+int
|
// Minimal: Print(BinaryOp) with operator "+"; supports string+string and int+int
|
||||||
@ -168,17 +137,15 @@ static box MiniVm {
|
|||||||
if vpos < 0 { return null }
|
if vpos < 0 { return null }
|
||||||
vpos = vpos + k_val.length()
|
vpos = vpos + k_val.length()
|
||||||
if ty == "int" || ty == "i64" || ty == "integer" {
|
if ty == "int" || ty == "i64" || ty == "integer" {
|
||||||
// read digits directly
|
// read digits via MiniJson
|
||||||
local digits = read_digits(json, vpos)
|
local digits = new MiniJson().read_digits_from(json, vpos)
|
||||||
return digits
|
return digits
|
||||||
}
|
}
|
||||||
if ty == "string" {
|
if ty == "string" {
|
||||||
// Find opening and closing quotes (no escape handling in MVP)
|
// read quoted via MiniJson
|
||||||
local i = index_of_from(json, "\"", vpos)
|
local i = index_of_from(json, "\"", vpos)
|
||||||
if i < 0 { return null }
|
if i < 0 { return null }
|
||||||
local j = index_of_from(json, "\"", i+1)
|
return new MiniJson().read_quoted_from(json, i)
|
||||||
if j < 0 { return null }
|
|
||||||
return json.substring(i+1, j)
|
|
||||||
}
|
}
|
||||||
// Other types not supported yet
|
// Other types not supported yet
|
||||||
return null
|
return null
|
||||||
@ -287,6 +254,33 @@ static box MiniVm {
|
|||||||
}
|
}
|
||||||
// (reserved) helper for future robust binop scan
|
// (reserved) helper for future robust binop scan
|
||||||
run(json) {
|
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.
|
// Single-purpose fast path for smoke: if BinaryOp '+' exists, try expression-bounded extractor first.
|
||||||
if json.indexOf("\"BinaryOp\"") >= 0 && json.indexOf("\"operator\":\"+\"") >= 0 {
|
if json.indexOf("\"BinaryOp\"") >= 0 && json.indexOf("\"operator\":\"+\"") >= 0 {
|
||||||
// Bind to first Print and extract value×2 within expression bounds
|
// Bind to first Print and extract value×2 within expression bounds
|
||||||
@ -534,6 +528,222 @@ static box MiniVm {
|
|||||||
}
|
}
|
||||||
return 0
|
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
|
// Program entry: prefer argv[0] JSON, fallback to embedded sample
|
||||||
|
|||||||
@ -1,8 +1,12 @@
|
|||||||
using selfhost.vm.scan as MiniVmScan
|
using selfhost.vm.scan as MiniVmScan
|
||||||
using selfhost.vm.binop as MiniVmBinOp
|
using selfhost.vm.binop as MiniVmBinOp
|
||||||
using selfhost.vm.compare as MiniVmCompare
|
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 {
|
static box MiniVmPrints {
|
||||||
|
// dev trace flag (0=OFF)
|
||||||
|
_trace_enabled() { return 0 }
|
||||||
// literal string within Print
|
// literal string within Print
|
||||||
try_print_string_value_at(json, end, print_pos) {
|
try_print_string_value_at(json, end, print_pos) {
|
||||||
local scan = new MiniVmScan()
|
local scan = new MiniVmScan()
|
||||||
@ -26,21 +30,14 @@ static box MiniVmPrints {
|
|||||||
if obj_start <= 0 || obj_start >= end { return -1 }
|
if obj_start <= 0 || obj_start >= end { return -1 }
|
||||||
local obj_end = scan.find_balanced_object_end(json, obj_start)
|
local obj_end = scan.find_balanced_object_end(json, obj_start)
|
||||||
if obj_end <= 0 || obj_end > end { return -1 }
|
if obj_end <= 0 || obj_end > end { return -1 }
|
||||||
local k_kind = "\"kind\":\"Literal\""
|
// robust: look for explicit int type within expression object
|
||||||
local kpos = scan.index_of_from(json, k_kind, obj_start)
|
local k_tint = "\"type\":\"int\""
|
||||||
if kpos <= 0 || kpos >= obj_end { return -1 }
|
local tpos = scan.index_of_from(json, k_tint, obj_start)
|
||||||
local k_type = "\"type\":\""
|
|
||||||
local tpos = scan.index_of_from(json, k_type, kpos)
|
|
||||||
if tpos <= 0 || tpos >= obj_end { return -1 }
|
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 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 }
|
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 }
|
if digits == "" { return -1 }
|
||||||
print(digits)
|
print(digits)
|
||||||
return obj_end + 1
|
return obj_end + 1
|
||||||
@ -66,7 +63,7 @@ static box MiniVmPrints {
|
|||||||
local arr_end = scan.find_balanced_array_end(json, arr_start)
|
local arr_end = scan.find_balanced_array_end(json, arr_start)
|
||||||
if arr_start <= 0 || arr_end <= 0 || arr_end > end { return -1 }
|
if arr_start <= 0 || arr_end <= 0 || arr_end > end { return -1 }
|
||||||
// handle empty args []
|
// 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 nn > 0 && nn <= arr_end {
|
||||||
if json.substring(nn, nn+1) == "]" {
|
if json.substring(nn, nn+1) == "]" {
|
||||||
if fname == "echo" { print("") return arr_end + 1 }
|
if fname == "echo" { print("") return arr_end + 1 }
|
||||||
@ -115,6 +112,7 @@ static box MiniVmPrints {
|
|||||||
local pos = start
|
local pos = start
|
||||||
local printed = 0
|
local printed = 0
|
||||||
local guard = 0
|
local guard = 0
|
||||||
|
local trace = _trace_enabled()
|
||||||
loop (true) {
|
loop (true) {
|
||||||
guard = guard + 1
|
guard = guard + 1
|
||||||
if guard > 200 { break }
|
if guard > 200 { break }
|
||||||
@ -125,6 +123,11 @@ static box MiniVmPrints {
|
|||||||
local p_obj_start = scan.index_of_from(json, "{", p)
|
local p_obj_start = scan.index_of_from(json, "{", p)
|
||||||
local p_obj_end = scan.find_balanced_object_end(json, p_obj_start)
|
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() }
|
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
|
// 1) BinaryOp
|
||||||
local nextp = bin.try_print_binop_sum_any(json, end, p)
|
local nextp = bin.try_print_binop_sum_any(json, end, p)
|
||||||
if nextp > 0 { printed = printed + 1 pos = p_obj_end + 1 continue }
|
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)
|
nextp = cmp.try_print_compare_at(json, end, p)
|
||||||
if nextp > 0 { printed = printed + 1 pos = p_obj_end + 1 continue }
|
if nextp > 0 { printed = printed + 1 pos = p_obj_end + 1 continue }
|
||||||
// 3) FunctionCall minimal
|
// 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 }
|
if nextp > 0 { printed = printed + 1 pos = p_obj_end + 1 continue }
|
||||||
// 4) literal string
|
// 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 }
|
if nextp > 0 { printed = printed + 1 pos = p_obj_end + 1 continue }
|
||||||
// 5) literal int via type
|
// 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 }
|
if nextp > 0 { printed = printed + 1 pos = p_obj_end + 1 continue }
|
||||||
// Unknown shape: skip forward
|
// 5b) literal int (simple pattern inside current Print object)
|
||||||
pos = p + k_print.length()
|
{
|
||||||
if pos <= p { pos = p + 1 }
|
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
|
return printed
|
||||||
}
|
}
|
||||||
@ -178,11 +190,11 @@ static box MiniVmPrints {
|
|||||||
if arr_start < 0 { return 0 }
|
if arr_start < 0 { return 0 }
|
||||||
local arr_end = new MiniVmScan().find_balanced_array_end(json, arr_start)
|
local arr_end = new MiniVmScan().find_balanced_array_end(json, arr_start)
|
||||||
if arr_end < 0 { return 0 }
|
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-Literal values in Program.statements (string/int only; MVP)
|
||||||
print_all_print_literals(json) {
|
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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
apps/selfhost-vm/collect_mixed_using_smoke.nyash
Normal file
11
apps/selfhost-vm/collect_mixed_using_smoke.nyash
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
use crate::runner::NyashRunner;
|
use crate::runner::NyashRunner;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
/// Strip `using` lines and register modules/aliases into the runtime registry.
|
/// Strip `using` lines and register modules/aliases into the runtime registry.
|
||||||
/// Returns cleaned source. No-op when `NYASH_ENABLE_USING` is not set.
|
/// 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() {
|
if !crate::config::env::enable_using() {
|
||||||
return Ok(code.to_string());
|
return Ok(code.to_string());
|
||||||
}
|
}
|
||||||
let mut out = String::with_capacity(code.len());
|
// Optional external combiner (default OFF): NYASH_USING_COMBINER=1
|
||||||
let mut used_names: Vec<(String, Option<String>)> = Vec::new();
|
if std::env::var("NYASH_USING_COMBINER").ok().as_deref() == Some("1") {
|
||||||
for line in code.lines() {
|
let fix_braces = std::env::var("NYASH_RESOLVE_FIX_BRACES").ok().as_deref() == Some("1");
|
||||||
let t = line.trim_start();
|
let dedup_box = std::env::var("NYASH_RESOLVE_DEDUP_BOX").ok().as_deref() == Some("1");
|
||||||
if t.starts_with("using ") {
|
let dedup_fn = std::env::var("NYASH_RESOLVE_DEDUP_FN").ok().as_deref() == Some("1");
|
||||||
crate::cli_v!("[using] stripped line: {}", line);
|
let seam_dbg = std::env::var("NYASH_RESOLVE_SEAM_DEBUG").ok().as_deref() == Some("1");
|
||||||
let rest0 = t.strip_prefix("using ").unwrap().trim();
|
let mut cmd = std::process::Command::new("python3");
|
||||||
let rest0 = rest0.strip_suffix(';').unwrap_or(rest0).trim();
|
cmd.arg("tools/using_combine.py")
|
||||||
let (target, alias) = if let Some(pos) = rest0.find(" as ") {
|
.arg("--entry").arg(filename);
|
||||||
(rest0[..pos].trim().to_string(), Some(rest0[pos + 4..].trim().to_string()))
|
if fix_braces { cmd.arg("--fix-braces"); }
|
||||||
} else {
|
if dedup_box { cmd.arg("--dedup-box"); }
|
||||||
(rest0.to_string(), None)
|
if dedup_fn { cmd.arg("--dedup-fn"); }
|
||||||
};
|
if seam_dbg { cmd.arg("--seam-debug"); }
|
||||||
let is_path = target.starts_with('"')
|
match cmd.output() {
|
||||||
|| target.starts_with("./")
|
Ok(out) => {
|
||||||
|| target.starts_with('/')
|
if out.status.success() {
|
||||||
|| target.ends_with(".nyash");
|
let combined = String::from_utf8_lossy(&out.stdout).to_string();
|
||||||
if is_path {
|
return Ok(preexpand_at_local(&combined));
|
||||||
let path = target.trim_matches('"').to_string();
|
} else {
|
||||||
let name = alias.clone().unwrap_or_else(|| {
|
let err = String::from_utf8_lossy(&out.stderr);
|
||||||
std::path::Path::new(&path)
|
return Err(format!("using combiner failed: {}", err));
|
||||||
.file_stem()
|
}
|
||||||
.and_then(|s| s.to_str())
|
}
|
||||||
.unwrap_or("module")
|
Err(e) => {
|
||||||
.to_string()
|
return Err(format!("using combiner spawn error: {}", e));
|
||||||
});
|
}
|
||||||
used_names.push((name, Some(path)));
|
}
|
||||||
} else {
|
}
|
||||||
used_names.push((target, alias));
|
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_str(line);
|
||||||
out.push('\n');
|
out.push('\n');
|
||||||
}
|
}
|
||||||
|
out
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
37
tools/test/smoke/selfhost/collect_prints_using_mixed.sh
Normal file
37
tools/test/smoke/selfhost/collect_prints_using_mixed.sh
Normal 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
390
tools/using_combine.py
Normal 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()
|
||||||
Reference in New Issue
Block a user