// 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, // boxes: Array, // methods: Array (qualified: Box.method/arity), // calls: Array // } 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= 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 } }