static box RuleDeadMethodsBox {
  // IR expects: methods(Array<String>), calls(Array<Map{from,to}>), entrypoints(Array<String>)
  apply_ir(ir, path, out) {
    local methods = ir.get("methods")
    // If IR has no methods, or methods is empty, rebuild from source file.
    if methods == null || methods.size() == 0 {
      // Prefer in-memory source if provided (avoids FileBox/plugin dependency)
      local src = ir.get("source")
      if src != null { methods = me._scan_methods_from_text(src) } else {
        // Fallback to FileBox only when no source text provided
        local fb = new FileBox()
        if fb.open(path) == 0 { local text = fb.read(); fb.close(); methods = me._scan_methods_from_text(text) } else { methods = new ArrayBox() }
      }
    }
    if methods == null || methods.size() == 0 { return }
    local calls = ir.get("calls");
    if (calls == null || calls.size() == 0) {
      // build minimal calls from source text (avoid plugin)
      local src = ir.get("source"); if src != null { calls = me._scan_calls_from_text(src) } else { calls = new ArrayBox() }
    }
    local eps = ir.get("entrypoints"); if eps == null { eps = new ArrayBox() }
    // build graph
    local adj = new MapBox()
    local i = 0; while i < methods.size() { adj.set(methods.get(i), new ArrayBox()); i = i + 1 }
    i = 0; while i < calls.size() {
      local c=calls.get(i); local f=c.get("from"); local t=c.get("to")
      // normalize from: prefer exact, otherwise try adding "/0" suffix
      local ff = f
      if adj.has(ff) == 0 { local f0 = f + "/0"; if adj.has(f0) == 1 { ff = f0 } }
      if adj.has(ff) == 1 { adj.get(ff).push(t) }
      i = i + 1
    }
    // DFS from entrypoints
    local seen = new MapBox();
    // resolve seeds: accept exact or prefix ("name/arity") matches for entrypoint names
    local seeds = new ArrayBox()
    // collect keys
    local keys = new ArrayBox(); i = 0; while i < methods.size() { keys.push(methods.get(i)); i = i + 1 }
    local j = 0
    while j < eps.size() {
      local ep = eps.get(j)
      // exact match
      if adj.has(ep) == 1 { seeds.push(ep) }
      // prefix match: ep + "/"
      local pref = ep + "/"
      local k = 0; while k < keys.size() { local key = keys.get(k); if key.indexOf(pref) == 0 { seeds.push(key) } k = k + 1 }
      j = j + 1
    }
    // fallback: common Main.main/0 if still empty
    if seeds.size() == 0 {
      if adj.has("Main.main/0") == 1 { seeds.push("Main.main/0") }
    }
    // run DFS from seeds
    j = 0; while j < seeds.size() { me._dfs(adj, seeds.get(j), seen); j = j + 1 }
    // report dead = methods not seen (filter with simple call-text heuristic)
    local src_text = ir.get("source")
    local cands = new ArrayBox()
    i = 0; while i < methods.size() { local m=methods.get(i); if seen.has(m)==0 { cands.push(m) }; i = i + 1 }
    i = 0; while i < cands.size() {
      local m = cands.get(i)
      local keep = 1
      if src_text != null {
        // If source text contains a call like ".methodName(", consider it reachable
        local slash = m.lastIndexOf("/")
        local dotp = m.lastIndexOf(".")
        if dotp >= 0 {
          local meth = (slash>dotp)? m.substring(dotp+1, slash) : m.substring(dotp+1)
          if src_text.indexOf("." + meth + "(") >= 0 { keep = 0 }
        }
      }
      if keep == 1 { out.push("[HC011] unreachable method (dead code): PLACEHOLDER :: " + m) }
      i = i + 1
    }
  }
  _scan_methods_from_text(text) {
    local res = new ArrayBox()
    if text == null { return res }
    // use local implementation to avoid external static calls
    local lines = me._split_lines(text)
    local cur = null
    local depth = 0
    local i = 0
    while i < lines.size() {
      local ln = me._ltrim(lines.get(i))
      if ln.indexOf("static box ") == 0 {
        local rest = ln.substring("static box ".length())
        local p = rest.indexOf("{")
        if p > 0 { cur = me._rstrip(rest.substring(0,p)) } else { cur = me._rstrip(rest) }
        depth = depth + 1
        i = i + 1; continue
      }
      if cur != null && ln.indexOf("method ") == 0 {
        local rest = ln.substring("method ".length())
        local p1 = rest.indexOf("(")
        local name = (p1>0)? me._rstrip(rest.substring(0,p1)) : me._rstrip(rest)
        local ar = 0
        local p2 = rest.indexOf(")", (p1>=0)?(p1+1):0)
        if p1>=0 && p2>p1+1 {
          local inside = rest.substring(p1+1,p2)
          // count commas + 1 if any non-space
          local any = 0; local cnt = 1; local k=0; while k < inside.length() { local c=inside.substring(k,k+1); if c=="," { cnt = cnt + 1 }; if c!=" "&&c!="\t" { any=1 }; k=k+1 }
          if any == 1 { ar = cnt }
        }
        res.push(cur + "." + name + "/" + me._itoa(ar))
      }
      // adjust depth by braces on the line
      local j=0; while j < ln.length() { local ch=ln.substring(j,j+1); if ch=="{" { depth = depth + 1 } else { if ch=="}" { depth = depth - 1; if depth < 0 { depth = 0 } } } j=j+1 }
      if depth == 0 { cur = null }
      i = i + 1
    }
    return res
  }
  _ltrim(s) { return me._ltrim_chars(s, " \t") }
  _rstrip(s) {
    local n = s.length()
    local last = n
    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)
  }
  _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
  }
  _scan_calls_from_text(text) {
    local arr = new ArrayBox(); if text == null { return arr }
    local lines = me._split_lines(text)
    local src_m = "Main.main/0"
    local i=0; while i < lines.size() {
      local ln = lines.get(i)
      // naive: detect patterns like "Main.foo("
      local pos = 0; local n = ln.length()
      loop (pos < n) {
        local k = ln.indexOf(".", pos); if k < 0 { break }
        // scan ident before '.'
        local lhs = me._scan_ident_rev(ln, k-1)
        // scan ident after '.'
        local rhs = me._scan_ident_fwd(ln, k+1)
        if lhs != null && rhs != null {
          local to = lhs + "." + rhs + "/0"
          local rec = new MapBox(); rec.set("from", src_m); rec.set("to", to); arr.push(rec)
        }
        pos = k + 1
      }
      i = i + 1
    }
    return arr
  }
  _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
  }
  _dfs(adj, node, seen) {
    if node == null { return }
    if seen.has(node) == 1 { return }
    seen.set(node, 1)
    if adj.has(node) == 0 { return }
    local arr = adj.get(node)
    local k = 0; while k < arr.size() { me._dfs(adj, arr.get(k), seen); k = k + 1 }
  }
}

static box RuleDeadMethodsMain { method main(args) { return 0 } }
