diff --git a/lang/src/llvm_ir/boxes/aot_prep/README.md b/lang/src/llvm_ir/boxes/aot_prep/README.md index 262b5a35..9fc47ba6 100644 --- a/lang/src/llvm_ir/boxes/aot_prep/README.md +++ b/lang/src/llvm_ir/boxes/aot_prep/README.md @@ -16,6 +16,14 @@ - CollectionsHot(`NYASH_AOT_COLLECTIONS_HOT=1`) - Array/Map の単純 `boxcall` を `externcall`(`nyash.array.*` / `nyash.map.*`)へ張替え - Map キー戦略は `NYASH_AOT_MAP_KEY_MODE={h|i64|hh|auto}`(既定: `h`/`i64`) + - 型確定の優先順位(上から順に試行): + 1. 型テーブル(`type_table.has(bvid)`) + 2. PHI型推論(`peek_phi_type`) + 3. **後方MIR解析(`resolve_recv_type_backward`)** - NEW! + 4. メソッド専用判別(push は Array のみ) + 5. 直接マップ(`arr_recv`/`map_recv`) + 6. キー文脈ヒューリスティック(`is_stringy_key_in_block`) + 7. newbox 逆スキャン - BinopCSE(`NYASH_MIR_LOOP_HOIST=1`) - 同一 binop(加算/乗算を含む)を1回だけ発行し、`copy` で再利用することで線形インデックス(例: `i*n+k`)の再計算を抑制 - `LoopHoist` と組み合わせることで `const` + `binop` の共通部分を前出しする汎用CSEになっている @@ -25,6 +33,7 @@ ENV トグル一覧 - `NYASH_AOT_COLLECTIONS_HOT=1` … Array/Map hot-path 置換を有効化 - `NYASH_AOT_MAP_KEY_MODE` … `h`/`i64`(既定), `hh`, `auto`(将来拡張) - `auto` モードでは `_is_const_or_linear` で args を走査し、単純な i64 定数/線形表現と判定できる場合に `nyash.map.*_h` を選ぶ(それ以外は `*_hh`)。 +- `NYASH_AOT_CH_TRACE=1` … CollectionsHot の詳細診断出力(型推論・書き換え情報) - `NYASH_VERIFY_RET_PURITY=1` … Return 直前の副作用を Fail-Fast で検出。ベンチはこのトグルをオンにした状態で回しています。 使い方(例) diff --git a/lang/src/llvm_ir/boxes/aot_prep/passes/collections_hot.hako b/lang/src/llvm_ir/boxes/aot_prep/passes/collections_hot.hako index bd0e6021..1e5fde6d 100644 --- a/lang/src/llvm_ir/boxes/aot_prep/passes/collections_hot.hako +++ b/lang/src/llvm_ir/boxes/aot_prep/passes/collections_hot.hako @@ -4,11 +4,339 @@ using selfhost.shared.common.string_helpers as StringHelpers using selfhost.llvm.ir.aot_prep.helpers.common as AotPrepHelpers // for is_const_or_linear and _seek_object_end static box AotPrepCollectionsHotBox { + // Small helpers for field reads + read_field(text, key) { + local needle = "\"" + key + "\":\"" + local idx = text.indexOf(needle) + if idx < 0 { return "" } + return JsonFragBox.read_string_after(text, idx + needle.length()) + } + read_digits_field(text, key) { + local needle = "\"" + key + "\":" + local idx = text.indexOf(needle) + if idx < 0 { return "" } + return StringHelpers.read_digits(text, idx + needle.length()) + } + + // Build lightweight type table: vid -> "arr" | "map" + build_type_table(text) { + local tmap = new MapBox() + // multi-pass to propagate copy/phi until fixpoint (cap at 12) + local pass = 0 + loop(pass < 12) { + local before = tmap.size() + local i = 0 + loop(true) { + local os = text.indexOf("{", i) + if os < 0 { break } + local oe = AotPrepHelpers._seek_object_end(text, os) + if oe < 0 { break } + local inst = text.substring(os, oe+1) + local op = AotPrepCollectionsHotBox.read_field(inst, "op") + if op == "newbox" { + local dst = AotPrepCollectionsHotBox.read_digits_field(inst, "dst") + if dst != "" { + if inst.indexOf("\"type\":\"ArrayBox\"") >= 0 { tmap.set(dst, "arr") } + if inst.indexOf("\"type\":\"MapBox\"") >= 0 { tmap.set(dst, "map") } + } + } else if op == "copy" { + local dst = AotPrepCollectionsHotBox.read_digits_field(inst, "dst") + local src = AotPrepCollectionsHotBox.read_digits_field(inst, "src") + if dst != "" && src != "" && tmap.has(src) && !tmap.has(dst) { tmap.set(dst, tmap.get(src)) } + } else if op == "phi" { + local dst = AotPrepCollectionsHotBox.read_digits_field(inst, "dst") + if dst != "" { + local kin = inst.indexOf("\"incoming\":[") + if kin >= 0 { + local abr = inst.indexOf("[", kin) + if abr >= 0 { + local aend = JsonFragBox._seek_array_end(inst, abr) + if aend > abr { + local body = inst.substring(abr+1, aend) + // Accept only when ALL incomings are already typed and all equal + local tval = ""; local all_same = 1 + local posb = body.indexOf("[") + loop(posb >= 0) { + local vid = StringHelpers.read_digits(body, posb+1) + if vid != "" && tmap.has(vid) { + local tv = tmap.get(vid) + if tval == "" { tval = tv } else { if tv != tval { all_same = 0 break } } + } else { all_same = 0 break } + posb = body.indexOf("[", posb+1) + } + if all_same == 1 && tval != "" { tmap.set(dst, tval) } + } + } + } + } + } + i = oe + 1 + } + // Fixpoint check + if tmap.size() == before { break } + pass = pass + 1 + } + return tmap + } + // Static helpers replacing local fun + find_block_span(text, p) { + local key = "\"instructions\":[" + // single-arg lastIndexOf: search within prefix + local prefix = text.substring(0, p) + local start = prefix.lastIndexOf(key) + if start < 0 { return "" } + local lb = text.indexOf("[", start) + if lb < 0 { return "" } + local rb = JsonFragBox._seek_array_end(text, lb) + if rb < 0 { return "" } + return StringHelpers.int_to_str(lb) + ":" + StringHelpers.int_to_str(rb) + } + + // Try to infer type for a value id from its PHI definition on the fly. + // copy_src is used to resolve copy chains in incoming vids. + // Returns "arr" | "map" | "" (unknown). + peek_phi_type(text, dst, tmap, copy_src) { + if dst == "" { return "" } + // Scan objects and find a phi with matching dst + local i = 0 + loop(true) { + local os = text.indexOf("{", i) + if os < 0 { break } + local oe = AotPrepHelpers._seek_object_end(text, os) + if oe < 0 { break } + local inst = text.substring(os, oe+1) + local op = AotPrepCollectionsHotBox.read_field(inst, "op") + if op == "phi" { + local kdst = inst.indexOf("\"dst\":") + local dvid = (kdst>=0 ? StringHelpers.read_digits(inst, kdst+6) : "") + if dvid == dst { + local kin = inst.indexOf("\"incoming\":[") + if kin >= 0 { + local abr = inst.indexOf("[", kin) + if abr >= 0 { + local aend = JsonFragBox._seek_array_end(inst, abr) + if aend > abr { + local body = inst.substring(abr+1, aend) + local tval = ""; local all_same = 1 + local posb = body.indexOf("[") + loop(posb >= 0) { + local vid = StringHelpers.read_digits(body, posb+1) + // Follow copy chains to original typed vid if available + if vid != "" { vid = AotPrepCollectionsHotBox.resolve_copy(copy_src, vid) } + if vid != "" && tmap.has(vid) { + local tv = tmap.get(vid) + if tval == "" { tval = tv } else { if tv != tval { all_same = 0 break } } + } else { all_same = 0 break } + posb = body.indexOf("[", posb+1) + } + if all_same == 1 && tval != "" { return tval } + } + } + } + } + } + i = oe + 1 + } + return "" + } + resolve_copy(copy_src, vid) { + local cur = vid + local depth = 0 + loop(true){ if cur == "" { break } if !copy_src.has(cur) { break } cur = copy_src.get(cur); depth = depth + 1; if depth >= 12 { break } } + return cur + } + + // Resolve receiver type by backward MIR output analysis. + // Scans [lb, k) range backward to find last dst assignment for bvid, then analyzes its type. + // Returns "arr" | "map" | "" (unknown). + resolve_recv_type_backward(out, bvid, lb, k, tmap, copy_src) { + if bvid == "" { return "" } + // Early return if already in type table (avoid redundant work) + if tmap.has(bvid) { return "" } + + // Step 1: Resolve bvid through copy chains to original source + local resolved_vid = AotPrepCollectionsHotBox.resolve_copy(copy_src, bvid) + if resolved_vid == "" { resolved_vid = bvid } + + // Step 2: Backward scan in [lb, k) range for last "dst": occurrence + local needle = "\"dst\":" + resolved_vid + local block_slice = out.substring(lb, k) + local last_pos = block_slice.lastIndexOf(needle) + if last_pos < 0 { return "" } + + // Convert to absolute position + local abs_pos = lb + last_pos + + // Step 3: Find the object containing this dst assignment + local obj_start = out.substring(0, abs_pos).lastIndexOf("{") + if obj_start < 0 { return "" } + local obj_end = AotPrepHelpers._seek_object_end(out, obj_start) + if obj_end < 0 || obj_end >= k { return "" } + + local inst = out.substring(obj_start, obj_end+1) + local op = AotPrepCollectionsHotBox.read_field(inst, "op") + + // Step 4: Analyze instruction to determine type + if op == "newbox" { + if inst.indexOf("\"type\":\"ArrayBox\"") >= 0 { return "arr" } + if inst.indexOf("\"type\":\"MapBox\"") >= 0 { return "map" } + return "" + } else if op == "copy" { + // Follow copy chain recursively (with depth limit) + local ksrc = inst.indexOf("\"src\":") + if ksrc >= 0 { + local src_vid = StringHelpers.read_digits(inst, ksrc+6) + if src_vid != "" { + local depth = 0 + loop(depth < 12) { + if tmap.has(src_vid) { return tmap.get(src_vid) } + if !copy_src.has(src_vid) { break } + src_vid = copy_src.get(src_vid) + depth = depth + 1 + } + // Try backward scan again on resolved src_vid + if src_vid != "" && src_vid != resolved_vid { + return AotPrepCollectionsHotBox.resolve_recv_type_backward(out, src_vid, lb, k, tmap, copy_src) + } + } + } + return "" + } else if op == "phi" { + // Reuse peek_phi_type logic + local kdst = inst.indexOf("\"dst\":") + local phi_dst = (kdst>=0 ? StringHelpers.read_digits(inst, kdst+6) : "") + if phi_dst != "" { + return AotPrepCollectionsHotBox.peek_phi_type(out, phi_dst, tmap, copy_src) + } + return "" + } + + return "" + } + expr_key_in_block(text, block_lb, k, vid, copy_src) { + if vid == "" { return "" } + local needle = "\"dst\":" + vid + local pos = JsonFragBox.index_of_from(text, needle, block_lb) + local last = -1 + while pos >= 0 && pos < k { last = pos; pos = JsonFragBox.index_of_from(text, needle, pos + 1) } + if last < 0 { return "" } + local os = text.substring(0, last).lastIndexOf("{") + if os < 0 { return "" } + local oe = AotPrepHelpers._seek_object_end(text, os) + if oe < 0 || oe >= k { return "" } + local inst = text.substring(os, oe+1) + if inst.indexOf("\"op\":\"const\"") >= 0 { + local kval = inst.indexOf("\"value\":{\"type\":\"i64\",\"value\":") + local val = (kval>=0 ? StringHelpers.read_digits(inst, kval+30) : "") + if val != "" { return "const:" + val } + } + if inst.indexOf("\"op\":\"copy\"") >= 0 { + local ksrc = inst.indexOf("\"src\":") + local svid = (ksrc>=0 ? StringHelpers.read_digits(inst, ksrc+6) : "") + if svid != "" { return "copy:" + AotPrepCollectionsHotBox.resolve_copy(copy_src, svid) } + } + if inst.indexOf("\"op\":\"binop\"") >= 0 { + local lhs_pos = inst.indexOf("\"lhs\":") + local rhs_pos = inst.indexOf("\"rhs\":") + local op_key = inst.indexOf("\"operation\":\"") + if lhs_pos >= 0 && rhs_pos >= 0 && op_key >= 0 { + local lhs = StringHelpers.read_digits(inst, lhs_pos + 6) + local rhs = StringHelpers.read_digits(inst, rhs_pos + 6) + local op = JsonFragBox.read_string_after(inst, op_key + 13) + lhs = AotPrepCollectionsHotBox.resolve_copy(copy_src, lhs); rhs = AotPrepCollectionsHotBox.resolve_copy(copy_src, rhs) + if op == "+" || op == "add" || op == "*" || op == "mul" { + local li = StringHelpers.to_i64(lhs) + local ri = StringHelpers.to_i64(rhs) + if li != null && ri != null && ri < li { local tmp = lhs; lhs = rhs; rhs = tmp } + } + return "binop:" + op + ":" + lhs + ":" + rhs + } + } + return "" + } + + // Heuristic: detect if key vid in this block is string-ish (derived from StringBox/toString/binop '+') + is_stringy_key_in_block(text, block_lb, k, vid, copy_src) { + if vid == "" { return 0 } + local needle = "\"dst\":" + vid + local pos = JsonFragBox.index_of_from(text, needle, block_lb) + local last = -1 + while pos >= 0 && pos < k { last = pos; pos = JsonFragBox.index_of_from(text, needle, pos + 1) } + if last < 0 { return 0 } + local os = text.substring(0, last).lastIndexOf("{") + if os < 0 { return 0 } + local oe = AotPrepHelpers._seek_object_end(text, os) + if oe < 0 || oe >= k { return 0 } + local inst = text.substring(os, oe+1) + // Quick checks + if inst.indexOf("\"method\":\"toString\"") >= 0 { return 1 } + if inst.indexOf("\"op\":\"binop\"") >= 0 { + // If block contains a StringBox const before k, treat as stringy join + local head = text.substring(block_lb, k) + if head.indexOf("\"box_type\":\"StringBox\"") >= 0 { return 1 } + } + return 0 + } + find_last_set_index_in_block(text, block_lb, k, recv_vid) { + if recv_vid == "" { return "" } + local slice = text.substring(block_lb, k) + local p = slice.lastIndexOf("\"op\":\"boxcall\"") + while p >= 0 { + local abs = block_lb + p + local os = text.substring(0, abs).lastIndexOf("{") + if os < 0 { break } + local oe = AotPrepHelpers._seek_object_end(text, os) + if oe < 0 || oe >= k { break } + local inst = text.substring(os, oe+1) + if inst.indexOf("\"method\":\"set\"") >= 0 { + local kbox = inst.indexOf("\"box\":") + local bid = (kbox>=0 ? StringHelpers.read_digits(inst, kbox+6) : "") + if bid == recv_vid { + local kargs = inst.indexOf("\"args\":[") + if kargs >= 0 { + local idx = StringHelpers.read_digits(inst, kargs+8) + if idx != "" { return idx } + } + } + } + p = slice.lastIndexOf("\"op\":\"boxcall\"", p-1) + } + return "" + } + find_last_set_key_in_block(text, block_lb, k, recv_vid) { + if recv_vid == "" { return "" } + local slice = text.substring(block_lb, k) + local p = slice.lastIndexOf("\"op\":\"boxcall\"") + while p >= 0 { + local abs = block_lb + p + local os = text.substring(0, abs).lastIndexOf("{") + if os < 0 { break } + local oe = AotPrepHelpers._seek_object_end(text, os) + if oe < 0 || oe >= k { break } + local inst = text.substring(os, oe+1) + if inst.indexOf("\"method\":\"set\"") >= 0 { + local kbox = inst.indexOf("\"box\":") + local bid = (kbox>=0 ? StringHelpers.read_digits(inst, kbox+6) : "") + if bid == recv_vid { + local kargs = inst.indexOf("\"args\":[") + if kargs >= 0 { + local keyvid = StringHelpers.read_digits(inst, kargs+8) + if keyvid != "" { return keyvid } + } + } + } + p = slice.lastIndexOf("\"op\":\"boxcall\"", p-1) + } + return "" + } run(json) { if json == null { return null } - local arr_recv = {} - local map_recv = {} - local copy_src = {} + local arr_recv = new MapBox() + local map_recv = new MapBox() + local copy_src = new MapBox() + // diagnostics counters (gated by NYASH_AOT_CH_TRACE) + local diag_rewrites = 0 + local diag_unknown = 0 { local pos = 0 loop(true){ @@ -17,8 +345,8 @@ static box AotPrepCollectionsHotBox { local kdst = JsonFragBox.index_of_from(json, "\"dst\":", k) local dsts = StringHelpers.read_digits(json, kdst+6) if dsts != "" { - if JsonFragBox.index_of_from(json, "\"type\":\"ArrayBox\"", k) >= 0 { arr_recv[dsts] = 1 } - if JsonFragBox.index_of_from(json, "\"type\":\"MapBox\"", k) >= 0 { map_recv[dsts] = 1 } + if JsonFragBox.index_of_from(json, "\"type\":\"ArrayBox\"", k) >= 0 { arr_recv.set(dsts, 1) } + if JsonFragBox.index_of_from(json, "\"type\":\"MapBox\"", k) >= 0 { map_recv.set(dsts, 1) } } pos = k + 1 } @@ -31,171 +359,211 @@ static box AotPrepCollectionsHotBox { if kdst >= 0 && ksrc >= 0 { local dsts = StringHelpers.read_digits(json, kdst+6) local srcs = StringHelpers.read_digits(json, ksrc+6) - if dsts != "" && srcs != "" { copy_src[dsts] = srcs } + if dsts != "" && srcs != "" { copy_src.set(dsts, srcs) } } pos = k + 1 } } - if arr_recv.size() == 0 && map_recv.size() == 0 { return json } + if arr_recv.size() == 0 && map_recv.size() == 0 { + // Even if no direct newbox, we may have types via copies/phi; still attempt table + } + // Prepare out text before analysis to align IDs with rewrite scan local out = json + // Supplement copy_src by scanning current out text as well + { + local pos = 0 + loop(true){ + local k = JsonFragBox.index_of_from(out, "\"op\":\"copy\"", pos) + if k < 0 { break } + local kdst = JsonFragBox.index_of_from(out, "\"dst\":", k) + local ksrc = JsonFragBox.index_of_from(out, "\"src\":", k) + if kdst >= 0 && ksrc >= 0 { + local dsts = StringHelpers.read_digits(out, kdst+6) + local srcs = StringHelpers.read_digits(out, ksrc+6) + if dsts != "" && srcs != "" && !copy_src.has(dsts) { copy_src.set(dsts, srcs) } + } + pos = k + 1 + } + } + // Build lightweight SSA type table (arr/map) on out text for consistency with rewrite scan + local type_table = AotPrepCollectionsHotBox.build_type_table(out) + { + // Optional trace + local tr = env.get("NYASH_AOT_CH_TRACE") + if tr != null && (""+tr) == "1" { + print("[aot/collections_hot] arr_recv=" + arr_recv.size() + ", map_recv=" + map_recv.size() + ", tmap_size=" + type_table.size()) + } + } local key_mode = env.get("NYASH_AOT_MAP_KEY_MODE") - local find_block_span = fun(text, p) { - local key = "\"instructions\":[" - local start = text.lastIndexOf(key, p) - if start < 0 { return [-1, -1] } - local lb = text.indexOf("[", start) - if lb < 0 { return [-1, -1] } - local rb = JsonFragBox._seek_array_end(text, lb) - if rb < 0 { return [-1, -1] } - return [lb, rb] - } - local resolve_copy = fun(vid) { - local cur = vid - local depth = 0 - loop(true){ if cur == "" { break } if !copy_src.contains(cur) { break } cur = copy_src[cur]; depth = depth + 1; if depth >= 12 { break } } - return cur - } - local expr_key_in_block = fun(text, block_lb, k, vid) { - if vid == "" { return "" } - local needle = "\"dst\":" + vid - local pos = JsonFragBox.index_of_from(text, needle, block_lb) - local last = -1 - while pos >= 0 && pos < k { last = pos; pos = JsonFragBox.index_of_from(text, needle, pos + 1) } - if last < 0 { return "" } - local os = text.lastIndexOf("{", last) - if os < 0 { return "" } - local oe = AotPrepHelpers._seek_object_end(text, os) - if oe < 0 || oe >= k { return "" } - local inst = text.substring(os, oe+1) - if inst.indexOf("\"op\":\"const\"") >= 0 { - local kval = inst.indexOf("\"value\":{\"type\":\"i64\",\"value\":") - local val = (kval>=0 ? StringHelpers.read_digits(inst, kval+30) : "") - if val != "" { return "const:" + val } - } - if inst.indexOf("\"op\":\"copy\"") >= 0 { - local ksrc = inst.indexOf("\"src\":") - local svid = (ksrc>=0 ? StringHelpers.read_digits(inst, ksrc+6) : "") - if svid != "" { return "copy:" + resolve_copy(svid) } - } - if inst.indexOf("\"op\":\"binop\"") >= 0 { - local lhs_pos = inst.indexOf("\"lhs\":") - local rhs_pos = inst.indexOf("\"rhs\":") - local op_key = inst.indexOf("\"operation\":\"") - if lhs_pos >= 0 && rhs_pos >= 0 && op_key >= 0 { - local lhs = StringHelpers.read_digits(inst, lhs_pos + 6) - local rhs = StringHelpers.read_digits(inst, rhs_pos + 6) - local op = JsonFragBox.read_string_after(inst, op_key + 13) - lhs = resolve_copy(lhs); rhs = resolve_copy(rhs) - if op == "+" || op == "add" || op == "*" || op == "mul" { - local li = StringHelpers.to_i64(lhs) - local ri = StringHelpers.to_i64(rhs) - if li != null && ri != null && ri < li { local tmp = lhs; lhs = rhs; rhs = tmp } - } - return "binop:" + op + ":" + lhs + ":" + rhs - } - } - return "" - } - // helper: find last set(box,a0,*) for same receiver inside block - local find_last_set_index_in_block = fun(text, block_lb, k, recv_vid) { - if recv_vid == "" { return "" } - // naive scan: look back in block slice for boxcall set with same box - local slice = text.substring(block_lb, k) - local p = slice.lastIndexOf("\"op\":\"boxcall\"") - while p >= 0 { - local abs = block_lb + p - local os = text.lastIndexOf("{", abs) - if os < 0 { break } - local oe = AotPrepHelpers._seek_object_end(text, os) - if oe < 0 || oe >= k { break } - local inst = text.substring(os, oe+1) - if inst.indexOf("\"method\":\"set\"") >= 0 { - local kbox = inst.indexOf("\"box\":") - local bid = (kbox>=0 ? StringHelpers.read_digits(inst, kbox+6) : "") - if bid == recv_vid { - local kargs = inst.indexOf("\"args\":[") - if kargs >= 0 { - local idx = StringHelpers.read_digits(inst, kargs+8) - if idx != "" { return idx } - } - } - } - // search previous occurrence - p = slice.lastIndexOf("\"op\":\"boxcall\"", p-1) - } - return "" - } - // helper: find last set(map,a0,*) key vid for same receiver inside block - local find_last_set_key_in_block = fun(text, block_lb, k, recv_vid) { - if recv_vid == "" { return "" } - local slice = text.substring(block_lb, k) - local p = slice.lastIndexOf("\"op\":\"boxcall\"") - while p >= 0 { - local abs = block_lb + p - local os = text.lastIndexOf("{", abs) - if os < 0 { break } - local oe = AotPrepHelpers._seek_object_end(text, os) - if oe < 0 || oe >= k { break } - local inst = text.substring(os, oe+1) - if inst.indexOf("\"method\":\"set\"") >= 0 { - local kbox = inst.indexOf("\"box\":") - local bid = (kbox>=0 ? StringHelpers.read_digits(inst, kbox+6) : "") - if bid == recv_vid { - local kargs = inst.indexOf("\"args\":[") - if kargs >= 0 { - local keyvid = StringHelpers.read_digits(inst, kargs+8) - if keyvid != "" { return keyvid } - } - } - } - p = slice.lastIndexOf("\"op\":\"boxcall\"", p-1) - } - return "" - } + + + + + + + + // rewrites proceed + + + + + + + local pos2 = 0 - local seen_key_vid = {} + local seen_key_vid = new MapBox() loop(true){ local k = JsonFragBox.index_of_from(out, "\"op\":\"boxcall\"", pos2) if k < 0 { break } - local kdst = JsonFragBox.index_of_from(out, "\"dst\":", k) - local dvid = (kdst>=0 ? StringHelpers.read_digits(out, kdst+6) : "") - local kbox = JsonFragBox.index_of_from(out, "\"box\":", k) - if kbox < 0 { pos2 = k + 1; continue } - local bvid = StringHelpers.read_digits(out, kbox+6) - if bvid != "" { bvid = resolve_copy(bvid) } - local mend = JsonFragBox.index_of_from(out, "\"method\":\"", k) - if mend < 0 { pos2 = k + 1; continue } - local mname = JsonFragBox.read_string_after(out, mend+10) - local is_arr = arr_recv.contains(bvid) - local is_map = map_recv.contains(bvid) - if !(is_arr || is_map) { pos2 = k + 1; continue } - local kargs = JsonFragBox.index_of_from(out, "\"args\":[", k) - local a0 = ""; local a1 = "" - if kargs >= 0 { - a0 = StringHelpers.read_digits(out, kargs+8) - local sep = out.indexOf(",", kargs) - if sep > 0 { a1 = StringHelpers.read_digits(out, sep+1) } + // Determine current object span and parse fields locally + local obj_start = -1; local obj_end = -1 + { + local sscan = out.substring(0, k) + local probe = sscan.lastIndexOf("{") + loop(probe >= 0) { + local oe = AotPrepHelpers._seek_object_end(out, probe) + if oe >= k { obj_start = probe obj_end = oe break } + sscan = out.substring(0, probe) + probe = sscan.lastIndexOf("{") + } } - if a0 != "" { a0 = resolve_copy(a0) } - do { - local span = find_block_span(out, k) - local lb = span[0]; local rb = span[1] - if lb >= 0 && rb >= 0 { - local key = expr_key_in_block(out, lb, k, a0) - if key != "" { - local sc_key = (is_map ? "map:" : "arr:") + bvid + "|" + key - if seen_key_vid.contains(sc_key) { a0 = seen_key_vid[sc_key] } else { seen_key_vid[sc_key] = a0 } - } else if is_arr && mname == "get" { - // Safe set->get index reuse inside same block - local prev_idx = find_last_set_index_in_block(out, lb, k, bvid) - if prev_idx != "" { a0 = prev_idx } - } else if is_map && mname == "get" { - // Fallback: reuse last set key vid inside block for same map receiver - local prev_key = find_last_set_key_in_block(out, lb, k, bvid) - if prev_key != "" { a0 = prev_key } + if obj_start < 0 || obj_end < 0 { pos2 = k + 1; continue } + local inst = out.substring(obj_start, obj_end+1) + local kdst = inst.indexOf("\"dst\":") + local dvid = (kdst>=0 ? StringHelpers.read_digits(inst, kdst+6) : "") + local kbox = inst.indexOf("\"box\":") + if kbox < 0 { pos2 = k + 1; continue } + local bvid = StringHelpers.read_digits(inst, kbox+6) + if bvid != "" { bvid = AotPrepCollectionsHotBox.resolve_copy(copy_src, bvid) } + local mend = inst.indexOf("\"method\":\"") + if mend < 0 { pos2 = k + 1; continue } + local mname = JsonFragBox.read_string_after(inst, mend+10) + // Calculate block span early for backward resolution + local span = AotPrepCollectionsHotBox.find_block_span(out, k) + local lb = -1; local rb = -1 + if span != null && span != "" { + local sep = span.indexOf(":") + if sep >= 0 { + local s_lb = span.substring(0, sep) + local s_rb = span.substring(sep+1, span.length()) + lb = StringHelpers.to_i64(s_lb) + rb = StringHelpers.to_i64(s_rb) + } + } + // Prefer SSA type table; fallback to direct maps/newbox-scan + local ttag = (type_table.has(bvid) ? ("" + type_table.get(bvid)) : "") + local is_arr = (ttag == "arr") + local is_map = (ttag == "map") + if !(is_arr || is_map) { + // On-the-fly PHI peek when table lacks the receiver (resolve copy chains) + local phi_t = AotPrepCollectionsHotBox.peek_phi_type(out, bvid, type_table, copy_src) + if phi_t == "arr" { ttag = "arr"; is_arr = 1 } + else if phi_t == "map" { ttag = "map"; is_map = 1 } + } + if !(is_arr || is_map) { + // Backward MIR output analysis within current block + if lb != null && rb != null && lb >= 0 && rb >= 0 { + local back_t = AotPrepCollectionsHotBox.resolve_recv_type_backward(out, bvid, lb, k, type_table, copy_src) + if back_t == "arr" { + ttag = "arr"; is_arr = 1 + local tr = env.get("NYASH_AOT_CH_TRACE") + if tr != null && (""+tr) == "1" { + print("[aot/collections_hot] recv_backtrace => arr") + } + } else if back_t == "map" { + ttag = "map"; is_map = 1 + local tr = env.get("NYASH_AOT_CH_TRACE") + if tr != null && (""+tr) == "1" { + print("[aot/collections_hot] recv_backtrace => map") + } } } - } while(false) + } + if !(is_arr || is_map) { + // Method-specific disambiguation: 'push' exists only on ArrayBox + if mname == "push" { + is_arr = 1 + } + } + if !(is_arr || is_map) { + is_arr = arr_recv.has(bvid) + is_map = map_recv.has(bvid) + } + if !(is_arr || is_map) { + // Fallback classification: scan last newbox before this call (single-arg lastIndexOf) + local prefix = out.substring(0, k) + local marker = prefix.lastIndexOf("\"op\":\"newbox\"") + if marker >= 0 { + local os_nb = out.substring(0, marker).lastIndexOf("{") + if os_nb >= 0 { + local oe_nb = AotPrepHelpers._seek_object_end(out, os_nb) + if oe_nb > os_nb { + local inst_nb = out.substring(os_nb, oe_nb+1) + local kdst_nb = inst_nb.indexOf("\"dst\":") + local dst_nb = (kdst_nb>=0 ? StringHelpers.read_digits(inst_nb, kdst_nb+6) : "") + { + local tr = env.get("NYASH_AOT_CH_TRACE") + if tr != null && (""+tr) == "1" { + print("[aot/collections_hot] nb.dst=" + dst_nb + ", expect recv=" + bvid + ", typeArr=" + (inst_nb.indexOf("\"ArrayBox\"")>=0) + ", typeMap=" + (inst_nb.indexOf("\"MapBox\"")>=0)) + } + } + if dst_nb == bvid { + if inst_nb.indexOf("\"type\":\"ArrayBox\"") >= 0 { is_arr = 1 } + if inst_nb.indexOf("\"type\":\"MapBox\"") >= 0 { is_map = 1 } + } + } } + } + } + // Defer unknown handling until after key/idx heuristics + local kargs = inst.indexOf("\"args\":[") + local a0 = ""; local a1 = "" + if kargs >= 0 { + a0 = StringHelpers.read_digits(inst, kargs+8) + local sep = inst.indexOf(",", kargs) + if sep > 0 { a1 = StringHelpers.read_digits(inst, sep+1) } + } + if a0 != "" { a0 = AotPrepCollectionsHotBox.resolve_copy(copy_src, a0) } + { + // Trace type table classification before any rewrite to locate Unknowns + local tr = env.get("NYASH_AOT_CH_TRACE") + if tr != null && (""+tr) == "1" { + local tlab = (ttag == "arr" ? "Arr" : (ttag == "map" ? "Map" : "Unknown")) + print("[aot/collections_hot] pre-rewrite type_table recv=" + bvid + " => " + tlab + ", mname=" + mname + ", a0=" + a0 + ", a1=" + a1) + // Keep legacy line as well for quick skim + print("[aot/collections_hot] visit mname=" + mname + ", is_arr=" + is_arr + ", is_map=" + is_map + ", a0=" + a0 + ", a1=" + a1) + } + } + // Within the same block, try safe key/index reuse (lb/rb already calculated earlier) + if lb != null && rb != null && lb >= 0 && rb >= 0 { + if !(is_arr || is_map) { + // Context heuristic: for get/set/has, string-like key => Map, otherwise Array + if (mname == "get" || mname == "set" || mname == "has") { + if AotPrepCollectionsHotBox.is_stringy_key_in_block(out, lb, k, a0, copy_src) { is_map = 1 } else { is_arr = 1 } + } + } + local key = AotPrepCollectionsHotBox.expr_key_in_block(out, lb, k, a0, copy_src) + if key != "" { + local sc_key = (is_map ? "map:" : "arr:") + bvid + "|" + key + if seen_key_vid.has(sc_key) { a0 = seen_key_vid.get(sc_key) } else { seen_key_vid.set(sc_key, a0) } + } else if is_arr && mname == "get" { + // Safe set->get index reuse inside same block + local prev_idx = AotPrepCollectionsHotBox.find_last_set_index_in_block(out, lb, k, bvid) + if prev_idx != "" { a0 = prev_idx } + } else if is_map && mname == "get" { + // Fallback: reuse last set key vid inside block for same map receiver + local prev_key = AotPrepCollectionsHotBox.find_last_set_key_in_block(out, lb, k, bvid) + if prev_key != "" { a0 = prev_key } + } + } + if !(is_arr || is_map) { + { + local tr = env.get("NYASH_AOT_CH_TRACE") + if tr != null && (""+tr) == "1" { + print("[aot/collections_hot] boxcall mname=" + mname + ", recv=" + bvid + " (not array/map)") + } + } + diag_unknown = diag_unknown + 1 + pos2 = k + 1; continue } local func = ""; local args = "" if is_arr { if mname == "get" { func = "nyash.array.get_h"; args = bvid + "," + a0 } @@ -218,16 +586,34 @@ static box AotPrepCollectionsHotBox { else if (mname == "len" || mname == "length" || mname == "size") { func = "nyash.map.size_h"; args = bvid } } } - if func == "" { pos2 = k + 1; continue } - local obj_start = out.lastIndexOf("{", k) - if obj_start < 0 { pos2 = k + 1; continue } - local obj_end = AotPrepHelpers._seek_object_end(out, obj_start) - if obj_end < 0 { pos2 = k + 1; continue } + if func == "" { + { + local tr = env.get("NYASH_AOT_CH_TRACE") + if tr != null && (""+tr) == "1" { + print("[aot/collections_hot] skip method=" + mname + ", is_arr=" + is_arr + ", is_map=" + is_map) + } + } + pos2 = k + 1; continue + } + // obj_start/obj_end already computed above local dst_part = (dvid != "" ? ("\"dst\":" + dvid + ",") : "") local repl = "{" + dst_part + "\"op\":\"externcall\",\"func\":\"" + func + "\",\"args\":[" + args + "]}" out = out.substring(0, obj_start) + repl + out.substring(obj_end+1, out.length()) + diag_rewrites = diag_rewrites + 1 + { + local tr = env.get("NYASH_AOT_CH_TRACE") + if tr != null && (""+tr) == "1" { + print("[aot/collections_hot] rewrite " + mname + " -> externcall " + func) + } + } pos2 = obj_start + repl.length() } + { + local tr = env.get("NYASH_AOT_CH_TRACE") + if tr != null && (""+tr) == "1" { + print("[aot/collections_hot] summary rewrites=" + diag_rewrites + ", unknown_recv=" + diag_unknown + ", tmap_size=" + type_table.size()) + } + } return out }