diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index 23f4c625..98f1fe7a 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -21,11 +21,31 @@ Update (today) - Analyzer CLI: --format/--debug/--source-file を順不同で処理 - Analyzer IR: AST 空時の methods をテキスト走査でフォールバック +Footing update (A→B→C, today) — 仕様と入口をまず固定 +- A. 仕様/インターフェース(hako_check 診断 I/F と出力規約) + - 診断スキーマ(型付き)を明文化: `{rule, message, line, severity?, quickFix?}`。 + - 互換性: ルールが文字列を `out.push("[HCxxx] ...")` で返す場合も受理(CLI 側で string→diag 変換)。 + - 抑制ルール: HC012 > HC011(Box 全体が dead の場合、個別メソッドの unreachable は抑制)。 + - 既定の閾値: HC012=warning、HC011=info(CLI 側で override 可能に)。 + - Quiet/JSON: `NYASH_JSON_ONLY=1`(JSON/LSP 生成時は冗長ログ抑止)、plugin_guard 出力は stderr のみ。 +- B. AST 拡張(解析に必要な最小メタ) + - `parser_core.hako`: `boxes[].span_line`(定義開始行)、`methods[].arity`(引数個数)、`is_static` を bool で統一。 + - `tokenizer.hako`: 行・桁の位置情報を維持(ブロックコメント除去後も一貫)。 +- C. Analyzer 適用(初期化の堅牢化と AST 優先) + - `analysis_consumer.hako`: `_ensure_array(ir, key)` を導入し、`methods/calls/boxes` へ `push` 前に必ず確保。 + - AST 取り込みを優先(`boxes/uses/includes` と `*_arr` の二系統に両対応)、欠落時のみ簡易テキスト走査にフォールバック。 + - CLI: ルール出力をそのまま透過(typed diag を優先、string は変換)、HC012>HC011 の抑制を集約段で適用。 + +Acceptance(Footing A→B→C) +- A: `tools/hako_check/run_tests.sh` が `--format json-lsp` で純 JSON を返し、HC012/HC011 混在入力で抑制が働く。 +- B: `parser_core.hako` の AST に `span_line`/`arity` が入り、ダンプで確認可能(最小ケース2件)。 +- C: `analysis_consumer.hako` が未初期化 IR に対しても `push` で落ちず、AST 経路で HC011/HC012 が緑。 + Remaining (21.4) -1) Hako Parser MVP 実装(tokenizer/parser_core/ast_emit/cli) -2) Analyzer AST 入力切替(analysis_consumer.hako) +1) Hako Parser MVP 実装(tokenizer/parser_core/ast_emit/cli)【A/B 完了、微修整残り】 +2) Analyzer AST 入力切替(analysis_consumer.hako)【C: ensure_array 完了、AST優先の薄化継続】 3) 代表ルール AST 化(HC001/002/003/010/011/020) -4) `--format json-lsp` 出力 +4) `--format json-lsp` 出力【CLI整備済み、追加テスト】 5) テスト駆動(tests//)を 3 ルール分用意 6) 限定 `--fix`(HC002/003/500) 7) DOT エッジ ON(calls→edges, cluster by box) @@ -68,10 +88,21 @@ Next (21.2 — TBD) - dev は Adapter 登録や by‑name fallback を許容(トグル)、prod は Adapter 必須(Fail‑Fast)。 Next Steps (immediate) -1) B: plugin_guard の runner 全体適用(残存の直書き除去) -2) C: tokenizer/parser_core の tokens 精緻化 + methods 埋めの安定化(AST 経路で HC011 緑) -3) ast_emit.hako の安定化(整列/quote/数値) -4) `--format json-lsp` のテスト追加(OK/NG/edge) +1) C: AST優先取り込みの最終化(text fallback を最小に) +2) C: calls の arity 推定(実装済み)に基づく HC015 足場準備 +3) C: HC012 line 精度(span_line)適用(後回し可) +4) `--format json-lsp` の代表テスト追加(OK/NG/edge) + +Context Compression Notes +- A(診断I/F+抑制)は実装済み。文字列診断→型付き変換あり。 +- B(AST: span_line/is_static=bool)は実装済み。methods.arity は既に出力。 +- C は ensure_array と calls arity 推定を反映。残は AST優先の徹底と HC012 line 精度。 + +TODO (short-term, non-HC0) +- AST-first intake: minimize text fallback; ensure analyzer never uses FileBox in tests. +- DOT edges: keep unique edges; add box clusters (optional, post-21.4). +- Quiet JSON: enforce NYASH_JSON_ONLY=1 in wrappers; stderr-only plugin hints. +- HC015 prep: rely on inferred call arity for arity mismatch rule later. Rules Backlog(候補・優先提案) - HC012: Dead Static Box — 定義のみで参照/呼出ゼロの static box を検出 diff --git a/tools/hako_check/README.md b/tools/hako_check/README.md new file mode 100644 index 00000000..9b5c37e7 --- /dev/null +++ b/tools/hako_check/README.md @@ -0,0 +1,33 @@ +# Hako Check — Diagnostics Contract (MVP) + +This tool lints .hako sources and emits diagnostics. + +Diagnostics schema (typed) +- Map fields: + - `rule`: string like "HC011" + - `message`: string (human-readable, one line) + - `file`: string (path) + - `line`: int (1-based) + - `severity`: string ("error"|"warning"|"info"), optional (default: warning) + - `quickFix`: string, optional + +Backwards compatibility +- Rules may still `out.push("[HCxxx] ...")` with a single-line string. +- The CLI accepts both forms. String diagnostics are converted to typed internally. + +Suppression policy +- HC012 (dead static box) takes precedence over HC011 (unreachable method). +- If a box is reported by HC012, HC011 diagnostics for methods in that box are suppressed at aggregation. + +Quiet / JSON output +- When `--format json-lsp` is used, output is pure JSON (pretty). Combine with `NYASH_JSON_ONLY=1` in the runner to avoid extra lines. +- Non-JSON formats print human-readable lines per finding. + +Planned AST metadata (parser_core.hako) +- `boxes[].span_line`: starting line of the `static box` declaration. +- `methods[].arity`: parameter count as an integer. +- `boxes[].is_static`: boolean. + +Notes +- Prefer AST intake; text scans are a minimal fallback. +- For tests, use `tools/hako_check/run_tests.sh`. diff --git a/tools/hako_check/analysis_consumer.hako b/tools/hako_check/analysis_consumer.hako index 34529211..ef172c44 100644 --- a/tools/hako_check/analysis_consumer.hako +++ b/tools/hako_check/analysis_consumer.hako @@ -33,40 +33,58 @@ static box HakoAnalysisBuilderBox { 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 { ir.get("uses").push(ln.substring(q1+1, q2)) } + 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 + // 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 } } - if need_method_scan == 1 { + // 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 = ir.get("boxes") + 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 @@ -98,7 +120,8 @@ static box HakoAnalysisBuilderBox { 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) + 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 } @@ -111,8 +134,11 @@ static box HakoAnalysisBuilderBox { 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)) + // 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 } @@ -124,9 +150,10 @@ static box HakoAnalysisBuilderBox { // 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( + // 3) calls: naive pattern Box.method( or Alias.method( — arity推定付き // 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) @@ -143,9 +170,12 @@ static box HakoAnalysisBuilderBox { 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" + // 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); ir.get("calls").push(c) + local c = new MapBox(); c.set("from", src); c.set("to", tgt) + calls_arr.push(c) } pos = dot + 1 k = k + 1 @@ -315,6 +345,40 @@ static box HakoAnalysisBuilderBox { // This avoids runtime errors when method_spans is absent or malformed in MVP builds. return "Main.main" } + _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 } } diff --git a/tools/hako_check/cli.hako b/tools/hako_check/cli.hako index 18b9b587..beb04ea9 100644 --- a/tools/hako_check/cli.hako +++ b/tools/hako_check/cli.hako @@ -9,6 +9,8 @@ using tools.hako_check.rules.rule_dead_methods as RuleDeadMethodsBox using tools.hako_check.rules.rule_jsonfrag_usage as RuleJsonfragUsageBox using tools.hako_check.rules.rule_unused_alias as RuleUnusedAliasBox using tools.hako_check.rules.rule_non_ascii_quotes as RuleNonAsciiQuotesBox +using tools.hako_check.rules.rule_dead_static_box as RuleDeadStaticBoxBox +using tools.hako_check.rules.rule_duplicate_method as RuleDuplicateMethodBox using tools.hako_check.render.graphviz as GraphvizRenderBox using tools.hako_parser.parser_core as HakoParserCoreBox @@ -84,13 +86,29 @@ static box HakoAnalyzerBox { local added = after_n - before_n print("[hako_check/HC011] file=" + p + " added=" + me._itoa(added) + " total_out=" + me._itoa(after_n)) } - // flush - // for j in 0..(n-1) - local n = out.size(); if n > 0 && fmt == "text" { - local j = 0; while j < n { print(out.get(j)); j = j + 1 } + before_n = out.size() + RuleDeadStaticBoxBox.apply_ir(ir, p, out) + if debug == 1 { + local after_n = out.size() + local added = after_n - before_n + local boxes_count = (ir.get("boxes")!=null)?ir.get("boxes").size():0 + print("[hako_check/HC012] file=" + p + " boxes=" + me._itoa(boxes_count) + " added=" + me._itoa(added) + " total_out=" + me._itoa(after_n)) } - // also collect diagnostics for json-lsp - local j2 = 0; while j2 < n { local msg = out.get(j2); local d = me._parse_msg_to_diag(msg, p); if d != null { diags.push(d) }; j2 = j2 + 1 } + before_n = out.size() + RuleDuplicateMethodBox.apply_ir(ir, p, out) + if debug == 1 { + local after_n = out.size() + local added = after_n - before_n + print("[hako_check/HC013] file=" + p + " added=" + me._itoa(added) + " total_out=" + me._itoa(after_n)) + } + // suppression: HC012(dead box) > HC011(unreachable method) + local filtered = me._suppress_overlap(out) + // flush (text only) + local n = filtered.size(); if n > 0 && fmt == "text" { + local j = 0; while j < n { print(filtered.get(j)); j = j + 1 } + } + // collect diagnostics for json-lsp + local j2 = 0; while j2 < n { local msg = filtered.get(j2); local d = me._parse_msg_to_diag(msg, p); if d != null { diags.push(d) }; j2 = j2 + 1 } fail = fail + n } // optional DOT/JSON output @@ -142,6 +160,73 @@ static box HakoAnalyzerBox { print("]}") return 0 } + // Build dead-box set and drop HC011 for methods inside dead boxes + _suppress_overlap(out) { + if out == null { return new ArrayBox() } + // collect dead boxes from HC012 messages + local dead = new MapBox() + local i = 0 + while i < out.size() { + local s = out.get(i) + if me._is_hc012(s) == 1 { + local bx = me._extract_box_from_hc012(s) + if bx != null { dead.set(bx, 1) } + } + i = i + 1 + } + // filter + local res = new ArrayBox() + i = 0 + while i < out.size() { + local s = out.get(i) + if me._is_hc011(s) == 1 { + local qual = me._extract_method_from_hc011(s) + if qual != null { + // method qual: Box.method/arity → Box + local dot = qual.lastIndexOf(".") + if dot > 0 { + local box_name = qual.substring(0, dot) + if dead.has(box_name) == 1 { i = i + 1; continue } + } + } + } + res.push(s) + i = i + 1 + } + return res + } + _is_hc011(s) { + if s == null { return 0 } + if s.indexOf("[HC011]") == 0 { return 1 } + return 0 + } + _is_hc012(s) { + if s == null { return 0 } + if s.indexOf("[HC012]") == 0 { return 1 } + return 0 + } + _extract_box_from_hc012(s) { + // format: [HC012] dead static box (never referenced): Name + if s == null { return null } + local p = s.lastIndexOf(":") + if p < 0 { return null } + local name = s.substring(p+1) + // trim spaces + local t = 0; while t < name.length() { local c=name.substring(t,t+1); if c==" "||c=="\t" { t=t+1; continue } break } + if t > 0 { name = name.substring(t) } + return name + } + _extract_method_from_hc011(s) { + // format: [HC011] ... :: Box.method/arity + if s == null { return null } + local p = s.lastIndexOf("::") + if p < 0 { return null } + local qual = s.substring(p+2) + // trim leading space + local t = 0; while t < qual.length() { local c=qual.substring(t,t+1); if c==" "||c=="\t" { t=t+1; continue } break } + if t > 0 { qual = qual.substring(t) } + return qual + } _parse_msg_to_diag(msg, path) { if msg == null { return null } // Expect prefixes like: [HC002] ... path:LINE or [HC011] ... :: Method diff --git a/tools/hako_check/rules/rule_dead_static_box.hako b/tools/hako_check/rules/rule_dead_static_box.hako new file mode 100644 index 00000000..ce82a69e --- /dev/null +++ b/tools/hako_check/rules/rule_dead_static_box.hako @@ -0,0 +1,65 @@ +// tools/hako_check/rules/rule_dead_static_box.hako — HC012: Dead Static Box +// Detect static boxes that are never referenced from any code path. +// Excludes Main box (entry point) and boxes referenced in calls. + +using selfhost.shared.common.string_helpers as Str + +static box RuleDeadStaticBoxBox { + method apply_ir(ir, path, out) { + local boxes = ir.get("boxes") + if boxes == null { return 0 } + + local calls = ir.get("calls") + if calls == null { return 0 } + + // Collect all box names that are referenced in calls + local referenced_boxes = new MapBox() + local ci = 0 + while ci < calls.size() { + local call = calls.get(ci) + if call == null { ci = ci + 1; continue } + local to = call.get("to") + if to != null { + // Extract box name from qualified call (e.g., "Box.method/0" -> "Box") + local dot = to.indexOf(".") + if dot > 0 { + local box_name = to.substring(0, dot) + referenced_boxes.set(box_name, "1") + } + } + ci = ci + 1 + } + + // Check each box + local bi = 0 + while bi < boxes.size() { + local box_info = boxes.get(bi) + if box_info == null { bi = bi + 1; continue } + + local name = box_info.get("name") + if name == null { bi = bi + 1; continue } + + local is_static = box_info.get("is_static") + + // Skip Main box (entry point) + if name == "Main" { bi = bi + 1; continue } + + // Only check static boxes + if is_static == null || is_static == 0 { bi = bi + 1; continue } + + // Check if box is referenced - if not in map, it returns "[map/missing]" StringBox + local ref_check = referenced_boxes.get(name) + + // If ref_check is null or doesn't equal "1", box is unreferenced + if ref_check == null || ref_check != "1" { + // Box is never referenced - report error + out.push("[HC012] dead static box (never referenced): " + name) + } + + bi = bi + 1 + } + return out.size() + } +} + +static box RuleDeadStaticBoxMain { method main(args) { return 0 } } diff --git a/tools/hako_check/rules/rule_duplicate_method.hako b/tools/hako_check/rules/rule_duplicate_method.hako new file mode 100644 index 00000000..008291b6 --- /dev/null +++ b/tools/hako_check/rules/rule_duplicate_method.hako @@ -0,0 +1,83 @@ +// tools/hako_check/rules/rule_duplicate_method.hako — HC013: Duplicate Method +// Detect methods with the same name and arity defined multiple times in the same box. + +using selfhost.shared.common.string_helpers as Str + +static box RuleDuplicateMethodBox { + method apply_ir(ir, path, out) { + local boxes = ir.get("boxes") + if boxes == null { return 0 } + + // Check each box for duplicate methods + local bi = 0 + while bi < boxes.size() { + local box_info = boxes.get(bi) + if box_info == null { bi = bi + 1; continue } + + local box_name = box_info.get("name") + if box_name == null { bi = bi + 1; continue } + + local methods = box_info.get("methods") + if methods == null { bi = bi + 1; continue } + + // Track seen methods: {name/arity -> first_span} + local seen = new MapBox() + + local mi = 0 + while mi < methods.size() { + local method = methods.get(mi) + if method == null { mi = mi + 1; continue } + + local name = method.get("name") + local arity = method.get("arity") + if name == null || arity == null { mi = mi + 1; continue } + + // Create signature: name/arity + local sig = name + "/" + me._itoa(arity) + + // Check if already seen + local first_span = seen.get(sig) + if first_span != null { + // Check if it's a [map/missing] error + local first_span_str = first_span + "" + if first_span_str.indexOf("[map/missing]") != 0 { + // Duplicate detected! + local span = method.get("span") + local line = (span != null) ? span : 1 + out.push("[HC013] duplicate method definition: " + box_name + "." + sig + " at line " + me._itoa(line)) + } else { + // First occurrence + local span = method.get("span") + seen.set(sig, span) + } + } else { + // First occurrence + local span = method.get("span") + seen.set(sig, span) + } + + mi = mi + 1 + } + + bi = bi + 1 + } + return out.size() + } + + _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 + } +} + +static box RuleDuplicateMethodMain { method main(args) { return 0 } } diff --git a/tools/hako_check/run_tests.sh b/tools/hako_check/run_tests.sh index 994ba451..4c0194c9 100644 --- a/tools/hako_check/run_tests.sh +++ b/tools/hako_check/run_tests.sh @@ -45,6 +45,7 @@ run_case() { NYASH_DISABLE_NY_COMPILER=1 HAKO_DISABLE_NY_COMPILER=1 \ NYASH_PARSER_STAGE3=1 HAKO_PARSER_STAGE3=1 NYASH_PARSER_SEAM_TOLERANT=1 HAKO_PARSER_SEAM_TOLERANT=1 \ NYASH_ENABLE_USING=1 HAKO_ENABLE_USING=1 NYASH_USING_AST=1 \ + NYASH_JSON_ONLY=1 \ "$BIN" --backend vm tools/hako_check/cli.hako -- "${ARGS[@]}" >"$tmp_out" 2>&1 || true # Extract diagnostics JSON (one-line or pretty block) tmp_json="/tmp/hako_test_json_$$.json" diff --git a/tools/hako_check/tests/HC012_dead_static_box/expected.json b/tools/hako_check/tests/HC012_dead_static_box/expected.json new file mode 100644 index 00000000..ba95779b --- /dev/null +++ b/tools/hako_check/tests/HC012_dead_static_box/expected.json @@ -0,0 +1,3 @@ +{"diagnostics":[ + {"file":"ng.hako","line":1,"rule":"HC012","message":"[HC012] dead static box (never referenced): UnusedBox","quickFix":"","severity":"warning"} +]} diff --git a/tools/hako_check/tests/HC012_dead_static_box/ng.hako b/tools/hako_check/tests/HC012_dead_static_box/ng.hako new file mode 100644 index 00000000..1ef98f70 --- /dev/null +++ b/tools/hako_check/tests/HC012_dead_static_box/ng.hako @@ -0,0 +1,14 @@ +// ng.hako — contains dead static box (UnusedBox) + +static box UnusedBox { + method unused() { + return "never called" + } +} + +static box Main { + method main() { + // UnusedBox is never referenced + return 0 + } +} diff --git a/tools/hako_check/tests/HC012_dead_static_box/ok.hako b/tools/hako_check/tests/HC012_dead_static_box/ok.hako new file mode 100644 index 00000000..319c6431 --- /dev/null +++ b/tools/hako_check/tests/HC012_dead_static_box/ok.hako @@ -0,0 +1,15 @@ +// ok.hako — all static boxes are referenced + +static box HelperBox { + method helper() { + return "helper" + } +} + +static box Main { + method main() { + // HelperBox is referenced here + HelperBox.helper() + return 0 + } +} diff --git a/tools/hako_check/tests/HC013_duplicate_method/expected.json b/tools/hako_check/tests/HC013_duplicate_method/expected.json new file mode 100644 index 00000000..c9844da8 --- /dev/null +++ b/tools/hako_check/tests/HC013_duplicate_method/expected.json @@ -0,0 +1,3 @@ +{"diagnostics":[ + {"file":"ng.hako","line":1,"rule":"HC013","message":"[HC013] duplicate method definition: Calculator.add/2 at line 8","quickFix":"","severity":"warning"} +]} diff --git a/tools/hako_check/tests/HC013_duplicate_method/ng.hako b/tools/hako_check/tests/HC013_duplicate_method/ng.hako new file mode 100644 index 00000000..3dd3ceb2 --- /dev/null +++ b/tools/hako_check/tests/HC013_duplicate_method/ng.hako @@ -0,0 +1,18 @@ +// ng.hako — contains duplicate method with same name and arity + +static box Calculator { + method add(a, b) { + return a + b + } + + method add(a, b) { + return a + b + 1 + } +} + +static box Main { + method main() { + Calculator.add(1, 2) + return 0 + } +} diff --git a/tools/hako_check/tests/HC013_duplicate_method/ok.hako b/tools/hako_check/tests/HC013_duplicate_method/ok.hako new file mode 100644 index 00000000..9c51cf7f --- /dev/null +++ b/tools/hako_check/tests/HC013_duplicate_method/ok.hako @@ -0,0 +1,18 @@ +// ok.hako — no duplicate methods (overloading by arity is OK) + +static box Calculator { + method add(a, b) { + return a + b + } + + method add(a, b, c) { + return a + b + c + } +} + +static box Main { + method main() { + Calculator.add(1, 2) + return 0 + } +} diff --git a/tools/hako_parser/parser_core.hako b/tools/hako_parser/parser_core.hako index 16407715..522ad089 100644 --- a/tools/hako_parser/parser_core.hako +++ b/tools/hako_parser/parser_core.hako @@ -44,19 +44,24 @@ static box HakoParserCoreBox { // static box Name { methods } // STATIC BOX IDENT LBRACE ... RBRACE local save = p + local static_tok = t p = me._advance(p, N) // STATIC local tb = me._peek(toks, p, N) if me._eq(tb, "BOX") == 0 { p = save + 1; continue } + local box_tok = tb p = me._advance(p, N) local tn = me._peek(toks, p, N) if me._eq(tn, "IDENT") == 0 { continue } - local box_name = tn.get("lexeme"); p = me._advance(p, N) + local box_name = tn.get("lexeme"); + local box_line = tn.get("line"); + if box_line == null { box_line = static_tok.get("line") } + p = me._advance(p, N) // expect '{' local tl = me._peek(toks, p, N) if me._eq(tl, "LBRACE") == 0 { continue } p = me._advance(p, N) - // register box - local b = new MapBox(); b.set("name", box_name); b.set("is_static", 1); b.set("methods", new ArrayBox()) + // register box (bool is_static, and span_line metadata) + local b = new MapBox(); b.set("name", box_name); b.set("is_static", true); b.set("span_line", box_line); b.set("methods", new ArrayBox()) ast.get("boxes").push(b) // scan until matching RBRACE (flat, tolerate nested braces count) local depth = 1 @@ -80,7 +85,8 @@ static box HakoParserCoreBox { // consume any token inside params p = me._advance(p, N); any = 1 } - if any == 1 && arity == 0 { arity = 1 } + // arity = comma count + 1 (if non-empty) + if any == 1 { arity = arity + 1 } // record method local m = new MapBox(); m.set("name", mname); m.set("arity", arity); m.set("span", mline) b.get("methods").push(m)