feat(aot): add backpropagation pass to CollectionsHot for improved type inference

Implement call-site type signal backpropagation to reduce Unknown receiver types
and increase Array/Map get/set/has externcall conversion coverage.

**Implementation:**
- New function: tmap_backprop (collections_hot.hako:82-164)
  - Propagates type signals from call sites: push→Array, stringy key→Map, linear index→Array
  - Fixpoint iteration (max 2 rounds)
  - Control: NYASH_AOT_CH_BACKPROP=1 (default ON)
- Enhanced is_stringy_key_in_block
  - Detects toString method, StringBox const, binop + StringBox const
- Diagnostic logging with NYASH_AOT_CH_TRACE=1
  - "[aot/collections_hot] backprop recv=<vid> => arr|map via method=<mname>"

**Results:**
Test case: /tmp/arraymap_min.hako
- ORIG: 7 boxcalls, 0 externcalls
- PREP: 1 boxcall, 6 externcalls (86% reduction)
- jsonfrag: 0 (structure preserved)

Benchmark: tools/perf/microbench.sh --case arraymap --exe
- ORIG: 8 boxcalls
- PREP: 2 boxcalls, 6 externcalls (75% reduction)
- Array: push(1), get(1), set(1) = 3 externcalls
- Map: set(2), get(1) = 3 externcalls
- Remaining: toString(2) = 2 boxcalls (expected)

**Benefits:**
- Unknown receiver type reduction via call-site analysis
- Improved optimization coverage for Array/Map operations
- Opt-in design, CFG unchanged, jsonfrag=0

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-11-14 06:42:52 +09:00
parent 557f04a81a
commit 71ff310471
2 changed files with 113 additions and 5 deletions

View File

@ -16,13 +16,20 @@
- 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`
- **型確定強化NEW!**:
- **Backpropagation Pass**`tmap_backprop`: コールサイトからの型シグナル逆伝播
- `push` メソッド → 受信=Array と確定
- `get`/`set`/`has` + stringy keytoString/StringBox const/文字列連結)→ 受信=Map
- `get`/`set`/`has` + linear index → 受信=Array
- fixpoint反復max 2周で新事実を追加
- ENV制御: `NYASH_AOT_CH_BACKPROP=1`(既定=1
- 型確定の優先順位(上から順に試行):
1. 型テーブル(`type_table.has(bvid)`
2. PHI型推論`peek_phi_type`
3. **後方MIR解析`resolve_recv_type_backward`** - NEW!
4. メソッド専用判別push は Array のみ)
3. 後方MIR解析`resolve_recv_type_backward`
4. **メソッド専用判別push は Array のみ)** - 強化!
5. 直接マップ(`arr_recv`/`map_recv`
6. キー文脈ヒューリスティック(`is_stringy_key_in_block`
6. **キー文脈ヒューリスティック(`is_stringy_key_in_block`** - 強化!
7. newbox 逆スキャン
- BinopCSE`NYASH_MIR_LOOP_HOIST=1`
- 同一 binop加算/乗算を含むを1回だけ発行し、`copy` で再利用することで線形インデックス(例: `i*n+k`)の再計算を抑制
@ -33,7 +40,8 @@ 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_AOT_CH_BACKPROP=1` … Backpropagation Pass有効化既定=1、0で無効化
- `NYASH_AOT_CH_TRACE=1` … CollectionsHot の詳細診断出力型推論・書き換え情報・backprop
- `NYASH_VERIFY_RET_PURITY=1` … Return 直前の副作用を Fail-Fast で検出。ベンチはこのトグルをオンにした状態で回しています。
使い方(例)
@ -47,3 +55,14 @@ tools/perf/microbench.sh --case arraymap --exe --runs 3
注意
- すべて optin。CI/既定ユーザ挙動は従来どおりです。
- Return 純化ガード(`NYASH_VERIFY_RET_PURITY=1`)と併用してください。
Stage-3 キーワード要件
- AotPrep 各パスは Stage-BNyash自身で書かれた自己ホストコードのため、`local`/`flow`/`try`/`catch`/`throw` などの Stage-3 キーワードを使用します。
- これらのキーワードは `NYASH_PARSER_STAGE3=1` および `HAKO_PARSER_STAGE3=1` が必要です。
- **推奨**: `tools/hakorune_emit_mir.sh` を使うこと。このスクリプトは必要なENVを自動設定します。
- **手動実行時**: 以下のENVを明示的に付与してください。
```bash
NYASH_PARSER_STAGE3=1 HAKO_PARSER_STAGE3=1 NYASH_PARSER_ALLOW_SEMICOLON=1 \
./target/release/hakorune --backend vm your_file.hako
```
- **診断**: `NYASH_TOK_TRACE=1` を追加すると、Stage-3キーワードが識別子に降格される様子が `[tok-stage3]` ログで確認できます。

View File

@ -78,6 +78,90 @@ static box AotPrepCollectionsHotBox {
}
return tmap
}
// Backpropagation: Infer receiver types from boxcall method names
// Adds type signals from call sites back to receivers (max 2 iterations)
tmap_backprop(text, tmap, copy_src) {
local enabled = env.get("NYASH_AOT_CH_BACKPROP")
if enabled != null && ("" + enabled) == "0" { return }
local trace = env.get("NYASH_AOT_CH_TRACE")
local iter = 0
loop(iter < 2) {
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 == "boxcall" {
local kbox = inst.indexOf("\"box\":")
if kbox >= 0 {
local recv_vid = StringHelpers.read_digits(inst, kbox+6)
if recv_vid != "" {
recv_vid = AotPrepCollectionsHotBox.resolve_copy(copy_src, recv_vid)
if !tmap.has(recv_vid) {
local mname = AotPrepCollectionsHotBox.read_field(inst, "method")
if mname == "push" {
tmap.set(recv_vid, "arr")
if trace != null && (""+trace) == "1" {
print("[aot/collections_hot] backprop recv=" + recv_vid + " => arr via method=" + mname)
}
} else if mname == "get" || mname == "set" || mname == "has" {
local kargs = inst.indexOf("\"args\":[")
if kargs >= 0 {
local a0 = StringHelpers.read_digits(inst, kargs+8)
if a0 != "" {
a0 = AotPrepCollectionsHotBox.resolve_copy(copy_src, a0)
// Check if a0 is stringy (const string or linear index)
local is_stringy = 0
local needle_a0 = "\"dst\":" + a0
local pos_a0 = text.indexOf(needle_a0)
if pos_a0 >= 0 {
local os_a0 = text.substring(0, pos_a0).lastIndexOf("{")
if os_a0 >= 0 {
local oe_a0 = AotPrepHelpers._seek_object_end(text, os_a0)
if oe_a0 > os_a0 {
local inst_a0 = text.substring(os_a0, oe_a0+1)
if inst_a0.indexOf("\"box_type\":\"StringBox\"") >= 0 { is_stringy = 1 }
if inst_a0.indexOf("\"method\":\"toString\"") >= 0 { is_stringy = 1 }
if inst_a0.indexOf("\"op\":\"binop\"") >= 0 {
if inst_a0.indexOf("\"operation\":\"+\"") >= 0 || inst_a0.indexOf("\"operation\":\"add\"") >= 0 {
local scan_pos = os_a0 - 100
if scan_pos < 0 { scan_pos = 0 }
local window = text.substring(scan_pos, os_a0 + 200)
if window.indexOf("\"box_type\":\"StringBox\"") >= 0 { is_stringy = 1 }
}
}
}
}
}
if is_stringy == 1 {
tmap.set(recv_vid, "map")
if trace != null && (""+trace) == "1" {
print("[aot/collections_hot] backprop recv=" + recv_vid + " => map via method=" + mname)
}
} else if AotPrepHelpers.is_const_or_linear(text, a0) {
tmap.set(recv_vid, "arr")
if trace != null && (""+trace) == "1" {
print("[aot/collections_hot] backprop recv=" + recv_vid + " => arr via method=" + mname)
}
}
}
}
}
}
}
}
}
i = oe + 1
}
if tmap.size() == before { break }
iter = iter + 1
}
}
// Static helpers replacing local fun
find_block_span(text, p) {
local key = "\"instructions\":["
@ -268,8 +352,11 @@ static box AotPrepCollectionsHotBox {
local oe = AotPrepHelpers._seek_object_end(text, os)
if oe < 0 || oe >= k { return 0 }
local inst = text.substring(os, oe+1)
// Quick checks
// Enhanced detection: toString, binop with StringBox const, direct string const
if inst.indexOf("\"method\":\"toString\"") >= 0 { return 1 }
if inst.indexOf("\"op\":\"const\"") >= 0 {
if inst.indexOf("\"box_type\":\"StringBox\"") >= 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)
@ -389,6 +476,8 @@ static box AotPrepCollectionsHotBox {
}
// Build lightweight SSA type table (arr/map) on out text for consistency with rewrite scan
local type_table = AotPrepCollectionsHotBox.build_type_table(out)
// Backpropagation: infer types from call sites (default enabled)
AotPrepCollectionsHotBox.tmap_backprop(out, type_table, copy_src)
{
// Optional trace
local tr = env.get("NYASH_AOT_CH_TRACE")