- Add HAKO_TRACE_EXECUTION to trace executor route - Rust hv1_inline: stderr [trace] executor: hv1_inline (rust) - Hakovm dispatcher: stdout [trace] executor: hakovm (hako) - test_runner: trace lines for hv1_inline/core/hakovm routes - Add HAKO_VERIFY_SHOW_LOGS and HAKO_DEBUG=1 (enables both) - verify_v1_inline_file() log passthrough with numeric rc extraction - test_runner exports via HAKO_DEBUG - Canary expansion under phase2170 (state spec) - Array: push×5/10 → size, len/length alias, per‑recv/global, flow across blocks - Map: set dup-key non-increment, value_state get/has - run_all.sh: unify, remove SKIPs; all PASS - Docs - ENV_VARS.md: add Debug/Tracing toggles and examples - PLAN.md/CURRENT_TASK.md: mark 21.7 green, add Quickstart lines All changes gated by env vars; default behavior unchanged.
388 lines
14 KiB
Plaintext
388 lines
14 KiB
Plaintext
// tools/hako_check/analysis_consumer.hako - HakoAnalysisBuilderBox (MVP)
|
|
// Build a minimal Analysis IR from raw .hako source (no Rust parser needed).
|
|
// IR (MapBox): {
|
|
// path: String,
|
|
// uses: Array<String>,
|
|
// boxes: Array<Map{name,is_static,methods:Array<Map{name,arity,span}}}>,
|
|
// methods: Array<String> (qualified: Box.method/arity),
|
|
// calls: Array<Map{from,to}},
|
|
// entrypoints: Array<String>
|
|
// }
|
|
|
|
using selfhost.shared.common.string_helpers as Str
|
|
using tools.hako_parser.parser_core as HakoParserCoreBox
|
|
|
|
static box HakoAnalysisBuilderBox {
|
|
build_from_source(text, path) { return me.build_from_source_flags(text, path, 0) }
|
|
build_from_source_flags(text, path, no_ast) {
|
|
local ir = new MapBox()
|
|
ir.set("path", path)
|
|
ir.set("uses", new ArrayBox())
|
|
ir.set("boxes", new ArrayBox())
|
|
ir.set("methods", new ArrayBox())
|
|
ir.set("calls", new ArrayBox())
|
|
ir.set("method_spans", new ArrayBox())
|
|
ir.set("source", text)
|
|
local eps = new ArrayBox(); eps.push("Main.main"); eps.push("main"); ir.set("entrypoints", eps)
|
|
// debug disabled in strict environments
|
|
local debug = 0
|
|
|
|
// Prefer AST (Hako Parser) if possible
|
|
local ast = null
|
|
if no_ast == 0 { ast = HakoParserCoreBox.parse(text) }
|
|
if ast != null {
|
|
// uses (with fallback for backward compat)
|
|
local uses = ast.get("uses"); if uses == null { uses = ast.get("uses_arr") }
|
|
if uses != null { local ui=0; while ui<uses.size() { me._ensure_array(ir, "uses").push(uses.get(ui)); ui=ui+1 } }
|
|
// aliases (if present)
|
|
local aliases = ast.get("aliases")
|
|
if aliases != null { ir.set("aliases", aliases) }
|
|
// methods (qualified: Box.method/arity) and boxes metadata
|
|
local boxes_ast = ast.get("boxes"); if boxes_ast == null { boxes_ast = ast.get("boxes_arr") }
|
|
if boxes_ast != null {
|
|
local bi=0
|
|
while bi<boxes_ast.size() {
|
|
local b = boxes_ast.get(bi)
|
|
local name = b.get("name")
|
|
local is_static = b.get("is_static")
|
|
local span_line = b.get("span_line")
|
|
|
|
// Add box to IR boxes array for HC012 dead static box detection
|
|
local box_info = new MapBox()
|
|
box_info.set("name", name)
|
|
box_info.set("is_static", is_static)
|
|
box_info.set("methods", new ArrayBox())
|
|
if span_line != null { box_info.set("span_line", span_line) }
|
|
|
|
local ms = b.get("methods")
|
|
if ms != null {
|
|
local mi=0
|
|
while mi<ms.size() {
|
|
local m = ms.get(mi)
|
|
local qual = name + "." + m.get("name") + "/" + me._itoa(m.get("arity"))
|
|
me._ensure_array(ir, "methods").push(qual)
|
|
// capture start line spans for later source mapping
|
|
local spans = me._ensure_array(ir, "method_spans")
|
|
local rec = new MapBox(); rec.set("name", qual); rec.set("line", m.get("span"))
|
|
spans.push(rec)
|
|
|
|
// Also add method metadata to box_info
|
|
local method_info = new MapBox()
|
|
method_info.set("name", m.get("name"))
|
|
method_info.set("arity", m.get("arity"))
|
|
method_info.set("span", m.get("span"))
|
|
local box_info_methods = box_info.get("methods")
|
|
box_info_methods.push(method_info)
|
|
|
|
mi = mi + 1
|
|
}
|
|
}
|
|
|
|
// Add box_info to IR boxes array
|
|
me._ensure_array(ir, "boxes").push(box_info)
|
|
bi = bi + 1
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1) collect using lines(scan text only when AST is empty)
|
|
local lines = me._split_lines(text)
|
|
if ir.get("uses") == null || ir.get("uses").size() == 0 {
|
|
local _i = 0
|
|
while _i < lines.size() {
|
|
local ln = me._ltrim(lines.get(_i))
|
|
if ln.indexOf('using "') == 0 {
|
|
// using "pkg.name" as Alias
|
|
local q1 = ln.indexOf('"')
|
|
local q2 = -1
|
|
if q1 >= 0 { q2 = ln.indexOf('"', q1+1) }
|
|
if q1 >= 0 && q2 > q1 { me._ensure_array(ir, "uses").push(ln.substring(q1+1, q2)) }
|
|
}
|
|
_i = _i + 1
|
|
}
|
|
}
|
|
|
|
// 2) scan static/box and methods when AST did not populate any methods or boxes
|
|
local need_method_scan = 1
|
|
if ir.get("methods") != null { if ir.get("methods").size() > 0 { need_method_scan = 0 } }
|
|
// Also scan if boxes array is empty (for HC012 dead static box detection)
|
|
local need_box_scan = 0
|
|
if ir.get("boxes") != null { if ir.get("boxes").size() == 0 { need_box_scan = 1 } }
|
|
if need_method_scan == 1 || need_box_scan == 1 {
|
|
// debug noop
|
|
local boxes_arr = me._ensure_array(ir, "boxes")
|
|
local methods_arr = me._ensure_array(ir, "methods")
|
|
local cur_name = null
|
|
local cur_is_static = 0
|
|
local i2 = 0
|
|
while i2 < lines.size() {
|
|
local ln = me._ltrim(lines.get(i2))
|
|
// static box Name {
|
|
if ln.indexOf("static box ") == 0 {
|
|
local rest = ln.substring("static box ".length())
|
|
local sp = me._upto(rest, " {")
|
|
cur_name = sp
|
|
cur_is_static = 1
|
|
local b = new MapBox(); b.set("name", cur_name); b.set("is_static", true); b.set("methods", new ArrayBox())
|
|
boxes_arr.push(b)
|
|
i2 = i2 + 1
|
|
continue
|
|
}
|
|
// method foo(args) {
|
|
if ln.indexOf("method ") == 0 {
|
|
if cur_name == null { cur_name = "Main" }
|
|
local rest = ln.substring("method ".length())
|
|
local p = rest.indexOf("(")
|
|
local mname = (p>0) ? rest.substring(0,p) : rest
|
|
mname = me._rstrip(mname)
|
|
local arity = me._count_commas_in_parens(rest)
|
|
local method = new MapBox(); method.set("name", mname); method.set("arity", arity); method.set("span", (i2+1))
|
|
// Safe push pattern: get box, then get its methods array, then push
|
|
local last_box = boxes_arr.get(boxes_arr.size()-1)
|
|
local box_methods = last_box.get("methods")
|
|
box_methods.push(method)
|
|
methods_arr.push(cur_name + "." + mname + "/" + me._itoa(arity))
|
|
i2 = i2 + 1
|
|
continue
|
|
}
|
|
// box boundary heuristic
|
|
if ln == "}" { cur_name = null; cur_is_static = 0; }
|
|
i2 = i2 + 1
|
|
}
|
|
}
|
|
// Final fallback: super simple scan over raw text if still no methods
|
|
if ir.get("methods").size() == 0 { me._scan_methods_fallback(text, ir) }
|
|
|
|
// 3) calls: naive pattern Box.method( or Alias.method( - with arity inference
|
|
// For MVP, we scan whole text and link within same file boxes only.
|
|
// debug noop
|
|
local calls_arr = me._ensure_array(ir, "calls")
|
|
local i3 = 0
|
|
while i3 < lines.size() {
|
|
local ln = lines.get(i3)
|
|
// source context: try to infer last seen method
|
|
// We fallback to "Main.main" when unknown
|
|
local src = me._last_method_for_line(ir, i3+1)
|
|
local pos = 0
|
|
local L = ln.length()
|
|
local k = 0
|
|
while k <= L {
|
|
local dot = ln.indexOf(".", pos)
|
|
if dot < 0 { break }
|
|
// find ident before '.' and after '.'
|
|
local lhs = me._scan_ident_rev(ln, dot-1)
|
|
local rhs = me._scan_ident_fwd(ln, dot+1)
|
|
if lhs != null && rhs != null {
|
|
// infer arity by scanning parentheses immediately after method
|
|
local ar = me._infer_call_arity(ln, dot + 1 + rhs.length())
|
|
local tgt = lhs + "." + rhs + "/" + me._itoa(ar)
|
|
// record
|
|
local c = new MapBox(); c.set("from", src); c.set("to", tgt)
|
|
calls_arr.push(c)
|
|
}
|
|
pos = dot + 1
|
|
k = k + 1
|
|
}
|
|
i3 = i3 + 1
|
|
}
|
|
return ir
|
|
}
|
|
|
|
// utilities
|
|
_ltrim(s) { return me._ltrim_chars(s, " \t") }
|
|
_itoa(n) { local v=0+n; if v==0 { return "0" } local out=""; local digits="0123456789"; local tmp=""; while v>0 { local d=v%10; tmp=digits.substring(d,d+1)+tmp; v=v/10 } out=tmp; return out }
|
|
_split_lines(s) {
|
|
local arr = new ArrayBox(); if s == null { return arr }
|
|
local n = s.length(); local last = 0; local i = 0
|
|
loop (i < n) { local ch = s.substring(i,i+1); if ch == "\n" { arr.push(s.substring(last,i)); last = i+1 } i = i + 1 }
|
|
if last <= n { arr.push(s.substring(last)) }
|
|
return arr
|
|
}
|
|
_rstrip(s) {
|
|
local n = s.length()
|
|
local last = n
|
|
// scan from end using reverse index
|
|
local r = 0
|
|
while r < n {
|
|
local i4 = n-1-r
|
|
local c = s.substring(i4, i4+1)
|
|
if c != " " && c != "\t" { last = i4+1; break }
|
|
if r == n-1 { last = 0 }
|
|
r = r + 1
|
|
}
|
|
return s.substring(0, last)
|
|
}
|
|
_ltrim_chars(s, cs) {
|
|
local n = s.length()
|
|
local head = 0
|
|
local idx = 0
|
|
while idx < n {
|
|
local ch = s.substring(idx, idx+1)
|
|
if ch != " " && ch != "\t" { head = idx; break }
|
|
if idx == n-1 { head = n }
|
|
idx = idx + 1
|
|
}
|
|
return s.substring(head)
|
|
}
|
|
_upto(s, needle) {
|
|
local p = s.indexOf(needle)
|
|
if p < 0 { return me._rstrip(s) }
|
|
return s.substring(0,p)
|
|
}
|
|
_count_commas_in_parens(rest) {
|
|
// method foo(a,b,c) -> 3 ; if empty -> 0
|
|
local p1 = rest.indexOf("("); local p2 = rest.indexOf(")", p1+1)
|
|
if p1 < 0 || p2 < 0 || p2 <= p1+1 { return 0 }
|
|
local inside = rest.substring(p1+1, p2)
|
|
local cnt = 1; local n=inside.length(); local any=0
|
|
local i5 = 0
|
|
while i5 < n {
|
|
local c = inside.substring(i5,i5+1)
|
|
if c == "," { cnt = cnt + 1 }
|
|
if c != " " && c != "\t" { any = 1 }
|
|
i5 = i5 + 1
|
|
}
|
|
if any==0 { return 0 }
|
|
return cnt
|
|
}
|
|
_scan_methods_fallback(text, ir) {
|
|
if text == null { return 0 }
|
|
local methods = ir.get("methods")
|
|
local box_name = "Main"
|
|
// find "static box Name" to prefer given name
|
|
local pbox = text.indexOf("static box ")
|
|
if pbox >= 0 {
|
|
local after = pbox + "static box ".length()
|
|
local name = ""
|
|
local i = after
|
|
loop (i < text.length()) {
|
|
local ch = text.substring(i,i+1)
|
|
if (ch >= "A" && ch <= "Z") || (ch >= "a" && ch <= "z") || ch == "_" || (ch >= "0" && ch <= "9") {
|
|
name = name + ch
|
|
i = i + 1
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
if name != "" { box_name = name }
|
|
}
|
|
// scan for "method " occurrences
|
|
local pos = 0
|
|
local n = text.length()
|
|
loop (pos < n) {
|
|
local k = text.indexOf("method ", pos)
|
|
if k < 0 { break }
|
|
local i = k + "method ".length()
|
|
// read ident
|
|
local mname = ""
|
|
loop (i < n) {
|
|
local ch2 = text.substring(i,i+1)
|
|
if (ch2 >= "A" && ch2 <= "Z") || (ch2 >= "a" && ch2 <= "z") || ch2 == "_" || (ch2 >= "0" && ch2 <= "9") {
|
|
mname = mname + ch2
|
|
i = i + 1
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
// look ahead for params (...) and count commas
|
|
local ar = 0
|
|
local lp = text.indexOf("(", i)
|
|
if lp >= 0 {
|
|
local rp = text.indexOf(")", lp+1)
|
|
if rp > lp+1 {
|
|
local inside = text.substring(lp+1, rp)
|
|
local any = 0; local c = 0; local j=0
|
|
loop (j < inside.length()) {
|
|
local ch3=inside.substring(j,j+1)
|
|
if ch3 == "," { c = c + 1 }
|
|
if ch3 != " " && ch3 != "\t" { any = 1 }
|
|
j = j + 1
|
|
}
|
|
if any == 1 { ar = c + 1 }
|
|
}
|
|
}
|
|
if mname != "" { methods.push(box_name + "." + mname + "/" + me._itoa(ar)) }
|
|
pos = i
|
|
}
|
|
return methods.size()
|
|
}
|
|
_scan_ident_rev(s, i) {
|
|
if i<0 { return null }
|
|
local n = i
|
|
local start = 0
|
|
local rr = 0
|
|
while rr <= n {
|
|
local j = i - rr
|
|
local c = s.substring(j, j+1)
|
|
if me._is_ident_char(c) == 0 { start = j+1; break }
|
|
if j == 0 { start = 0; break }
|
|
rr = rr + 1
|
|
}
|
|
if start>i { return null }
|
|
return s.substring(start, i+1)
|
|
}
|
|
_scan_ident_fwd(s, i) {
|
|
local n=s.length(); if i>=n { return null }
|
|
local endp = i
|
|
local off = 0
|
|
while off < n {
|
|
local j = i + off
|
|
if j >= n { break }
|
|
local c = s.substring(j, j+1)
|
|
if me._is_ident_char(c) == 0 { endp = j; break }
|
|
if j == n-1 { endp = n; break }
|
|
off = off + 1
|
|
}
|
|
if endp == i { return null }
|
|
return s.substring(i, endp)
|
|
}
|
|
_is_ident_char(c) {
|
|
if c == "_" { return 1 }
|
|
if c >= "A" && c <= "Z" { return 1 }
|
|
if c >= "a" && c <= "z" { return 1 }
|
|
if c >= "0" && c <= "9" { return 1 }
|
|
return 0
|
|
}
|
|
_last_method_for_line(ir, line_num) {
|
|
// Conservative: return default entry when spans are not guaranteed to be maps
|
|
// This avoids runtime errors when method_spans is absent or malformed in MVP builds.
|
|
return "Main.main/0"
|
|
}
|
|
_infer_call_arity(line, after_name_pos) {
|
|
// Count commas between the first '(' after method name and its matching ')'
|
|
if line == null { return 0 }
|
|
local n = line.length()
|
|
local i = after_name_pos
|
|
// seek to '('
|
|
while i < n {
|
|
local ch = line.substring(i,i+1)
|
|
if ch == "(" { break }
|
|
// stop early if we hit non-space before '('
|
|
if ch != " " && ch != "\t" { return 0 }
|
|
i = i + 1
|
|
}
|
|
if i >= n || line.substring(i,i+1) != "(" { return 0 }
|
|
// scan until matching ')', no nesting assumed for MVP
|
|
local any = 0; local commas = 0
|
|
i = i + 1
|
|
while i < n {
|
|
local ch = line.substring(i,i+1)
|
|
if ch == ")" { break }
|
|
if ch == "," { commas = commas + 1 }
|
|
if ch != " " && ch != "\t" { any = 1 }
|
|
i = i + 1
|
|
}
|
|
if any == 0 { return 0 }
|
|
return commas + 1
|
|
}
|
|
_ensure_array(m, key) {
|
|
if m == null { return new ArrayBox() }
|
|
// If key absent, create and return a new ArrayBox
|
|
if m.has(key) == 0 { local arr = new ArrayBox(); m.set(key, arr); return arr }
|
|
// Otherwise return the existing value
|
|
return m.get(key)
|
|
}
|
|
}
|
|
|
|
static box HakoAnalysisBuilderMain { method main(args) { return 0 } }
|