Files
hakorune/tools/hako_check/analysis_consumer.hako
nyash-codex fa3091061d trace: add execution route visibility + debug passthrough; phase2170 canaries; docs
- 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.
2025-11-08 23:45:29 +09:00

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 } }