2025-11-07 19:32:44 +09:00
// 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
2025-11-08 00:46:34 +09:00
using tools.hako_parser.parser_core as HakoParserCoreBox
2025-11-07 19:32:44 +09:00
static box HakoAnalysisBuilderBox {
2025-11-08 00:46:34 +09:00
build_from_source(text, path) { return me.build_from_source_flags(text, path, 0) }
build_from_source_flags(text, path, no_ast) {
2025-11-07 19:32:44 +09:00
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())
2025-11-08 00:46:34 +09:00
ir.set("source", text)
2025-11-07 19:32:44 +09:00
local eps = new ArrayBox(); eps.push("Main.main"); eps.push("main"); ir.set("entrypoints", eps)
2025-11-08 00:46:34 +09:00
// debug disabled in strict environments
local debug = 0
2025-11-07 19:32:44 +09:00
2025-11-08 00:46:34 +09:00
// Prefer AST (Hako Parser) if possible
local ast = null
if no_ast == 0 { ast = HakoParserCoreBox.parse(text) }
if ast != null {
// uses
local uses = ast.get("uses")
if uses != null { local ui=0; while ui<uses.size() { ir.get("uses").push(uses.get(ui)); ui=ui+1 } }
// methods (qualified: Box.method/arity)
local boxes_ast = ast.get("boxes")
if boxes_ast != null {
local bi=0
while bi<boxes_ast.size() {
local b = boxes_ast.get(bi)
local name = b.get("name")
local ms = b.get("methods")
if ms != null {
local mi=0
while mi<ms.size() {
local m = ms.get(mi)
ir.get("methods").push(name + "." + m.get("name") + "/" + me._itoa(m.get("arity")))
// capture start line spans for later source mapping
local spans = ir.get("method_spans"); if spans == null { spans = new ArrayBox(); ir.set("method_spans", spans) }
local rec = new MapBox(); rec.set("name", name + "." + m.get("name") + "/" + me._itoa(m.get("arity"))); rec.set("line", m.get("span")); spans.push(rec)
mi = mi + 1
}
}
bi = bi + 1
}
2025-11-07 19:32:44 +09:00
}
}
2025-11-08 00:46:34 +09:00
// 1) collect using lines( AST が無い or ASTにmethodが無い場合はテキスト走査)
local lines = me._split_lines(text)
// Decide later whether to scan for methods; always collect "using" for both paths
if 1 == 1 {
// debug noop
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 { ir.get("uses").push(ln.substring(q1+1, q2)) }
}
_i = _i + 1
2025-11-07 19:32:44 +09:00
}
2025-11-08 00:46:34 +09:00
}
2025-11-07 19:32:44 +09:00
2025-11-08 00:46:34 +09:00
// 2) scan static/box and methods when AST did not populate any methods
local need_method_scan = 1
if ir.get("methods") != null { if ir.get("methods").size() > 0 { need_method_scan = 0 } }
if need_method_scan == 1 {
// debug noop
local boxes = ir.get("boxes")
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.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))
boxes.get(boxes.size()-1).get("methods").push(method)
ir.get("methods").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
2025-11-07 19:32:44 +09:00
}
}
2025-11-08 00:46:34 +09:00
// Final fallback: super simple scan over raw text if still no methods
if ir.get("methods").size() == 0 { me._scan_methods_fallback(text, ir) }
2025-11-07 19:32:44 +09:00
// 3) calls: naive pattern Box.method( or Alias.method(
// For MVP, we scan whole text and link within same file boxes only.
2025-11-08 00:46:34 +09:00
// debug noop
2025-11-07 19:32:44 +09:00
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
2025-11-08 00:46:34 +09:00
local L = ln.length()
2025-11-07 19:32:44 +09:00
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 {
local tgt = lhs + "." + rhs + "/0"
// record
local c = new MapBox(); c.set("from", src); c.set("to", tgt); ir.get("calls").push(c)
}
pos = dot + 1
k = k + 1
}
i3 = i3 + 1
}
return ir
}
// utilities
_ltrim(s) { return me._ltrim_chars(s, " \t") }
2025-11-08 00:46:34 +09:00
_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
}
2025-11-07 19:32:44 +09:00
_rstrip(s) {
2025-11-08 00:46:34 +09:00
local n = s.length()
2025-11-07 19:32:44 +09:00
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) {
2025-11-08 00:46:34 +09:00
local n = s.length()
2025-11-07 19:32:44 +09:00
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)
2025-11-08 00:46:34 +09:00
local cnt = 1; local n=inside.length(); local any=0
2025-11-07 19:32:44 +09:00
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
}
2025-11-08 00:46:34 +09:00
_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()
}
2025-11-07 19:32:44 +09:00
_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) {
2025-11-08 00:46:34 +09:00
local n=s.length(); if i>=n { return null }
2025-11-07 19:32:44 +09:00
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) {
2025-11-08 00:46:34 +09:00
// 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.
2025-11-07 19:32:44 +09:00
return "Main.main"
}
}
static box HakoAnalysisBuilderMain { method main(args) { return 0 } }