- Add P3 section to phases/phase-100/README.md - Document allowed form: out = out + ch (Variable RHS only) - List Fail-Fast cases (Literal RHS, MethodCall RHS, non-string-ish) - Add example with length-based output 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Phase 100: Pinned Read‑Only Captures(設計メモ)
目的
JoinIR lowering のスコープ解決(ConditionEnv → LoopBodyLocalEnv → CapturedEnv)の前提を崩さずに、
「ループ外で定義した local(動的式でも可)」をループ内で receiver として参照できるようにする。
例(問題の形):
local s = "a" + "b" // 条件式とは無関係
loop(i < n) {
local ch = s.substring(i, i + 1) // receiver 解決が必要
}
方針(SSOT)
- “Pinned local” は 新しい Pattern ではなく、ループに入ってくる read‑only 入力として扱う。
- SSOT は増やさず
CapturedEnvに統合し、CapturedKindで区別する:Explicit: 従来の capturePinned: ループ内参照のために必要な read‑only local
Fail‑Fast 契約
Pinnedは loop body 内で 再代入されない(assignment target に現れたら拒否)。- loop entry 時点で host 側 ValueId が存在する(
variable_mapに無い場合は拒否)。 - 初期化が loop より後にある等、支配関係が曖昧な場合は拒否(理由付き)。
- init 式の副作用有無は問わない(評価はホスト側で1回、Pinned は値を渡すだけ)。
Loop Canonicalizer との関係(混ぜない)
- Phase 100 は「ループ外の値が loop 内で見えない」問題(capture/wiring)を解く。
- Loop Canonicalizer は「ループ内の不変式を外に出す」AST 変換であり、別ユースケース。
予定(段階投入)
この Phase は「自由度を上げるほどバグが出やすい領域」なので、段階投入で進める。
- P1(read‑only): ループ外 local を
PinnedとしてCapturedEnvに取り込み、receiver 解決を通す。- Fail‑Fast: loop body 内で再代入される変数は pinned 禁止(理由付き)
- P2(mutable): loop body 内で再代入される外側変数は
Pinnedでは扱わず、LoopState(carrier/env)に昇格して運ぶ。- 初期は “更新形を限定” して shape guard を作り、未対応は Fail‑Fast(曖昧に通さない)
- P3(hoist): ループ内の不変式の巻き上げは Loop Canonicalizer 側で扱う(別ユースケースとして分離)
P1: Pinned Local Captures (Explicit/Pinned) - 完了 ✅
Implemented CapturedKind enum to distinguish:
- Explicit: Traditional captures (condition variables, carriers)
- Pinned: Phase 100 read-only loop-outer locals used as method receivers
Status: P1-1 through P1-5 complete - pinned receiver functionality validated with fixture+smoke tests.
Example (now works):
local s = "a" + "b" + "c" # Dynamic construction (loop-outer)
loop(i < 1) {
local ch = s.substring(i, i+1) # Pinned receiver works!
print(i)
i = i + 1
}
Smoke test: phase100_pinned_local_receiver_vm.sh
Implementation Details
Search Order (SSOT):
- ConditionEnv (loop-outer scope)
- LoopBodyLocalEnv (body-local variables - Phase 226 cascading)
- CapturedEnv (pinned loop-outer locals)
- CarrierInfo (mutable carriers)
Key Components:
CapturedKindenum: Distinguishes Explicit vs Pinned capturesPinnedLocalAnalyzer: Identifies loop-outer read-only localsloop_body_local_init.rs: Updated receiver resolution with full search order
Test Coverage
- Fixture:
apps/tests/phase100_pinned_local_receiver_min.hako - Smoke Test:
tools/smokes/v2/profiles/integration/apps/phase100_pinned_local_receiver_vm.sh - Regression: Phase 96 and Phase 94 smoke tests pass
P2: Mutable Accumulators (s = s + x form only)
Constraint: Mutable variables are LIMITED to accumulator pattern.
Allowed form:
s = s + xwhere x ∈ {Const, BodyLocal, Captured, Pinned, Carrier}
Fail-Fast (未対応):
- Any other mutation:
s = f(...)/s = x + s/s += x/ multiple updates / conditional updates - Reason: Shape explosion prevention, safe accumulator support (JSON/CSV building)
Example (now works):
local out = ""
loop(i < 2) {
local ch = "a"
out = out + ch # Accumulator form → LoopState carrier
i = i + 1
}
print(out.length()) # Output: 2
Implementation:
- P2-1: MutableAccumulatorAnalyzer (AST shape detection only)
- P2-2: Pattern2 wiring (ScopeManager delegates read-only check)
- P2-3: Lowering (carrier update emission)
- P2-4: Integration test (fixture + smoke, length-based validation)
LLVM EXE parity: P2 accumulator を LLVM EXE smoke でも固定(phase100_mutable_accumulator_llvm_exe.sh)
P3: String Accumulator (out = out + ch, Variable RHS only)
Constraint: String accumulator は最小形のみサポート。
Allowed form:
out = out + chwhere ch ∈ {Variable (string-ish)}- RHS must be Variable (not Literal, not MethodCall)
Fail-Fast (未対応):
- Literal RHS:
out = out + "x"(P3.1 で解禁予定) - MethodCall RHS:
out = out + substring(...)(P3.2 で解禁予定) - Non-string-ish RHS: 型判定は既存の box facts に委譲
Example (now works):
local out = ""
local s = "abc"
loop(i < 3) {
local ch = s.substring(i, i+1) # body-local string 1文字
out = out + ch # String accumulator
print(out.length()) # Output: 1, 2, 3
i = i + 1
}
Implementation:
- P3-1: AccumulatorKind::{Int, String} 追加(型判定は委譲)
- P3-2: StringAccumulatorEmitter 専用箱(JoinIR lowering)
- P3-3: Pattern2 wiring(string carrier 昇格、emitter 分岐)
- P3-4: Fixture + smokes(VM + LLVM EXE)