From 18bf35e6d46f7ed4fe86faf521090d0149ae08fd Mon Sep 17 00:00:00 2001 From: nyash-codex Date: Sun, 14 Dec 2025 09:19:00 +0900 Subject: [PATCH] =?UTF-8?q?fix(llvm):=20Phase=20131-10=20-=20Smart=20conso?= =?UTF-8?q?le.log=20routing=EF=BC=88Segfault=E4=BF=AE=E6=AD=A3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 問題 - Integer値をi8*ポインタに変換 → Segfault(Exit 139) ## 解決策 - String literal → nyash.console.log(i8*) - Integer/Handle → nyash.console.log_handle(i64) ## 結果 - Case B (loop_min_while): LLVM outputs `0,1,2` ✅ - VM/LLVM完全パリティ達成 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- CURRENT_TASK.md | 3 + crates/nyash_kernel/src/plugin/console.rs | 3 +- .../current/main/01-JoinIR-Selfhost-INDEX.md | 2 + docs/development/current/main/10-Now.md | 4 +- .../phase131-3-llvm-lowering-inventory.md | 145 ++++++------------ .../current/main/phase131-6-next-steps.md | 7 + .../phase131-6-ssa-dominance-diagnosis.md | 11 +- src/llvm_py/instructions/externcall.py | 26 +++- 8 files changed, 97 insertions(+), 104 deletions(-) diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index fb458764..8f70f5a6 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -17,6 +17,8 @@ - Pattern2 の promoted carriers(DigitPos/Trim)について ExitLine 契約(ConditionOnly を exit PHI から除外)を E2E で固定済み。 - debug flag SSOT / DebugOutputBox 移行 / error tags 集約 / carrier init builder まで整備済み。 - **LLVM exe line SSOT 確立**: `tools/build_llvm.sh` を使用した .hako → executable パイプライン標準化完了。 +- **LLVM AOT(Python llvmlite)ループ復旧**: `apps/tests/loop_min_while.hako` が EMIT/LINK/RUN まで到達(Phase 131-3..10)。 + - 残り: `loop(true)` + `break/continue` の JoinIR パターン穴(Case C) - **Phase 88 完了**: continue + 可変ステップ(i=i+const 差分)を dev-only fixture で固定、StepCalculator Box 抽出。 - **Phase 89 完了**: P0(ContinueReturn detector)+ P1(lowering 実装)完了。 - **Phase 90 完了**: ParseStringComposite + `Null` literal + ContinueReturn(同一値の複数 return-if)を dev-only fixture で固定。 @@ -32,6 +34,7 @@ - `docs/development/current/main/phase87-selfhost-llvm-exe-line.md` - `docs/development/current/main/phase131-2-box-resolution-map.md` - `docs/development/current/main/phase131-3-llvm-lowering-inventory.md` +- `docs/development/current/main/phase131-5-taglink-fix-summary.md` --- diff --git a/crates/nyash_kernel/src/plugin/console.rs b/crates/nyash_kernel/src/plugin/console.rs index f28344dc..1bdaa6b0 100644 --- a/crates/nyash_kernel/src/plugin/console.rs +++ b/crates/nyash_kernel/src/plugin/console.rs @@ -25,12 +25,11 @@ pub extern "C" fn print(ptr: *const i8) -> i64 { #[export_name = "nyash.console.log_handle"] pub extern "C" fn nyash_console_log_handle(handle: i64) -> i64 { use nyash_rust::runtime::host_handles as handles; - eprintln!("DEBUG: handle={}", handle); if let Some(obj) = handles::get(handle as u64) { let s = obj.to_string_box().value; println!("{}", s); } else { - eprintln!("DEBUG: handle {} not found in registry", handle); + // Fallback: handle is an unboxed integer (Phase 131-10) println!("{}", handle); } 0 diff --git a/docs/development/current/main/01-JoinIR-Selfhost-INDEX.md b/docs/development/current/main/01-JoinIR-Selfhost-INDEX.md index f4059806..8639cb4c 100644 --- a/docs/development/current/main/01-JoinIR-Selfhost-INDEX.md +++ b/docs/development/current/main/01-JoinIR-Selfhost-INDEX.md @@ -101,6 +101,8 @@ Phase 文書は歴史や検証ログも含むので、「JoinIR の現役設計 - → `docs/development/current/main/phase131-2-summary.md`(要点) - LLVM(Python llvmlite)lowering の不具合切り分けで迷っているとき - → `docs/development/current/main/phase131-3-llvm-lowering-inventory.md`(再現ケース表 + 根本原因候補) + - → `docs/development/current/main/phase131-5-taglink-fix-summary.md`(TAG-LINK: symbol 名の修正ログ) + - → `docs/development/current/main/phase131-6-ssa-dominance-diagnosis.md`(TAG-RUN の初期診断ログ・歴史) - → `docs/development/current/main/phase87-selfhost-llvm-exe-line.md`(実行パイプラインのSSOT) - 「この Phase 文書は現役か?」で迷ったとき - → まず `docs/development/current/main/10-Now.md` と diff --git a/docs/development/current/main/10-Now.md b/docs/development/current/main/10-Now.md index 054d8fac..bf0aa18e 100644 --- a/docs/development/current/main/10-Now.md +++ b/docs/development/current/main/10-Now.md @@ -73,9 +73,9 @@ の 3 層が `logging_policy.md` で整理済み。JoinIR/Loop trace も同ドキュメントに集約。 - VM backend の Box 解決(UnifiedBoxRegistry / BoxFactoryRegistry)の経路図: - `docs/development/current/main/phase131-2-box-resolution-map.md` -- LLVM(Python llvmlite)lowering の棚卸し(Phase 131-3): +- LLVM(Python llvmlite)lowering の棚卸し(Phase 131-3..10): - `docs/development/current/main/phase131-3-llvm-lowering-inventory.md` - - 現在の主要ブロッカー(要約): PHI の配置/順序で LLVM IR が invalid になるケース、JoinIR ループパターンの未対応ケース + - 状態: Case B(Pattern1/loop_min_while)は EMIT/LINK/RUN まで復旧済み。残りは Case C の JoinIR ループパターン穴。 --- diff --git a/docs/development/current/main/phase131-3-llvm-lowering-inventory.md b/docs/development/current/main/phase131-3-llvm-lowering-inventory.md index ff2126e8..73ad1688 100644 --- a/docs/development/current/main/phase131-3-llvm-lowering-inventory.md +++ b/docs/development/current/main/phase131-3-llvm-lowering-inventory.md @@ -8,7 +8,7 @@ | Case | File | Emit | Link | Run | Notes | |------|------|------|------|-----|-------| | A | `apps/tests/phase87_llvm_exe_min.hako` | ✅ | ✅ | ✅ | **PASS** - Simple return 42, no BoxCall, exit code verified | -| B | `apps/tests/loop_min_while.hako` | ✅ | ✅ | ❌ | **TAG-RUN** - EMIT/LINK fixed (Phase 131-5), **infinite loop in runtime** (PHI incoming value bug - Phase 131-6) | +| B | `apps/tests/loop_min_while.hako` | ✅ | ✅ | ✅ | **PASS** - Loop/PHI path runs end-to-end (Phase 131-10): prints `0,1,2` and exits | | B2 | `/tmp/case_b_simple.hako` | ✅ | ✅ | ✅ | **PASS** - Simple print(42) without loop works | | C | `apps/tests/llvm_stage3_loop_only.hako` | ❌ | - | - | **TAG-EMIT** - Complex loop (break/continue) fails JoinIR pattern matching | @@ -137,7 +137,7 @@ declare i64 @nyash.console.log(i8*) --- -### 3. TAG-RUN: Loop Infinite Iteration (Case B) - ❌ CONFIRMED PHI BUG (Phase 131-6) +### 3. TAG-RUN: Loop Runtime Incorrect (Case B) - ✅ FIXED (Phase 131-10) **File**: `apps/tests/loop_min_while.hako` @@ -150,51 +150,28 @@ $ ./target/release/hakorune apps/tests/loop_min_while.hako RC: 0 ``` -**Actual Behavior** (LLVM): -```bash -$ /tmp/loop_min_while -0 -0 -0 -... (infinite loop, prints 0 forever) -``` +**Outcome** (Phase 131-10): +- LLVM AOT now matches VM behavior (prints `0,1,2`, terminates) +- No regression in Case A / B2 -**Root Cause** (Phase 131-6 Diagnosis - 2025-12-14): +**What happened (timeline)**: +- **Phase 131-6**: Confirmed MIR/VM correctness; investigated as “PHI incoming wiring bug”. + - Historical diagnosis: `docs/development/current/main/phase131-6-ssa-dominance-diagnosis.md` + - Historical plan: `docs/development/current/main/phase131-6-next-steps.md` +- **Phase 131-7**: Multi-pass lowering follow-up: ensure values defined in Pass A are visible to Pass C (global `vmap` sync). +- **Phase 131-8**: ExternCall argument resolution fixed: use PHI-safe resolution (`resolve_i64_strict`) so PHI values are not treated as null pointers. +- **Phase 131-9**: MIR global PHI type inference: fix loop-carrier PHI mistakenly inferred as `String` (prevent `"0" + "1"` concatenation behavior). + - Implemented in `src/mir/builder/lifecycle.rs` (Phase 131-9 section). +- **Phase 131-10**: Console ABI routing: avoid calling `nyash.console.log(i8*)` with non-string values. + - Route string literals to `nyash.console.log(i8*)` + - Route handles/integers to `nyash.console.log_handle(i64)` + - Unboxed integer fallback handled in `crates/nyash_kernel/src/plugin/console.rs` -The MIR is **correct**. The MIR dump shows proper SSA form: - -```mir -bb4: - %3 = phi [%2, bb0], [%12, bb7] // ← Should receive %12 from loop body - -bb7: - extern_call env.console.log(%3) // ← Prints current value - %11 = const 1 - %12 = %3 Add %11 // ← Computes i+1 - br label bb4 // ← Loops back with %12 -``` - -**Verification**: -- ✅ VM execution: Correctly outputs `0, 1, 2` -- ❌ LLVM execution: Infinite loop outputting `0` -- ✅ MIR SSA dominance: No use-before-def violations -- ❌ LLVM PHI wiring: Incoming value from bb7 not properly connected - -**Bug Location**: `/home/tomoaki/git/hakorune-selfhost/src/llvm_py/llvm_builder.py` -- Function: `finalize_phis()` (lines 601-735+) -- Suspected issue: Self-carry logic (lines 679-681) or value resolution (line 683) causes %12 to be ignored - -**Detailed Analysis**: See [`docs/development/current/main/phase131-6-ssa-dominance-diagnosis.md`](/home/tomoaki/git/hakorune-selfhost/docs/development/current/main/phase131-6-ssa-dominance-diagnosis.md) - -**Next Steps**: -1. Inspect generated LLVM IR for store instructions after PHI -2. Check if PHI value is being used in subsequent stores -3. Verify loop increment instruction sequence - -**Files to investigate**: -- `src/llvm_py/instructions/store.py` - Store instruction lowering -- `src/llvm_py/phi_wiring/wiring.py` - PHI value propagation -- `target/aot_objects/loop_min_while.ll` - Generated LLVM IR (if saved) +**Key lesson**: +- “PHI is correct but loop prints 0 forever” can be a combination of: + - multi-pass value propagation bugs, + - ExternCall resolution/ABI mismatch, + - and MIR type inference drift. --- @@ -278,46 +255,20 @@ Hint: This loop pattern is not supported. All loops must use JoinIR lowering. --- -### 🔥 Priority 3: Fix TAG-RUN (Loop Infinite Iteration) - IN PROGRESS (Phase 131-6) +### ✅ Priority 3: COMPLETED - Fix TAG-RUN (Loop Runtime Correctness) **Target**: Case B (`loop_min_while.hako`) -**Issue**: Loop counter not updating, causes infinite loop printing `0` +**Status**: ✅ FIXED (Phase 131-10) -**Phase 131-6 Investigation Results**: +**Fixes applied (high level)**: +1. Multi-pass lowering value propagation: Pass A outputs are synced for Pass C usage +2. ExternCall value resolution: unify on PHI-safe resolver (`resolve_i64_strict`) +3. MIR global PHI type inference: correct loop-carrier PHI types from incomings +4. Console ABI routing: `console.log(i8*)` vs `console.log_handle(i64)` dispatch -#### Bug #1: MIR Copy-to-PHI (FIXED) -- **Location**: `src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs` lines 419-440 -- **Problem**: Parameter binding was generating `Copy { dst: PHI_dst, src: value }` in loop latch -- **Fix**: Added check to skip Copy when `dst` is a header PHI destination -- **Status**: ✅ Copy instruction removed from block 7 - -#### Bug #2: Type Inference - String vs Integer (PARTIAL FIX) -- **Location**: `src/llvm_py/instructions/binop.py` lines 128-153 -- **Problem**: MIR marks `i + 1` as `dst_type: StringBox` (forward-looking hint from `print(i)` usage) -- **Impact**: Python harness was doing string concatenation instead of integer addition -- **Fix Attempted**: Removed `force_string` logic that trusted `dst_type` hint -- **Status**: ⚠️ Partially fixed but infinite loop persists - -#### Bug #3: Instruction Ordering Violation (DISCOVERED) -- **Location**: MIR builder (instruction scheduling) -- **Problem**: Copy instructions emitted AFTER values are used (violates SSA dominance) -- **Example**: `%6 = %4 + %5` appears before `%5 = copy %3` -- **Impact**: LLVM requires strict SSA form, Rust VM tolerates it -- **Status**: ❌ Not yet addressed - -#### Additional Findings -- Simple test `/tmp/test_simple_add.hako` (`i=0; i=i+1; print(i)`) also fails (prints 0 not 1) -- Issue exists even without loops, suggesting fundamental binop/type problem -- String tagging propagation may be marking integer PHI values as strings - -**Next Steps**: -1. Trace `resolver.is_stringish()` to see if integers are being marked as strings -2. Fix MIR instruction scheduling to respect SSA dominance -3. Consider runtime type checking instead of compile-time inference - -**Files Modified**: -- `src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs` (Phase 131-6 fix) -- `src/llvm_py/instructions/binop.py` (Phase 131-6 partial fix) +**Result**: +- Case B now passes EMIT ✅ LINK ✅ RUN ✅ +- Output matches VM behavior (`0,1,2`), and the program terminates --- @@ -458,7 +409,7 @@ NYASH_LLVM_OBJ_OUT="$OBJ" NYASH_LLVM_USE_HARNESS=1 \ --- -## Timeline Estimate +## Timeline Estimate (historical) - **P1 (Loop PHI → LLVM IR fix)**: 1-2 hours (harness BB emission logic) - **P2 (JoinIR pattern coverage)**: 3-4 hours (pattern design + implementation) @@ -468,19 +419,18 @@ NYASH_LLVM_OBJ_OUT="$OBJ" NYASH_LLVM_USE_HARNESS=1 \ --- -## Executive Summary +## Executive Summary (updated) -### Phase 131-5 Results (TAG-LINK Fix Complete!) +### Phase 131-10 Results (Case B End-to-End PASS) **✅ Case A (Minimal)**: PASS - Simple return works perfectly - EMIT ✅ LINK ✅ RUN ✅ - Validates: Build pipeline, NyKernel runtime, basic MIR→LLVM lowering -**⚠️ Case B (Loop+PHI)**: EMIT ✅ LINK ✅ RUN ❌ +**✅ Case B (Loop+PHI)**: PASS - Loop/PHI works in LLVM AOT - **Phase 131-4**: Fixed TAG-EMIT (PHI after terminator) ✅ - **Phase 131-5**: Fixed TAG-LINK (symbol name mismatch) ✅ -- **NEW ISSUE**: TAG-RUN (infinite loop - counter not updating) ❌ -- **Progress**: 2/3 milestones achieved, runtime bug discovered +- **Phase 131-10**: Fixed TAG-RUN (value propagation + console ABI routing) ✅ **✅ Case B2 (BoxCall)**: PASS - print() without loops works - EMIT ✅ LINK ✅ RUN ✅ @@ -492,7 +442,7 @@ NYASH_LLVM_OBJ_OUT="$OBJ" NYASH_LLVM_USE_HARNESS=1 \ --- -### Phase 131-5 Achievements +### Phase 131-5 Achievements (still valid) **✅ Fixed TAG-LINK (Symbol Name Mismatch)**: 1. **Investigation**: Used `objdump` to discover NyKernel exports symbols with dots @@ -512,8 +462,8 @@ NYASH_LLVM_OBJ_OUT="$OBJ" NYASH_LLVM_USE_HARNESS=1 \ 1. ✅ **Fix PHI ordering** (P1 - Phase 131-4) - DONE 2. ✅ **Fix symbol mapping** (P2 - Phase 131-5) - DONE -3. 🔥 **Fix loop runtime bug** (P3 - NEW) - IN PROGRESS -4. ⏳ **Add JoinIR Pattern 5** (P4) - PENDING +3. ✅ **Fix loop runtime correctness** (P3 - Phase 131-10) - DONE +4. ⏳ **Add JoinIR infinite-loop early-exit pattern** (P4) - PENDING 5. ⏳ **Comprehensive test** (P5) - PENDING **Total Effort So Far**: ~3 hours (Investigation + 2 fixes) @@ -534,7 +484,7 @@ NYASH_LLVM_OBJ_OUT="$OBJ" NYASH_LLVM_USE_HARNESS=1 \ - **Recommendation**: Box-ify with interface contract (MIR JSON v1 schema) #### 🔧 Technical Debt Found -1. **PHI emission ordering** - Architectural issue, not a quick fix +1. **Harness duplication** - Python harness vs Rust crate divergence risk 2. **Unreachable block handling** - MIR JSON marks all blocks `reachable: false` (may be stale metadata) 3. **Error logging** - Python harness errors lost after build_llvm.sh exits @@ -549,10 +499,11 @@ tmp/case_a echo $? # Expected: 42 ``` -### Case B (Loop PHI - FAIL at EMIT) +### Case B (Loop PHI - PASS) ```bash tools/build_llvm.sh apps/tests/loop_min_while.hako -o tmp/case_b -# Error: empty bb4 in LLVM IR +tmp/case_b +# Output: 0,1,2 (+ Result line), then exit ``` ### Case B2 (Simple BoxCall - PASS) @@ -567,7 +518,7 @@ static box Main { EOF tools/build_llvm.sh /tmp/case_b_simple.hako -o tmp/case_b2 tmp/case_b2 -# Output: (empty, but executes without crash) +# Output: prints `42` (+ Result line), then exit ``` ### Case C (Complex Loop - FAIL at MIR) @@ -594,12 +545,12 @@ jq '.cfg.functions[] | select(.name=="main") | .blocks[] | select(.id==4)' /tmp/ ## Success Criteria -**Phase 131-5 Complete** when: +**Phase 131-10 Complete** when: 1. ✅ Case A continues to pass (regression prevention) - **VERIFIED** -2. ⚠️ Case B (loop_min_while.hako) compiles to valid LLVM IR and links - **PARTIAL** (EMIT ✅ LINK ✅ RUN ❌) +2. ✅ Case B (loop_min_while.hako) passes end-to-end - **VERIFIED** (EMIT ✅ LINK ✅ RUN ✅) 3. ✅ Case B2 continues to pass (BoxCall regression prevention) - **VERIFIED** 4. ❌ Case C (llvm_stage3_loop_only.hako) lowers to JoinIR and runs - **NOT YET** -5. ⚠️ All 4 cases produce correct output - **PARTIAL** (2/4 passing) +5. ⚠️ All 4 cases produce correct output - **PARTIAL** (3/4 passing) 6. ⚠️ No plugin errors (or plugin errors are benign/documented) - **ACCEPTABLE** (plugin errors don't affect AOT execution) **Definition of Done**: diff --git a/docs/development/current/main/phase131-6-next-steps.md b/docs/development/current/main/phase131-6-next-steps.md index 7746e98f..4344072b 100644 --- a/docs/development/current/main/phase131-6-next-steps.md +++ b/docs/development/current/main/phase131-6-next-steps.md @@ -1,5 +1,12 @@ # Phase 131-6: Next Steps - PHI Bug Fix +## Update (Phase 131-10) + +Phase 131-6 時点では PHI wiring を主因として疑っていたが、実際には multi-pass lowering の値伝播・ExternCall の引数解決/ABI ルーティング・PHI 型推論など複数の要因が重なっていた。 + +最終的に Case B は解決済み(LLVM AOT で `0,1,2` を出して終了)。到達点と修正の全体像は次を SSOT とする: +- `docs/development/current/main/phase131-3-llvm-lowering-inventory.md` + ## Summary **Problem**: LLVM backend generates infinite loop for `loop_min_while.hako` diff --git a/docs/development/current/main/phase131-6-ssa-dominance-diagnosis.md b/docs/development/current/main/phase131-6-ssa-dominance-diagnosis.md index 62dbd935..ee142dcb 100644 --- a/docs/development/current/main/phase131-6-ssa-dominance-diagnosis.md +++ b/docs/development/current/main/phase131-6-ssa-dominance-diagnosis.md @@ -1,10 +1,17 @@ # Phase 131-6: MIR SSA Dominance Diagnosis +## Update (Phase 131-10) + +Case B (`apps/tests/loop_min_while.hako`) は **LLVM AOT でも `0,1,2` を出して終了**するところまで復旧済み。 + +この文書は Phase 131-6 時点の「切り分けログ(歴史)」として残し、最終的な到達点と修正の全体像は次を SSOT とする: +- `docs/development/current/main/phase131-3-llvm-lowering-inventory.md` + ## Executive Summary -**Status**: ❌ LLVM Backend Bug Confirmed +**Status (Phase 131-6 時点)**: ❌ LLVM Backend Bug Confirmed **Severity**: P0 - Breaks basic loop functionality -**Root Cause**: PHI node incoming values not properly wired in LLVM IR generation +**Root Cause (Phase 131-6 時点の仮説)**: PHI node incoming values not properly wired in LLVM IR generation ## Evidence Chain diff --git a/src/llvm_py/instructions/externcall.py b/src/llvm_py/instructions/externcall.py index 168c1180..3e72938b 100644 --- a/src/llvm_py/instructions/externcall.py +++ b/src/llvm_py/instructions/externcall.py @@ -80,10 +80,30 @@ def lower_externcall( # Many call sites pass handles or pointers; we coerce below. } + # Phase 131-10: Smart console.log routing + # If console.log is called with a non-string-literal argument, use console.log_handle instead + actual_c_symbol = c_symbol_name + use_handle_variant = False + + if llvm_name in ("nyash.console.log", "nyash.console.warn", "nyash.console.error"): + # Check if first argument is a string literal + if len(args) > 0: + first_arg = args[0] + is_string_literal = False + if resolver is not None and hasattr(resolver, 'string_ptrs'): + is_string_literal = first_arg in resolver.string_ptrs + + # If NOT a string literal, use the _handle variant + if not is_string_literal: + use_handle_variant = True + # Extract method name (log, warn, error) + method = llvm_name.split('.')[-1] + actual_c_symbol = f"nyash.console.{method}_handle" + # Find or declare function with appropriate prototype func = None for f in module.functions: - if f.name == c_symbol_name: + if f.name == actual_c_symbol: func = f break if not func: @@ -91,6 +111,10 @@ def lower_externcall( ret_ty, arg_tys = sig_map[llvm_name] fnty = ir.FunctionType(ret_ty, arg_tys) func = ir.Function(module, fnty, name=c_symbol_name) + elif use_handle_variant: + # console.*_handle: (i64) -> i64 + fnty = ir.FunctionType(i64, [i64]) + func = ir.Function(module, fnty, name=actual_c_symbol) elif llvm_name.startswith("nyash.console."): # console.*: (i8*) -> i64 fnty = ir.FunctionType(i64, [i8p])