From af99ccab91c5275ee091e63d9ce6b8234b406911 Mon Sep 17 00:00:00 2001 From: tomoaki Date: Wed, 24 Dec 2025 09:58:00 +0900 Subject: [PATCH] feat(llvm): Phase 285LLVM-0 - LLVM Conformance One-Pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 0: SSOT/Reality Alignment - Update Feature Matrix (lifecycle.md): - WeakRef: "285A1" → "285LLVM-1" - Leak Report: "partial" → "Parent process roots only (285LLVM-0)" - Add detailed LLVM limitation explanation - Update SKIP reason (phase285_weak_basic_llvm.sh): "285A1" → "285LLVM-1" Step 1: LLVM Leak Report Implementation - Add emit_leak_report() calls to llvm.rs (success + error paths) - Create phase285_leak_report_llvm.sh smoke test (3 test cases PASS) - Add NYASH_LEAK_LOG documentation to environment-variables.md - Manual test: All 3 cases PASS (no-log, LEVEL=1, LEVEL=2) - Smoke test: 3/3 PASS Step 2: WeakRef Design Preparation - Create phase-285llvm-1-design.md - Runtime representation candidate (Option B with caveat) - FFI signatures definition - Implementation checklist - Test strategy - NO CODE IMPLEMENTATION (design-only phase) Implementation: - Minimal 10-20 line code change (as planned) - Reuses existing leak_tracker.rs infrastructure - LLVM limitation transparently documented - Exit codes unchanged (0/1 preserved) - Fixture SSOT: apps/tests/*.hako shared between VM/LLVM Test Results: - ✅ Manual test: 3/3 cases PASS - ✅ Smoke test: 3/3 cases PASS - ✅ No regressions Files Changed: - src/runner/modes/llvm.rs (2 emit_leak_report() calls) - docs/reference/language/lifecycle.md (Feature Matrix + LLVM limitation) - docs/reference/environment-variables.md (NYASH_LEAK_LOG entry) - tools/smokes/v2/profiles/quick/lifecycle/phase285_weak_basic_llvm.sh (SKIP reason) Files Added: - tools/smokes/v2/profiles/quick/lifecycle/phase285_leak_report_llvm.sh - docs/development/current/main/phases/phase-285/phase-285llvm-1-design.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .../phase-285/phase-285llvm-1-design.md | 210 ++++++++++++++++++ docs/reference/environment-variables.md | 23 ++ docs/reference/language/lifecycle.md | 12 +- src/runner/modes/llvm.rs | 9 + .../lifecycle/phase285_leak_report_llvm.sh | 54 +++++ .../lifecycle/phase285_weak_basic_llvm.sh | 4 +- 6 files changed, 307 insertions(+), 5 deletions(-) create mode 100644 docs/development/current/main/phases/phase-285/phase-285llvm-1-design.md create mode 100644 tools/smokes/v2/profiles/quick/lifecycle/phase285_leak_report_llvm.sh diff --git a/docs/development/current/main/phases/phase-285/phase-285llvm-1-design.md b/docs/development/current/main/phases/phase-285/phase-285llvm-1-design.md new file mode 100644 index 00000000..326c785a --- /dev/null +++ b/docs/development/current/main/phases/phase-285/phase-285llvm-1-design.md @@ -0,0 +1,210 @@ +# Phase 285LLVM-1: WeakRef LLVM Implementation Design + +**Status**: Design phase (no implementation yet) +**Date**: 2025-12-24 +**Related**: Phase 285A0 (VM WeakRef implementation), Phase 285LLVM-0 (LLVM Leak Report) + +## Goal + +LLVM backendでWeakRefサポートを実装し、VM parityを達成する + +## Background + +### 現状 +- **VM**: WeakRef完全実装済み(Phase 285A0) + - `VMValue::WeakBox(Weak)` 実装 + - 8テスト全てPASS + - `apps/tests/phase285_weak_*.hako` にfixtureあり +- **LLVM**: 完全未実装 + - `src/llvm_py/instruction_lower.py` に WeakNew/WeakLoad handler存在せず + - fallback処理もなし(未知命令は無視される) + +### 制約 +- **WeakRef field契約**: フロントエンド(parser/MIR builder)の責務 + - 全バックエンドで既に効いている + - LLVM側で実装不要 +- **GC統合**: WeakRefはGC不要で動作(デザイン済み) + +## Runtime表現(候補) + +### 候補: Option B (i64 with convention, bit 63 = weak marker) + +**理由**: +- 最小FFI overhead(既存のhandle infrastructureを再利用) +- 実装が単純 +- 後でtagged unionに変更可能 + +**注意**: +⚠️ **Phase 285LLVM-1実装前に既存LLVM値表現との整合性確認が必要** +- 既存のhandle/boxing方式と衝突しないか検証 +- i64の最上位ビット(bit 63)が他の目的で使われていないか確認 + +### 代替案 + +**Option A**: Tagged Pointer `{i32 tag, i64 handle}` +- ✅ 型安全 +- ❌ 実装が複雑 +- ❌ FFI overhead増加 + +**Option C**: Opaque Pointer +- ✅ Future-proof +- ❌ Runtime support必要 +- ❌ より大きな変更が必要 + +## weak_to_strong() 返り値 + +**SSOT** (`docs/reference/language/lifecycle.md:179`): +- 成功: Strong BoxRef (i64 handle > 0) +- 失敗: null (i64(0) = Void) + +## FFI API + +Phase 285LLVM-1で実装する3つのFFI関数(`crates/nyash_kernel/src/lib.rs` または相当箇所): + +```rust +#[no_mangle] +pub extern "C" fn nyrt_weak_new(strong_handle: i64) -> i64 { + // Convert strong handle to weak handle + // Implementation: Phase 285LLVM-1 + unimplemented!("Phase 285LLVM-1") +} + +#[no_mangle] +pub extern "C" fn nyrt_weak_to_strong(weak_handle: i64) -> i64 { + // Upgrade weak to strong + // Returns: strong handle (>0) on success, 0 (Void) on failure + unimplemented!("Phase 285LLVM-1") +} + +#[no_mangle] +pub extern "C" fn nyrt_weak_drop(weak_handle: i64) { + // Release weak reference + unimplemented!("Phase 285LLVM-1") +} +``` + +## MIR Lowering + +LLVM IR生成時の変換(`src/llvm_py/instruction_lower.py` で実装): + +```python +# WeakNew: strong → weak 変換 +# MIR: WeakNew(dst=ValueId(10), box_val=ValueId(5)) +# LLVM IR: %10 = call i64 @nyrt_weak_new(i64 %5) + +elif op == "weak_new": + box_val = lower_value(inst["box_val"]) + result = builder.call(nyrt_weak_new_fn, [box_val]) + store_value(inst["dst"], result) + +# WeakLoad: weak → strong 変換(失敗時は0) +# MIR: WeakLoad(dst=ValueId(20), weak_ref=ValueId(10)) +# LLVM IR: %20 = call i64 @nyrt_weak_to_strong(i64 %10) + +elif op == "weak_load": + weak_ref = lower_value(inst["weak_ref"]) + result = builder.call(nyrt_weak_to_strong_fn, [weak_ref]) + store_value(inst["dst"], result) +``` + +## テスト戦略 + +### Fixture再利用 +VM fixtureを再利用(`apps/tests/phase285_weak_*.hako`): +- `phase285_weak_basic.hako` - 基本動作 +- `phase285_weak_upgrade.hako` - weak_to_strong()成功/失敗 +- その他6ファイル(合計8 fixtures) + +### Parity検証 +VM/LLVM出力parity検証: +- stdout比較(同じ出力であることを確認) +- exit code比較(同じ終了コードであることを確認) + +### スモークテスト +`tools/smokes/v2/profiles/quick/lifecycle/phase285_weak_basic_llvm.sh`: +- 現在SKIP中(Phase 285LLVM-1 message) +- 実装後にSKIP解除 + +## 実装チェックリスト + +### ファイル変更/新規作成 + +- [ ] **新規**: `src/llvm_py/instructions/weak.py` + - WeakNew/WeakLoad handler実装 + - `instruction_lower.py` から分離されたモジュール + +- [ ] **変更**: `src/llvm_py/instruction_lower.py` + - `elif op == "weak_new"` handler追加 + - `elif op == "weak_load"` handler追加 + - `weak.py` をimport + +- [ ] **変更**: `crates/nyash_kernel/src/lib.rs` (または相当箇所) + - `nyrt_weak_new()` 実装 + - `nyrt_weak_to_strong()` 実装 + - `nyrt_weak_drop()` 実装 + +- [ ] **変更**: `tools/smokes/v2/profiles/quick/lifecycle/phase285_weak_basic_llvm.sh` + - `test_skip` 削除 + - 実際のテスト実装(VM版と同じパターン) + +### 検証ステップ + +1. [ ] **Runtime表現の整合性確認** + - 既存LLVM値表現(handle/boxing方式)と衝突しないか検証 + - bit 63が他で使われていないか確認 + +2. [ ] **FFI関数単体テスト** + - `nyrt_weak_new()` が正しいweak handleを返すか + - `nyrt_weak_to_strong()` が成功時にstrong handle、失敗時に0を返すか + - `nyrt_weak_drop()` が正しくリソースを解放するか + +3. [ ] **MIR lowering検証** + - WeakNew命令が正しくLLVM IRに変換されるか + - WeakLoad命令が正しくLLVM IRに変換されるか + - 生成されたLLVM IRが実行可能か + +4. [ ] **VM/LLVM parity検証** + - 8 fixtureでVM/LLVMの出力が一致するか + - exit codeが一致するか + +5. [ ] **スモークテスト** + - `phase285_weak_basic_llvm.sh` PASS確認 + - 回帰テスト(既知FAIL以外増えないか) + +## 前提条件 + +- ✅ VM WeakRef完全実装済み(Phase 285A1) +- ⚠️ Runtime表現候補選定済み(Option B、実装前に既存LLVM値表現との整合性確認必要) +- ✅ FFI signatures定義済み(本ドキュメント) +- ✅ Phase 285LLVM-0完了(LLVM Leak Report実装済み) + +## 実装時の注意点 + +1. **Fail-Fast原則**: エラーは早期に明示的に失敗させる + - weak_to_strong()失敗時はnull (i64(0))を返す + - フォールバック処理は入れない + +2. **SSOT準拠**: `docs/reference/language/lifecycle.md` の仕様に厳密に従う + - weak_to_strong()の返り値は仕様通り + - WeakRef semanticsは変更しない + +3. **テスト駆動**: VM fixtureで動作確認してから進める + - 実装→テスト→修正のサイクルを回す + - VM parityを常に確認 + +## 成功基準 + +**Phase 285LLVM-1 完了時**: +- ✅ WeakNew/WeakLoad命令のLLVM IR生成が動作 +- ✅ FFI関数3つ(nyrt_weak_new, nyrt_weak_to_strong, nyrt_weak_drop)実装済み +- ✅ 8 fixtureでVM/LLVM parity達成(stdout + exit code一致) +- ✅ `phase285_weak_basic_llvm.sh` スモークテストPASS +- ✅ 回帰なし(既知FAIL以外増えない) + +## 参考資料 + +- VM実装: `src/runtime/execution/vm/value.rs` (`VMValue::WeakBox`) +- WeakRef SSOT: `docs/reference/language/lifecycle.md:170-180` +- Fixtures: `apps/tests/phase285_weak_*.hako` +- Phase 285A0: VM WeakRef実装(完了) +- Phase 285LLVM-0: LLVM Leak Report実装(完了) diff --git a/docs/reference/environment-variables.md b/docs/reference/environment-variables.md index d8aafd05..df1ebbde 100644 --- a/docs/reference/environment-variables.md +++ b/docs/reference/environment-variables.md @@ -18,12 +18,35 @@ Nyash の主要な環境変数をカテゴリ別に整理するよ。`適用経 | `NYASH_VM_DUMP_MIR=1` | OFF | Any | VM 実行前の MIR を出力 | | `NYASH_DUMP_JSON_IR=1` | OFF | Any | JSON IR をダンプ | | `NYASH_DEBUG_STACK_OVERFLOW=1` | OFF | Any | スタックオーバーフロー時に backtrace を有効化 | +| `NYASH_LEAK_LOG=1` | OFF | VM (full), LLVM (parent process roots only) | プログラム終了時に残存する強参照を報告(サマリー) | +| `NYASH_LEAK_LOG=2` | OFF | VM (full), LLVM (parent process roots only) | プログラム終了時に残存する強参照を報告(詳細、最初の10件まで) | ### ダンプの使い分け - 実行経路SSOT(推奨): `NYASH_VM_DUMP_MIR=1 ./target/release/hakorune --backend vm apps/tests/minimal.hako` - Rust AST 直通(compile-only): `./target/release/hakorune --dump-mir apps/tests/minimal.hako`(env は不要、stdout のみ) - JSON v0 経路/Stage-1: `RUST_MIR_DUMP_PATH=/tmp/out.mir NYASH_USE_STAGE1_CLI=1 STAGE1_EMIT_MIR_JSON=1 ./target/release/hakorune --dump-mir`(stdout + ファイル) +### NYASH_LEAK_LOG(Phase 285LLVM-0) + +**Backend Support**: VM (full), LLVM (parent process roots only) + +プログラム終了時に残存する強参照を報告する診断機能(デフォルトOFF)。 + +**値**: +- `1`: サマリーのみ(modules, host_handles, plugin_boxes の数) +- `2`: 詳細(名前/エントリを含む、最初の10件まで) + +**例**: +```bash +# VM: 完全なリークレポート +NYASH_LEAK_LOG=2 ./target/release/hakorune program.hako + +# LLVM: 親プロセスのroot snapshotのみ +NYASH_LEAK_LOG=2 NYASH_LLVM_USE_HARNESS=1 ./target/release/hakorune --backend llvm program.hako +``` + +**LLVM制限**: LLVM harness runnerは親プロセス(Rust VM側)のroot snapshotのみ報告。子プロセス(native executable)内部の到達可能性はプロセス境界の制約により見えない。 + --- ## Stage-1 / selfhost CLI diff --git a/docs/reference/language/lifecycle.md b/docs/reference/language/lifecycle.md index 418386da..67288a44 100644 --- a/docs/reference/language/lifecycle.md +++ b/docs/reference/language/lifecycle.md @@ -301,13 +301,19 @@ This section documents current backend reality so we can detect drift as bugs. | Feature | VM | LLVM | WASM | |---------|-----|------|------| -| WeakRef (`weak(x)`, `weak_to_strong()`) | ✅ | ❌ unsupported (285A1) | ❌ unsupported | -| Leak Report (`NYASH_LEAK_LOG`) | ✅ | ⚠️ partial (not yet) | ❌ | +| WeakRef (`weak(x)`, `weak_to_strong()`) | ✅ | ❌ unsupported (285LLVM-1) | ❌ unsupported | +| Leak Report (`NYASH_LEAK_LOG`) | ✅ | ⚠️ Parent process roots only (285LLVM-0) | ❌ | + +**LLVM Leak Report の制限** (Phase 285LLVM-0): +- LLVM harness runnerで親プロセス(Rust VM側)のroot snapshotを報告 +- 報告内容: modules, host_handles, plugin_boxes +- 子プロセス(native executable)内部の到達可能性は見えない(プロセス境界の制約) +- これは設計上の制約であり、バグではない ### Notes - **Block-scoped locals** are the language model (`local` drops at `}`), but the *observable* effects depend on where the last strong reference is held. -- **WeakRef** (Phase 285A0): VM backend fully supports `weak(x)` and `weak_to_strong()`. LLVM harness support is planned for Phase 285A1. +- **WeakRef** (Phase 285A0): VM backend fully supports `weak(x)` and `weak_to_strong()`. LLVM harness support is planned for Phase 285LLVM-1. - **WASM backend** currently treats MIR `WeakNew/WeakLoad` as plain copies (weak behaves like strong). This does not satisfy the SSOT weak semantics yet (see also: `docs/guides/wasm-guide/planning/unsupported_features.md`). - **Leak Report** (Phase 285): `NYASH_LEAK_LOG={1|2}` prints exit-time diagnostics showing global roots still held (modules, host_handles, plugin_boxes). See `docs/reference/environment-variables.md`. - Conformance gaps (any backend differences from this document) must be treated as bugs and tracked explicitly; do not "paper over" differences by changing this SSOT without a decision. diff --git a/src/runner/modes/llvm.rs b/src/runner/modes/llvm.rs index ab0d3a77..791d1000 100644 --- a/src/runner/modes/llvm.rs +++ b/src/runner/modes/llvm.rs @@ -352,10 +352,19 @@ impl NyashRunner { "✅ LLVM (harness) execution completed (exit={})", code ); + + // Phase 285LLVM-0: Emit Rust-side leak report before exit (if enabled) + // Note: Only reports Rust VM-side roots (modules, host_handles, plugin_boxes). + crate::runtime::leak_tracker::emit_leak_report(); + std::process::exit(code); } Err(e) => { crate::console_println!("❌ run executable error: {}", e); + + // Phase 285LLVM-0: Emit leak report even on error (for consistency) + crate::runtime::leak_tracker::emit_leak_report(); + std::process::exit(1); } } diff --git a/tools/smokes/v2/profiles/quick/lifecycle/phase285_leak_report_llvm.sh b/tools/smokes/v2/profiles/quick/lifecycle/phase285_leak_report_llvm.sh new file mode 100644 index 00000000..8c0bc664 --- /dev/null +++ b/tools/smokes/v2/profiles/quick/lifecycle/phase285_leak_report_llvm.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# phase285_leak_report_llvm.sh - Phase 285LLVM-0: LLVM Leak Report smoke test +# +# Verifies NYASH_LEAK_LOG={1,2} produces [lifecycle/leak] output at exit (LLVM backend). +# Note: LLVM reports Rust-side roots only (modules, host_handles, plugin_boxes). +# +# SSOT: apps/tests/phase285_leak_report.hako (shared with VM) + +source "$(dirname "$0")/../../../lib/test_runner.sh" +require_env || exit 2 + +FIXTURE="$NYASH_ROOT/apps/tests/phase285_leak_report.hako" + +# LLVM availability check: Run test and skip if LLVM harness unavailable +# (Following test_runner.sh conventions - run and check for specific error) + +# Test 1: Without NYASH_LEAK_LOG - no leak output +output_no_log=$(NYASH_LLVM_USE_HARNESS=1 NYASH_DISABLE_PLUGINS=1 "$NYASH_BIN" --backend llvm "$FIXTURE" 2>&1) +if echo "$output_no_log" | grep -q "\[lifecycle/leak\]"; then + log_error "phase285_leak_no_log: [lifecycle/leak] should NOT appear without NYASH_LEAK_LOG" + exit 1 +fi +if ! echo "$output_no_log" | grep -q "ok: cycle-created"; then + log_error "phase285_leak_no_log: Expected 'ok: cycle-created' output" + exit 1 +fi +log_success "phase285_leak_no_log: No leak output when NYASH_LEAK_LOG is unset" + +# Test 2: With NYASH_LEAK_LOG=1 - summary leak output +output_log1=$(NYASH_LEAK_LOG=1 NYASH_LLVM_USE_HARNESS=1 NYASH_DISABLE_PLUGINS=1 "$NYASH_BIN" --backend llvm "$FIXTURE" 2>&1) +if ! echo "$output_log1" | grep -q "\[lifecycle/leak\] Roots still held at exit:"; then + log_error "phase285_leak_log1: Expected '[lifecycle/leak] Roots still held at exit:' with NYASH_LEAK_LOG=1" + exit 1 +fi +if ! echo "$output_log1" | grep -q "\[lifecycle/leak\].*modules:"; then + log_error "phase285_leak_log1: Expected '[lifecycle/leak] modules: N' with NYASH_LEAK_LOG=1" + exit 1 +fi +if ! echo "$output_log1" | grep -q "ok: cycle-created"; then + log_error "phase285_leak_log1: Expected 'ok: cycle-created' output" + exit 1 +fi +log_success "phase285_leak_log1: Summary leak output with NYASH_LEAK_LOG=1" + +# Test 3: With NYASH_LEAK_LOG=2 - verbose leak output (module names) +output_log2=$(NYASH_LEAK_LOG=2 NYASH_LLVM_USE_HARNESS=1 NYASH_DISABLE_PLUGINS=1 "$NYASH_BIN" --backend llvm "$FIXTURE" 2>&1) +if ! echo "$output_log2" | grep -q "\[lifecycle/leak\].*module names:"; then + log_error "phase285_leak_log2: Expected '[lifecycle/leak] module names:' with NYASH_LEAK_LOG=2" + exit 1 +fi +log_success "phase285_leak_log2: Verbose leak output with NYASH_LEAK_LOG=2" + +log_success "phase285_leak_report_llvm: All tests passed" +exit 0 diff --git a/tools/smokes/v2/profiles/quick/lifecycle/phase285_weak_basic_llvm.sh b/tools/smokes/v2/profiles/quick/lifecycle/phase285_weak_basic_llvm.sh index a7e0ce85..fa83f30a 100644 --- a/tools/smokes/v2/profiles/quick/lifecycle/phase285_weak_basic_llvm.sh +++ b/tools/smokes/v2/profiles/quick/lifecycle/phase285_weak_basic_llvm.sh @@ -1,10 +1,10 @@ #!/bin/bash # phase285_weak_basic_llvm.sh - Phase 285A0.1: WeakRef basic smoke test (LLVM) # -# SKIP: WeakRef (weak/weak_to_strong) not yet supported in LLVM harness (Phase 285A1) +# SKIP: WeakRef (weak/weak_to_strong) not yet supported in LLVM harness (Phase 285LLVM-1) source "$(dirname "$0")/../../../lib/test_runner.sh" require_env || exit 2 -test_skip "phase285_weak_basic_llvm" "WeakRef (weak/weak_to_strong) not yet supported in LLVM harness (Phase 285A1)" +test_skip "phase285_weak_basic_llvm" "WeakRef (weak/weak_to_strong) not yet supported in LLVM harness (Phase 285LLVM-1)" exit 0