200 lines
6.2 KiB
Plaintext
200 lines
6.2 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
|
||
|
|
|
||
|
|
static box HakoAnalysisBuilderBox {
|
||
|
|
build_from_source(text, path) {
|
||
|
|
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())
|
||
|
|
local eps = new ArrayBox(); eps.push("Main.main"); eps.push("main"); ir.set("entrypoints", eps)
|
||
|
|
|
||
|
|
// 1) collect using lines
|
||
|
|
local lines = text.split("\n")
|
||
|
|
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
|
||
|
|
}
|
||
|
|
|
||
|
|
// 2) scan static/box and methods (very naive)
|
||
|
|
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(Str.len("static box "))
|
||
|
|
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)
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
// (non-static) box Name { // optional future; ignore for now
|
||
|
|
|
||
|
|
// method foo(args) {
|
||
|
|
if ln.indexOf("method ") == 0 && cur_name != null {
|
||
|
|
local rest = ln.substring(Str.len("method "))
|
||
|
|
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", Str.int_to_str(i2+1))
|
||
|
|
// attach to box
|
||
|
|
local arr = boxes.get(boxes.size()-1).get("methods"); arr.push(method)
|
||
|
|
// record qualified
|
||
|
|
ir.get("methods").push(cur_name + "." + mname + "/" + Str.int_to_str(arity))
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
// box boundary heuristic
|
||
|
|
if ln == "}" { cur_name = null; cur_is_static = 0; }
|
||
|
|
i2 = i2 + 1
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3) calls: naive pattern Box.method( or Alias.method(
|
||
|
|
// For MVP, we scan whole text and link within same file boxes only.
|
||
|
|
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 = Str.len(ln)
|
||
|
|
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") }
|
||
|
|
_rstrip(s) {
|
||
|
|
local n = Str.len(s)
|
||
|
|
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 = Str.len(s)
|
||
|
|
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=Str.len(inside); 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_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=Str.len(s); 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) {
|
||
|
|
// very naive: pick Main.main when unknown
|
||
|
|
// Future: track method spans. For MVP, return "Main.main".
|
||
|
|
return "Main.main"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static box HakoAnalysisBuilderMain { method main(args) { return 0 } }
|