diff --git a/apps/tests/phase285_leak_report.hako b/apps/tests/phase285_leak_report.hako new file mode 100644 index 00000000..42d06ed2 --- /dev/null +++ b/apps/tests/phase285_leak_report.hako @@ -0,0 +1,16 @@ +// Phase 285: Leak report test fixture +// This program creates user boxes that should appear in leak report +// Expected: With NYASH_LEAK_LOG=1, [lifecycle/leak] output appears + +box Node { other } + +static box Main { + main() { + local a = new Node() + local b = new Node() + a.other = b + b.other = a + print("ok: cycle-created") + return 0 + } +} diff --git a/apps/tests/phase285_parser_param_type_annot_should_not_hang.hako b/apps/tests/phase285_parser_param_type_annot_should_not_hang.hako new file mode 100644 index 00000000..2bd04708 --- /dev/null +++ b/apps/tests/phase285_parser_param_type_annot_should_not_hang.hako @@ -0,0 +1,14 @@ +box TestNode { + value: IntegerBox + + // ❌ Should error immediately (type annotation not supported) + setParent(p: Node) { + return 0 + } +} + +static box Main { + main() { + return 0 + } +} diff --git a/apps/tests/phase285_visibility_weak_sugar_ng.hako b/apps/tests/phase285_visibility_weak_sugar_ng.hako new file mode 100644 index 00000000..5d2fc86b --- /dev/null +++ b/apps/tests/phase285_visibility_weak_sugar_ng.hako @@ -0,0 +1,16 @@ +box Node { + public weak parent + + setParent(p) { + me.parent = p // ❌ Should fail: direct BoxRef assignment + } +} + +static box Main { + main() { + local n1 = new Node() + local n2 = new Node() + n1.setParent(n2) + return 0 + } +} diff --git a/apps/tests/phase285_visibility_weak_sugar_ok.hako b/apps/tests/phase285_visibility_weak_sugar_ok.hako new file mode 100644 index 00000000..b929c3e6 --- /dev/null +++ b/apps/tests/phase285_visibility_weak_sugar_ok.hako @@ -0,0 +1,18 @@ +box Node { + public weak parent + name + + setParent(p) { + me.parent = weak(p) // ✅ Explicit weak() required + } +} + +static box Main { + main() { + local n1 = new Node() + local n2 = new Node() + n1.setParent(n2) + print("OK: public weak parent sugar syntax") + return 0 + } +} diff --git a/apps/tests/phase285_weak_basic.hako b/apps/tests/phase285_weak_basic.hako new file mode 100644 index 00000000..b8ded279 --- /dev/null +++ b/apps/tests/phase285_weak_basic.hako @@ -0,0 +1,35 @@ +// Phase 285A0.1: WeakRef basic test +// SSOT: docs/reference/language/lifecycle.md:179 - weak(x)/weak_to_strong() +// +// Test: weak(x) creates WeakRef, weak_to_strong() returns Box when alive +// Note: Full drop semantics test deferred (needs GC/scope analysis) +// VM: PASS expected +// LLVM: SKIP (Phase 285A1) + +box SomeBox { + x +} + +static box Main { + main() { + local x = new SomeBox() + x.x = 42 + local w = weak(x) + + // Test 1: weak_to_strong should succeed while x is alive + local y = w.weak_to_strong() + if y == null { + print("ng: weak_to_strong returned null while x alive") + return 1 + } + + // Test 2: verify weak_to_strong returns same box + if y.x != 42 { + print("ng: weak_to_strong value differs from original") + return 1 + } + + print("ok: weak and weak_to_strong work correctly") + return 0 + } +} diff --git a/apps/tests/phase285_weak_field_legacy_init_ok.hako b/apps/tests/phase285_weak_field_legacy_init_ok.hako new file mode 100644 index 00000000..f7426370 --- /dev/null +++ b/apps/tests/phase285_weak_field_legacy_init_ok.hako @@ -0,0 +1,15 @@ +// Phase 285A1.2: Legacy init { weak } syntax compatibility test +// Tests that the old syntax still works and runs successfully +box Node { + init { weak parent } +} + +static box Main { + main() { + local n1 = new Node() + local n2 = new Node() + n1.parent = weak(n2) // ✅ Explicit weak() + print("OK: legacy init { weak parent } syntax still works") + return 0 + } +} diff --git a/apps/tests/phase285_weak_field_ng_boxref.hako b/apps/tests/phase285_weak_field_ng_boxref.hako index 86c889a0..10e3776e 100644 --- a/apps/tests/phase285_weak_field_ng_boxref.hako +++ b/apps/tests/phase285_weak_field_ng_boxref.hako @@ -1,5 +1,5 @@ box Node { - init { weak parent } + weak parent } static box Main { diff --git a/apps/tests/phase285_weak_field_ok_explicit.hako b/apps/tests/phase285_weak_field_ok_explicit.hako index 50e4b19f..1dfc2261 100644 --- a/apps/tests/phase285_weak_field_ok_explicit.hako +++ b/apps/tests/phase285_weak_field_ok_explicit.hako @@ -1,5 +1,5 @@ box Node { - init { weak parent } + weak parent } static box Main { diff --git a/apps/tests/phase285_weak_field_ok_transfer.hako b/apps/tests/phase285_weak_field_ok_transfer.hako index b6c0f1ac..1dded994 100644 --- a/apps/tests/phase285_weak_field_ok_transfer.hako +++ b/apps/tests/phase285_weak_field_ok_transfer.hako @@ -1,5 +1,6 @@ box Node { - init { weak parent, weak sibling } + weak parent + weak sibling } static box Main { diff --git a/apps/tests/phase285_weak_field_ok_void.hako b/apps/tests/phase285_weak_field_ok_void.hako index eebbba34..d2a75bd7 100644 --- a/apps/tests/phase285_weak_field_ok_void.hako +++ b/apps/tests/phase285_weak_field_ok_void.hako @@ -1,5 +1,5 @@ box Node { - init { weak parent } + weak parent } static box Main { diff --git a/docs/archive/core-language/portability-contract.md b/docs/archive/core-language/portability-contract.md new file mode 100644 index 00000000..2facd571 --- /dev/null +++ b/docs/archive/core-language/portability-contract.md @@ -0,0 +1,303 @@ +# 🤝 Nyash Portability Contract v0 + +*ChatGPT5アドバイス・全バックエンド互換性保証仕様* + +⚠️ **アーカイブ済み (2025-12-24)** - 旧所在地: `docs/reference/core-language/portability-contract.md` + +Language SSOT(仕様は以下を参照してください): +- Lifecycle/weak/fini/GC policy: `docs/reference/language/lifecycle.md` +- Truthiness + `null`/`void`: `docs/reference/language/types.md` + +Status: Draft / aspirational. This document is not the language SSOT. + +## 🎯 目的 + +**「nyash --target= interp / vm / wasm / aot-rust / jit-cranelift」で同一プログラムが同一結果を保証** + +全バックエンドでNyashプログラムが確実に動作し、最適化レベルに関係なく**決定的で予測可能な実行**を実現。 + +## 🔧 **Contract v0 仕様** + +### 1️⃣ **決定的破棄(Deterministic Finalization)** + +#### **強参照のみ伝播保証** +```rust +// ✅ 保証される動作 +box Parent { + child_strong: ChildBox // 強参照→破棄連鎖 +} + +parent.fini() // 必ずchild_strong.fini()も呼ばれる +``` + +#### **破棄順序の決定性** +```nyash +// 破棄順序: 最新→最古(スタック順序) +box Child from Parent { + init { data } + pack() { + from Parent.pack() // 1. Parent初期化 + me.data = "child" // 2. Child初期化 + } + // fini順序: 2→1(逆順破棄) +} +``` + +#### **例外安全性** +```rust +pub enum FinalizationGuarantee { + AlwaysExecuted, // cleanup がある場合は例外時も必ず実行(言語SSOTは cleanup に依存) + NoDoubleDestroy, // 同一オブジェクトの二重破棄禁止 + OrderPreserved, // 初期化と逆順での破棄保証 +} +``` + +### 2️⃣ **weak参照の非伝播+生存チェック** + +#### **非伝播保証** +```nyash +box Parent { + init { child_weak } + + pack() { + local child = new Child() + me.child_weak = weak(child) // weak参照生成 + // child がfini()されても Parent は影響なし + } +} +``` + +#### **生存チェック必須** +```mir +// MIR レベルでの生存チェック +%alive = weak_load %weak_ref +br %alive -> %use_bb, %null_bb + +%use_bb: + // weak参照が有効な場合の処理 + %value = /* weak_refの値使用 */ + jmp %continue_bb + +%null_bb: + // weak参照が無効な場合の処理 + %value = const null // note: `null` and `void` are equivalent at runtime (SSOT: types.md) + jmp %continue_bb + +%continue_bb: + // 合流地点(Phi必須) + %result = phi [%value from %use_bb, %value from %null_bb] +``` + +#### **失効時の挙動契約** +```rust +pub struct WeakContract { + auto_nullification: true, // weak_to_strong失敗時は `null` を返す(field access は自動で強参照化しない) + no_dangling_pointers: true, // ダングリングポインタ禁止 + thread_safe_access: true, // マルチスレッド安全アクセス +} +``` + +### 3️⃣ **Effect意味論(最適化可能性)** + +#### **Effect分類契約** +```rust +pub enum EffectLevel { + Pure, // 副作用なし→並び替え・除去・重複実行可能 + Mut, // メモリ変更→順序保証必要・並列化制限 + Io, // I/O操作→実行順序厳密保証・キャッシュ禁止 + Bus, // 分散通信→elision対象・ネットワーク最適化可能 +} +``` + +#### **最適化契約** +```mir +// Pure関数→最適化可能 +%result1 = call @pure_function(%arg) effects=[PURE] +%result2 = call @pure_function(%arg) effects=[PURE] +// → 最適化: %result2 = copy %result1 + +// Mut操作→順序保証 +store %value1 -> %ptr effects=[MUT] +store %value2 -> %ptr effects=[MUT] +// → 順序維持必須 + +// Bus操作→elision対象 +send %bus, %message effects=[BUS] +// → ネットワーク最適化・バッチ化可能 +``` + +### 4️⃣ **Bus-elision基盤契約** + +#### **elision ON/OFF同一結果保証** +```bash +# 最適化ON→高速実行 +nyash --elide-bus --target wasm program.hako + +# 最適化OFF→完全分散実行 +nyash --no-elide-bus --target vm program.hako + +# 結果は必ず同一(契約保証) +``` + +#### **Bus操作の意味保証** +```mir +// Bus送信の意味論 +send %bus, %message effects=[BUS] { + // elision OFF: 実際のネットワーク送信 + // elision ON: ローカル最適化(結果同一) +} + +// Bus受信の意味論 +%msg = recv %bus effects=[BUS] { + // elision OFF: ネットワーク受信待ち + // elision ON: ローカル値返却(結果同一) +} +``` + +## 🧪 **Contract検証システム** + +### **互換テストスイート** +```rust +// tests/portability_contract_tests.rs +#[test] +fn test_deterministic_finalization() { + let program = "/* fini順序テスト */"; + + let interp_result = run_interpreter(program); + let vm_result = run_vm(program); + let wasm_result = run_wasm(program); + + // 破棄順序・タイミングが全バックエンドで同一 + assert_eq!(interp_result.finalization_order, vm_result.finalization_order); + assert_eq!(vm_result.finalization_order, wasm_result.finalization_order); +} + +#[test] +fn test_weak_reference_semantics() { + let program = "/* weak参照テスト */"; + + // 生存チェック・null化が全バックエンドで同一動作 + let results = run_all_backends(program); + assert_all_equal(results.map(|r| r.weak_behavior)); +} + +#[test] +fn test_effect_optimization_equivalence() { + let program = "/* Effect最適化テスト */"; + + // PURE関数の最適化結果が同一 + let optimized = run_with_optimization(program); + let reference = run_without_optimization(program); + assert_eq!(optimized.output, reference.output); +} + +#[test] +fn test_bus_elision_equivalence() { + let program = "/* Bus通信テスト */"; + + let elision_on = run_with_flag(program, "--elide-bus"); + let elision_off = run_with_flag(program, "--no-elide-bus"); + + // Bus最適化ON/OFFで結果同一 + assert_eq!(elision_on.output, elision_off.output); +} +``` + +### **Golden Dump検証** +```bash +#!/bin/bash +# scripts/verify_portability_contract.sh + +echo "🧪 Portability Contract v0 検証中..." + +# 1. MIR出力一致検証 +nyash --dump-mir test.hako > golden.mir +nyash --dump-mir test.hako > current.mir +if ! diff golden.mir current.mir; then + echo "❌ MIR回帰エラー検出" + exit 1 +fi + +# 2. 全バックエンド同一出力 +declare -a backends=("interp" "vm" "wasm") +for backend in "${backends[@]}"; do + nyash --target $backend test.hako > ${backend}.out +done + +# 出力一致確認 +if diff interp.out vm.out && diff vm.out wasm.out; then + echo "✅ 全バックエンド出力一致" +else + echo "❌ バックエンド出力差異検出" + exit 1 +fi + +# 3. Bus-elision検証 +nyash --elide-bus test.hako > elision_on.out +nyash --no-elide-bus test.hako > elision_off.out +if diff elision_on.out elision_off.out; then + echo "✅ Bus-elision同一結果" +else + echo "❌ Bus-elision結果差異" + exit 1 +fi + +echo "🎉 Portability Contract v0 検証完了" +``` + +## 📊 **Contract適合レベル** + +### **Tier-0: 基本互換性** +- [ ] **決定的破棄**: fini()順序がバックエンド間で同一 +- [ ] **weak非伝播**: weak参照が親破棄に影響しない +- [ ] **基本Effect**: PURE/MUT/IO の意味論統一 +- [ ] **出力一致**: 同一プログラム→同一標準出力 + +### **Tier-1: 最適化互換性** +- [ ] **PURE最適化**: 純粋関数の除去・移動がバックエンド間で同等 +- [ ] **weak生存チェック**: 全バックエンドで同一タイミング +- [ ] **Bus-elision**: ON/OFF切り替えで結果同一 +- [ ] **性能予測**: 最適化レベル差が定量的 + +### **Tier-2: 高度互換性** +- [ ] **メモリレイアウト**: Box構造がバックエンド間で互換 +- [ ] **エラー処理**: 例外・パニックが同一動作 +- [ ] **並行性**: Future/awaitが同一意味論 +- [ ] **デバッグ**: スタックトレース・診断情報が同等 + +## ⚡ **実装優先順位** + +### **Phase 8.4(今すぐ)** +1. **Tier-0契約実装**: 基本互換性確保 +2. **Golden dump自動化**: CI/CDで回帰検出 +3. **Bus命令設計**: elision基盤構築 + +### **Phase 8.5(短期)** +1. **Tier-1契約実装**: 最適化互換性 +2. **性能ベンチマーク**: 契約準拠性測定 +3. **エラー契約**: 例外処理統一 + +### **Phase 9+(中長期)** +1. **Tier-2契約実装**: 高度互換性 +2. **形式検証**: 契約の数学的証明 +3. **認証システム**: 契約適合認定 + +--- + +## 🎯 **期待効果** + +### **開発者体験** +- **予測可能性**: どのバックエンドでも同一動作保証 +- **デバッグ容易性**: バックエンド切り替えで問題切り分け +- **最適化信頼性**: 高速化しても結果不変保証 + +### **Nyash言語価値** +- **差別化**: 「全バックエンド互換」言語として独自性 +- **信頼性**: エンタープライズ採用の技術的根拠 +- **拡張性**: 新バックエンド追加時の品質保証 + +--- + +*最終更新: 2025-08-14 - ChatGPT5アドバイス完全実装* + +*「Everything is Box」×「全バックエンド互換」= Nyashの技術的優位性* diff --git a/docs/development/current/main/investigations/loopbuilder-removal-compatibility-analysis.md b/docs/development/current/main/investigations/loopbuilder-removal-compatibility-analysis.md new file mode 100644 index 00000000..c1f51c9e --- /dev/null +++ b/docs/development/current/main/investigations/loopbuilder-removal-compatibility-analysis.md @@ -0,0 +1,619 @@ +# LoopBuilder Removal Compatibility Analysis + +**Date**: 2025-12-24 +**Investigator**: Claude Code +**Test**: `core_direct_array_oob_set_rc_vm` +**Error**: `[joinir/freeze] Loop lowering failed: JoinIR does not support this pattern, and LoopBuilder has been removed` +**User Hypothesis**: "昔のjoinirが今のjoinirに対応してないだけじゃない?" (Old JoinIR code not compatible with current JoinIR?) + +--- + +## Executive Summary + +**User's hypothesis is CORRECT!** This is not a bug in the current code—it's a **feature gap** in JoinIR pattern coverage. + +### Root Cause + +1. **LoopBuilder was physically deleted** in Phase 187 (commit `fa8a96a51`, Dec 4, 2025) + - Deleted: 8 files, ~1,758 lines of legacy loop lowering code + - Preserved: Only `IfInLoopPhiEmitter` moved to minimal module + +2. **BundleResolver.resolve/4 uses a loop pattern not yet supported by JoinIR** + - The `.hako` code in `lang/src/compiler/entry/bundle_resolver.hako` was written when LoopBuilder still existed + - This specific loop pattern hasn't been migrated to JoinIR patterns yet + +3. **This is NOT a regression**—it's an **expected gap** documented in Phase 188 + - Phase 188 identified 5 failing loop patterns after LoopBuilder removal + - The failing loop in `phase264_p0_bundle_resolver_loop_min.hako` matches the documented gap + +### Current Status + +- **LoopBuilder**: ❌ Completely removed (Dec 4, 2025, Phase 187) +- **JoinIR Patterns Implemented**: Pattern 1, 2, 3 (Phase 188, Dec 5-6, 2025) +- **Coverage**: ~80% of representative tests pass +- **Gap**: BundleResolver loop falls into the 20% not yet covered + +--- + +## Timeline: LoopBuilder Removal + +### Phase 186: Hard Freeze (Dec 4, 2025) +- Commit: `30f94c955` +- Added access control guard to prevent LoopBuilder usage +- Intent: Make LoopBuilder opt-in only with `NYASH_LEGACY_LOOPBUILDER=1` +- **Gap Found**: Guard was added but instantiation point wasn't protected! + +### Phase 187: Physical Removal (Dec 4, 2025) +- Commit: `fa8a96a51` +- **Deleted**: + - `src/mir/loop_builder/mod.rs` (158 lines) + - `src/mir/loop_builder/loop_form.rs` (578 lines) + - `src/mir/loop_builder/if_lowering.rs` (298 lines) + - `src/mir/loop_builder/phi_ops.rs` (333 lines) + - `src/mir/loop_builder/control.rs` (94 lines) + - `src/mir/loop_builder/statements.rs` (39 lines) + - `src/mir/loop_builder/joinir_if_phi_selector.rs` (168 lines) + - `src/mir/loop_builder/README.md` (15 lines) +- **Total Deletion**: 1,758 lines +- **Preserved**: Only `IfInLoopPhiEmitter` moved to `src/mir/phi_core/if_in_loop_phi_emitter/mod.rs` + +### Phase 188: JoinIR Pattern Expansion (Dec 5-6, 2025) +- Implemented 3 loop patterns to replace LoopBuilder: + 1. **Pattern 1**: Simple While Loop (`loop(i < 3) { i++ }`) + 2. **Pattern 2**: Loop with Conditional Break (`loop(true) { if(...) break }`) + 3. **Pattern 3**: Loop with If-Else PHI (`loop(...) { if(...) sum += x else sum += 0 }`) +- **Coverage**: Estimated 80% of representative tests +- **Documented Gaps**: 5 failing patterns identified in `inventory.md` + +--- + +## What is LoopBuilder? + +### Historical Context + +**LoopBuilder** was the original loop lowering system (pre-Phase 186): +- **Purpose**: Convert loop AST → MIR with SSA/PHI nodes +- **Implementation**: 8 files, ~1,000+ lines +- **Problems**: + - Complex PHI handling with historical bugs + - Mixed control flow responsibilities + - Silent fallback behavior (hid pattern gaps) + +### Why Was It Removed? + +From Phase 187 documentation: + +> **Reasons for Deletion**: +> 1. **Isolated**: Single instantiation point made deletion safe +> 2. **Legacy**: Original implementation had historical design issues +> 3. **Proven**: Phase 181 confirmed 80% of patterns work with JoinIR only +> 4. **Explicit Failure**: Removing fallback drives JoinIR improvement + +### What Replaced It? + +**JoinIR Frontend** (modern loop lowering): +- **Pattern-based approach**: Each loop pattern has dedicated lowering logic +- **Explicit failure**: Unsupported patterns fail with clear error messages +- **Extensible**: New patterns can be added incrementally + +--- + +## The Failing BundleResolver Loop + +### Location + +**Source**: `lang/src/compiler/entry/bundle_resolver.hako` +**Function**: `BundleResolver.resolve/4` +**Lines**: Multiple loops (25-45, 52-71, 76-87, 92-101, 106-112) + +### Example Loop (lines 25-45) + +```nyash +// Alias table parsing loop +local i = 0 +loop(i < table.length()) { + // find next delimiter or end + local j = table.indexOf("|||", i) + local seg = "" + if j >= 0 { + seg = table.substring(i, j) + } else { + seg = table.substring(i, table.length()) + } + + if seg != "" { + local pos = -1 + local k = 0 + loop(k < seg.length()) { + if seg.substring(k,k+1) == ":" { + pos = k + break + } + k = k + 1 + } + // ... more processing + } + + if j < 0 { break } + i = j + 3 +} +``` + +### Why This Fails + +**Pattern Characteristics**: +- **Multiple carriers**: `i`, `j`, `seg`, `pos`, `k` (5+ variables) +- **Nested loop**: Inner loop with `break` +- **Conditional assignments**: `seg = ... if ... else ...` +- **Non-unit increment**: `i = j + 3` (not `i++`) +- **Complex control flow**: Multiple `if` statements with `break` + +**Current JoinIR Pattern Coverage**: +1. ❌ **Pattern 1** (Simple While): Too simple—only handles single carrier, no breaks +2. ❌ **Pattern 2** (Conditional Break): Doesn't handle multiple carriers + nested loops +3. ❌ **Pattern 3** (If-Else PHI): Only handles "if-sum" pattern (arithmetic accumulation) +4. ❌ **Pattern 4** (Continue): Not applicable (no `continue` statements) + +**Result**: No pattern matches → falls through → explicit error + +--- + +## Phase 264: Recent Attempt to Fix Similar Issue + +### Context + +Phase 264 P0 documented a similar failure in `phase264_p0_bundle_resolver_loop_min.hako`: + +```nyash +local i = 0 +local seg = "" + +loop(i < 10) { + // Conditional assignment to seg + if i == 0 { + seg = "first" + } else { + seg = "other" + } + + // Non-unit increment + i = i + 2 +} +``` + +### Why It Failed + +**Pattern routing flow**: +1. **Pattern 8** (BoolPredicateScan): REJECT (condition right is not `.length()`) +2. **Pattern 3** (WithIfPhi): MATCHED → but rejects "Not an if-sum pattern" +3. **Pattern 1/2**: Not tried +4. **Result**: No match → ERROR + +### Root Cause (from Phase 264 analysis) + +**Classification Heuristic Issue**: + +```rust +// src/mir/loop_pattern_detection/mod.rs:227-230 +// Pattern 3 heuristic: has_if_else_phi if carrier_count > 1 +let has_if_else_phi = carrier_count > 1; +``` + +**Problem**: +- This heuristic is **too conservative** +- Any loop with 2+ carriers → classified as Pattern3IfPhi +- But Pattern3 only handles **if-sum patterns** (arithmetic accumulation) +- Simple conditional assignment → incorrectly routed to Pattern3 → rejected + +### Proposed Fix (Phase 264) + +**Option B** (adopted): Improve classification heuristic + +```rust +// Phase 264 P0: Improved if-else PHI detection +// Pattern3 heuristic: has_if_else_phi if there's an if-sum pattern signature +let has_if_else_phi = carrier_count > 1 && has_if_sum_signature(scope); +``` + +**Impact**: +- Simple conditional assignment → falls through to Pattern1 +- If-sum pattern → correctly routed to Pattern3 + +--- + +## Answer to Investigation Questions + +### 1. Where is "LoopBuilder has been removed" error generated? + +**Location**: `src/mir/builder/control_flow/mod.rs:136-145` + +```rust +// Phase 186: LoopBuilder Hard Freeze - Legacy path disabled +// Phase 187-2: LoopBuilder module removed - all loops must use JoinIR +use crate::mir::join_ir::lowering::error_tags; +return Err(error_tags::freeze(&format!( + "Loop lowering failed: JoinIR does not support this pattern, and LoopBuilder has been removed.\n\ + Function: {}\n\ + Hint: This loop pattern is not supported. All loops must use JoinIR lowering.", + self.scope_ctx.current_function.as_ref().map(|f| f.signature.name.as_str()).unwrap_or("") +))); +``` + +### 2. When was LoopBuilder removed? + +**Date**: December 4, 2025 +**Commit**: `fa8a96a51b909e1fbb7a61c8e1b989050c58d4ee` +**Phase**: 187 +**Files Deleted**: 8 files, 1,758 lines + +**Git History**: +```bash +git log --all --oneline --grep="LoopBuilder" | head -5 +# fa8a96a51 docs(joinir): Phase 187 LoopBuilder Physical Removal +# 1e350a6bc docs(joinir): Phase 186-4 Documentation Update +# 30f94c955 feat(joinir): Phase 186 LoopBuilder Hard Freeze +``` + +### 3. Are there any remaining LoopBuilder calls? + +**No**. The module was completely deleted in Phase 187: + +```bash +# Before Phase 187 +src/mir/loop_builder/ +├── mod.rs (158 lines) +├── loop_form.rs (578 lines) +├── if_lowering.rs (298 lines) +├── phi_ops.rs (333 lines) +├── control.rs (94 lines) +├── statements.rs (39 lines) +├── joinir_if_phi_selector.rs (168 lines) +└── README.md (15 lines) + +# After Phase 187 +❌ DELETED (all files) +✅ PRESERVED: IfInLoopPhiEmitter → src/mir/phi_core/if_in_loop_phi_emitter/mod.rs +``` + +**Remaining References**: Only in documentation and git history + +### 4. Where is BundleResolver defined? + +**Location**: `lang/src/compiler/entry/bundle_resolver.hako` +**Type**: `.hako` source code (selfhost compiler component) +**Function**: `BundleResolver.resolve/4` (static box method) + +**Purpose**: Stage-B bundling resolver +- Merges multiple source bundles +- Resolves module dependencies +- Handles duplicate/missing module detection + +### 5. Is it using old JoinIR patterns? + +**YES—this is the core issue!** + +**BundleResolver.resolve/4 characteristics**: +- Written during LoopBuilder era (before Phase 186-188) +- Uses loop patterns that worked with LoopBuilder +- Those patterns are NOT yet covered by current JoinIR patterns + +**Example incompatibility**: +```nyash +// This pattern worked with LoopBuilder +local i = 0 +loop(i < table.length()) { + local j = table.indexOf("|||", i) + // ... complex processing + if j < 0 { break } + i = j + 3 // Non-unit increment +} + +// Current JoinIR patterns can't handle: +// - Multiple carriers (i, j) +// - Non-unit increment (i = j + 3) +// - Conditional break +// - String method calls in condition/body +``` + +### 6. Can we see the actual loop structure that's failing? + +**Yes**—see section "The Failing BundleResolver Loop" above. + +**Key problematic features**: +1. **Multiple carrier variables** (i, j, seg, pos, k) +2. **Nested loops** (outer loop with inner loop) +3. **Conditional assignments** (`seg = if ... then ... else ...`) +4. **Non-unit increment** (`i = j + 3`) +5. **Complex control flow** (multiple `if`/`break`) +6. **String operations** (`.indexOf()`, `.substring()`, `.length()`) + +### 7. What replaced LoopBuilder? + +**JoinIR Frontend** with pattern-based lowering: + +| Pattern | Description | Status | Example | +|---------|-------------|--------|---------| +| **Pattern 1** | Simple While | ✅ Implemented | `loop(i < n) { i++ }` | +| **Pattern 2** | Conditional Break | ✅ Implemented | `loop(true) { if(...) break }` | +| **Pattern 3** | If-Else PHI | ✅ Implemented | `loop(...) { if(...) sum += x else sum += 0 }` | +| **Pattern 4** | Continue | ⏳ Planned | `loop(...) { if(...) continue }` | +| **Pattern 5** | Infinite Early Exit | ⏳ Planned | `loop(true) { if(...) break; if(...) continue }` | + +**Coverage**: ~80% of representative tests pass with Patterns 1-3 + +### 8. Is there a migration path documented? + +**YES**—documented in Phase 188: + +**From**: `docs/private/roadmap2/phases/phase-188-joinir-loop-pattern-expansion/README.md` + +> **Phase 188 Objective**: Expand JoinIR loop pattern coverage to handle loop patterns currently failing with `[joinir/freeze]` error. +> +> **Status**: Planning 100% complete, Pattern 1/2 lowering + execution 100% complete, Pattern 3 lowering 100% complete + +**Migration Strategy**: +1. **Identify failing patterns** (Task 188-1: Error Inventory) ✅ Done +2. **Classify patterns** (Task 188-2: Pattern Classification) ✅ Done +3. **Prioritize 2-3 patterns** (Task 188-2) ✅ Done (Patterns 1, 2, 3) +4. **Design JoinIR extensions** (Task 188-3) ✅ Done +5. **Implement lowering** (Task 188-4) ✅ Done (Patterns 1-3) +6. **Verify representative tests** (Task 188-5) ⏳ Partial (80% coverage) + +**Documented Gaps** (from Phase 188 inventory): + +| # | Pattern | Status | Priority | +|---|---------|--------|----------| +| 1 | Simple While Loop | ✅ Implemented | HIGH | +| 2 | Loop with Conditional Break | ✅ Implemented | HIGH | +| 3 | Loop with If-Else PHI | ✅ Implemented | MEDIUM | +| 4 | Loop with One-Sided If | ⏳ May be auto-handled | MEDIUM | +| 5 | Loop with Continue | ⏳ Deferred | LOW | +| **6** | **Complex Multi-Carrier** | ❌ **NOT PLANNED** | **?** | + +**BundleResolver falls into Gap #6**: Complex loops with multiple carriers, nested loops, and non-unit increments. + +--- + +## Root Cause Analysis + +### Is this a code update issue or a feature gap? + +**Answer**: **Feature gap** (not a bug) + +### Detailed Analysis + +**What happened**: +1. **Phase 186-187** (Dec 4): LoopBuilder removed to force explicit failures +2. **Phase 188** (Dec 5-6): JoinIR Patterns 1-3 implemented (80% coverage) +3. **Gap remains**: Complex loops (like BundleResolver) not yet covered +4. **This test fails**: Because BundleResolver uses a pattern outside the 80% coverage + +**Is the `.hako` code wrong?** +- **No**—the code is valid Nyash syntax +- It worked when LoopBuilder existed +- It's just a pattern that hasn't been migrated to JoinIR yet + +**Is the JoinIR implementation wrong?** +- **No**—JoinIR is working as designed +- It explicitly fails on unsupported patterns (as intended) +- This failure drives future pattern expansion + +**Is this a regression?** +- **No**—this is an **expected gap** documented in Phase 188 +- The removal of LoopBuilder was intentional to force explicit failures +- Phase 188 documented 5 failing patterns; this is one of them + +--- + +## What's the Fix? + +### Short-term Options + +#### Option A: Rewrite BundleResolver loops to use supported patterns ⭐ **RECOMMENDED** + +**Pros**: +- Immediate fix (no compiler changes needed) +- Makes code compatible with current JoinIR +- Educational exercise (learn supported patterns) + +**Cons**: +- Requires understanding which patterns are supported +- May require restructuring loop logic + +**Example Rewrite**: + +**Before** (unsupported): +```nyash +local i = 0 +loop(i < table.length()) { + local j = table.indexOf("|||", i) + // ... complex processing + if j < 0 { break } + i = j + 3 // Non-unit increment +} +``` + +**After** (Pattern 2 compatible): +```nyash +local i = 0 +local done = 0 +loop(i < table.length() and done == 0) { + local j = table.indexOf("|||", i) + // ... complex processing + if j < 0 { + done = 1 + } else { + i = j + 3 + } +} +``` + +#### Option B: Implement new JoinIR pattern for complex loops ⏳ **FUTURE WORK** + +**Pros**: +- Proper solution (makes compiler more capable) +- Benefits all similar loop patterns + +**Cons**: +- Large effort (Pattern 1-3 took 1,802 lines + design docs) +- Not urgent (only affects 20% of tests) + +**Steps**: +1. Design "Pattern 6: Complex Multi-Carrier Loop" +2. Implement detection logic +3. Implement JoinIR lowering +4. Test with BundleResolver and similar loops + +#### Option C: Temporarily restore LoopBuilder ❌ **NOT RECOMMENDED** + +**Pros**: +- Immediate fix (git revert) + +**Cons**: +- Defeats the purpose of Phase 186-188 +- Re-introduces legacy bugs +- Hides pattern gaps (prevents JoinIR improvement) + +**From Phase 187 docs**: +> **Lesson 3**: Explicit Failure Drives Architecture Forward +> Removing the fallback path forces future work to improve JoinIR, not work around it. + +### Long-term Solution + +**Phase 189+ planning needed**: +1. Analyze remaining 20% of failing tests +2. Identify common patterns (e.g., "Complex Multi-Carrier") +3. Prioritize by impact (selfhost critical paths) +4. Design and implement additional JoinIR patterns + +**From Phase 188 docs**: +> **Next**: Phase 189 - Multi-function JoinIR→MIR merge / Select instruction MIR bridge + +--- + +## Recommendations + +### Immediate Action (for this test failure) + +**1. Rewrite BundleResolver loops** (Option A) +- **Who**: Developer working on selfhost compiler +- **What**: Refactor loops to use Pattern 1 or Pattern 2 +- **Why**: Immediate fix, no compiler changes needed +- **How**: See "Example Rewrite" above + +**2. Document the pattern gap** (if not already documented) +- **Where**: `docs/private/roadmap2/phases/phase-188-joinir-loop-pattern-expansion/inventory.md` +- **Add**: BundleResolver.resolve/4 as example of "Complex Multi-Carrier" pattern +- **Tag**: Phase 189+ future work + +### Future Work + +**1. Analyze BundleResolver loop patterns systematically** +- Extract common loop structures +- Classify into existing or new patterns +- Prioritize by impact on selfhost pipeline + +**2. Design "Pattern 6: Complex Multi-Carrier Loop"** (if needed) +- Support multiple carrier variables (3+) +- Support non-unit increments +- Support nested loops +- Support string operations in conditions + +**3. Consider intermediate patterns** +- "Pattern 2.5": Conditional Break + Multiple Carriers +- "Pattern 3.5": If-Else PHI + Non-unit Increment + +### Testing Strategy + +**1. Create minimal reproducer** +- Extract minimal BundleResolver loop to separate test file +- Use as regression test for future pattern work + +**2. Add to Phase 188 inventory** +- Document exact pattern characteristics +- Mark as "Pattern 6 candidate" or "Defer to Phase 189+" + +**3. Track coverage metrics** +- Current: 80% (Patterns 1-3) +- Target: 95%+ (add Pattern 4-6) +- Critical: 100% of selfhost paths + +--- + +## Conclusion + +### Summary of Findings + +1. **User hypothesis is CORRECT**: This is old code (BundleResolver.hako) not compatible with new JoinIR patterns + +2. **LoopBuilder was removed intentionally** in Phase 187 (Dec 4, 2025) + - Deleted: 8 files, 1,758 lines + - Replaced by: JoinIR Frontend with pattern-based lowering + +3. **Current JoinIR coverage: ~80%** (Patterns 1-3 implemented in Phase 188) + - Pattern 1: Simple While ✅ + - Pattern 2: Conditional Break ✅ + - Pattern 3: If-Else PHI ✅ + +4. **BundleResolver loop falls in the 20% gap**: + - Multiple carriers (5+ variables) + - Nested loops + - Non-unit increments + - Complex control flow + +5. **This is NOT a bug**—it's a documented feature gap + - Expected from Phase 188 planning + - Failure is intentional (drives JoinIR improvement) + +### Recommended Fix + +**Short-term**: Rewrite BundleResolver loops to use Pattern 1 or Pattern 2 +**Long-term**: Implement "Pattern 6: Complex Multi-Carrier Loop" in Phase 189+ + +### Key Insight + +The removal of LoopBuilder forces **explicit failures** rather than **silent fallbacks**. This is a **feature, not a bug**—it ensures: +- Clear error messages guide developers +- Pattern gaps are visible (not hidden) +- Future work focuses on real needs (not legacy compatibility) + +**From Phase 187 documentation**: +> "Explicit failures replace implicit fallbacks. Future JoinIR expansion is the only way forward." + +--- + +## References + +### Documentation + +- **Phase 186**: `docs/private/roadmap2/phases/phase-186-loopbuilder-freeze/README.md` +- **Phase 187**: `docs/private/roadmap2/phases/phase-180-joinir-unification-before-selfhost/README.md#8-phase-187` +- **Phase 188**: `docs/private/roadmap2/phases/phase-188-joinir-loop-pattern-expansion/README.md` +- **Phase 264**: `docs/development/current/main/phases/phase-264/README.md` + +### Source Files + +- **Error location**: `src/mir/builder/control_flow/mod.rs:136-145` +- **Pattern detection**: `src/mir/loop_pattern_detection/mod.rs` +- **BundleResolver**: `lang/src/compiler/entry/bundle_resolver.hako` +- **Test file**: `apps/tests/phase264_p0_bundle_resolver_loop_min.hako` + +### Git Commits + +- **Phase 186 Freeze**: `30f94c955` (Dec 4, 2025) +- **Phase 187 Removal**: `fa8a96a51` (Dec 4, 2025) +- **Phase 188 Pattern 1**: `d303d24b4` (Dec 6, 2025) +- **Phase 188 Pattern 2**: `87e477b13` (Dec 6, 2025) +- **Phase 188 Pattern 3**: `638182a8a` (Dec 6, 2025) + +### Related Issues + +- Phase 188 Error Inventory: 5 failing patterns documented +- Phase 264 P0: Similar issue with classification heuristic +- Phase 189 Planning: Multi-function JoinIR merge (future) + +--- + +**Investigation Complete** +**Date**: 2025-12-24 +**Status**: ✅ Root cause identified, recommendations provided diff --git a/docs/development/current/main/investigations/loopbuilder-removal-quick-summary.md b/docs/development/current/main/investigations/loopbuilder-removal-quick-summary.md new file mode 100644 index 00000000..d901fcaf --- /dev/null +++ b/docs/development/current/main/investigations/loopbuilder-removal-quick-summary.md @@ -0,0 +1,147 @@ +# LoopBuilder Removal - Quick Summary + +**Date**: 2025-12-24 +**Test Failure**: `core_direct_array_oob_set_rc_vm` +**Error**: `[joinir/freeze] Loop lowering failed: JoinIR does not support this pattern, and LoopBuilder has been removed` + +--- + +## TL;DR + +✅ **User hypothesis is CORRECT**: Old `.hako` code not compatible with new JoinIR patterns + +❌ **NOT a bug**: Expected feature gap (20% of patterns not yet covered) + +⭐ **Fix**: Rewrite BundleResolver loops to use supported Pattern 1 or Pattern 2 + +--- + +## What Happened? + +### Timeline + +1. **Dec 4, 2025 (Phase 186-187)**: LoopBuilder deleted (1,758 lines removed) +2. **Dec 5-6, 2025 (Phase 188)**: JoinIR Patterns 1-3 implemented (80% coverage) +3. **Dec 24, 2025 (Today)**: BundleResolver test fails (falls in 20% gap) + +### Why This Fails + +**BundleResolver.resolve/4** uses a complex loop pattern: +- Multiple carriers (5+ variables: i, j, seg, pos, k) +- Nested loops +- Non-unit increment (`i = j + 3`) +- Conditional assignments + +**Current JoinIR patterns** don't cover this: +- ✅ Pattern 1: Simple While (single carrier, no breaks) +- ✅ Pattern 2: Conditional Break (simple breaks only) +- ✅ Pattern 3: If-Else PHI (arithmetic accumulation only) +- ❌ **Pattern 6** (Complex Multi-Carrier): **Not implemented yet** + +--- + +## What is LoopBuilder? + +**Before Phase 187**: +- Legacy loop lowering system (8 files, 1,000+ lines) +- Handled all loop patterns (but with bugs) +- Silent fallback when JoinIR didn't match + +**After Phase 187**: +- ❌ Completely deleted +- ✅ Replaced by JoinIR Frontend (pattern-based) +- ✅ Explicit failures (no silent fallbacks) + +**Why removed?** +> "Explicit failures replace implicit fallbacks. Future JoinIR expansion is the only way forward." + +--- + +## How to Fix + +### Option A: Rewrite BundleResolver Loops ⭐ **RECOMMENDED** + +**Before** (unsupported): +```nyash +local i = 0 +loop(i < table.length()) { + local j = table.indexOf("|||", i) + // ... complex processing + if j < 0 { break } + i = j + 3 // Non-unit increment +} +``` + +**After** (Pattern 2 compatible): +```nyash +local i = 0 +local done = 0 +loop(i < table.length() and done == 0) { + local j = table.indexOf("|||", i) + // ... complex processing + if j < 0 { + done = 1 + } else { + i = j + 3 + } +} +``` + +**Tips**: +- Convert non-unit increments to unit increments if possible +- Convert breaks to condition checks +- Reduce carrier count (merge variables if possible) + +### Option B: Implement Pattern 6 ⏳ **FUTURE WORK** + +**Effort**: Large (Pattern 1-3 took 1,802 lines + design) +**Timeline**: Phase 189+ +**Benefit**: Supports all complex loops + +--- + +## Quick Reference + +### Supported Patterns (Phase 188) + +| Pattern | Example | Status | +|---------|---------|--------| +| **Pattern 1** | `loop(i < n) { i++ }` | ✅ Works | +| **Pattern 2** | `loop(true) { if(...) break }` | ✅ Works | +| **Pattern 3** | `loop(...) { if(...) sum += x else sum += 0 }` | ✅ Works | +| **Complex Multi-Carrier** | BundleResolver loops | ❌ Not yet | + +### Where to Find More + +- **Full Analysis**: `loopbuilder-removal-compatibility-analysis.md` (this directory) +- **Phase 188 Docs**: `docs/private/roadmap2/phases/phase-188-joinir-loop-pattern-expansion/` +- **BundleResolver Source**: `lang/src/compiler/entry/bundle_resolver.hako` + +### Git History + +```bash +# Find LoopBuilder removal +git log --all --oneline --grep="Phase 187" +# fa8a96a51 docs(joinir): Phase 187 LoopBuilder Physical Removal + +# See deleted files +git show fa8a96a51 --stat +# 13 files changed, 56 insertions(+), 1758 deletions(-) +``` + +--- + +## Key Takeaways + +1. ✅ **This is NOT a bug**—it's an expected gap (20% of patterns) +2. ✅ **LoopBuilder is gone forever**—no plans to restore +3. ✅ **Fix: Rewrite loops**—use Pattern 1 or Pattern 2 +4. ✅ **Future: Implement Pattern 6**—if this becomes a blocker + +**Philosophy**: +> Explicit failures drive architecture forward. No silent fallbacks! + +--- + +**Last Updated**: 2025-12-24 +**Status**: Investigation complete, recommendations provided diff --git a/docs/development/current/main/phases/phase-285/phase-285a1-boxification.md b/docs/development/current/main/phases/phase-285/phase-285a1-boxification.md index c8f6b929..4dd7fa66 100644 --- a/docs/development/current/main/phases/phase-285/phase-285a1-boxification.md +++ b/docs/development/current/main/phases/phase-285/phase-285a1-boxification.md @@ -180,9 +180,140 @@ Phase 285A1 は Phase 285 の一部として、weak field 処理の基盤を整 **Note**: weak field 構文(`weak next: Node`)は未実装。Phase 285 P1 で実装予定。 +--- + +## A1.5: Parser Hang Fix - Parameter Type Annotations ✅ + +**Status**: ✅ Complete (2025-12-24) + +### Problem + +During Phase 285A1.4 implementation, discovered critical parser bug: +- User writes: `setParent(p: Node) { ... }` +- Parser hangs infinitely at COLON token (no advance on unexpected token) +- Workaround required removing type annotation → `setParent(p) { ... }` → Works + +### Root Cause + +**6 identical vulnerable parameter parsing loops** across the parser codebase: +1. `src/parser/declarations/box_def/members/methods.rs:21-30` +2. `src/parser/declarations/box_def/members/constructors.rs:27-34` (init) +3. `src/parser/declarations/box_def/members/constructors.rs:101-108` (pack) +4. `src/parser/declarations/box_def/members/constructors.rs:138-145` (birth) +5. `src/parser/items/functions.rs:34-51` +6. `src/parser/items/static_items.rs:72-87` + +**Vulnerable Code Pattern**: +```rust +while !p.match_token(&TokenType::RPAREN) && !p.is_at_end() { + must_advance!(p, _unused, "method parameter parsing"); + if let TokenType::IDENTIFIER(param) = &p.current_token().token_type { + params.push(param.clone()); + p.advance(); // ← Only advances on IDENTIFIER + } + // ⚠️ COLON token: not IDENTIFIER, not COMMA, not RPAREN → NO ADVANCE → INFINITE LOOP + if p.match_token(&TokenType::COMMA) { + p.advance(); + } +} +``` + +**EBNF Spec Finding**: `params` grammar undefined in EBNF.md, `:` TYPE is for return type only + +### Solution: Shared Helper Function (DRY Principle) + +Created `src/parser/common/params.rs` with common parameter parsing logic: + +```rust +/// Parse parameter name list with Fail-Fast on unexpected tokens +/// +/// Parses: IDENT (',' IDENT)* +/// Rejects: Type annotations, unexpected tokens, malformed comma sequences +pub(crate) fn parse_param_name_list( + p: &mut NyashParser, + context: &str, // "method", "constructor", "function" for error messages +) -> Result, ParseError> +``` + +**Key Features**: +- **Progress-zero detection**: Tracks token position, errors if stuck (prevents infinite loops) +- **Explicit token handling**: All token types (IDENTIFIER, COMMA, other) explicitly matched +- **Fail-Fast**: Either advances or errors (no infinite loop possible) +- **Unified error messages**: Context parameter customizes messages per call site + +### Files Modified + +**New Files** (2): +- `src/parser/common/params.rs` (~90 lines) - Helper function +- `tools/smokes/v2/profiles/quick/parser/phase285_param_type_annotation_nohang.sh` - Timeout smoke test + +**Modified Files** (8): +1. `src/parser/common/mod.rs` - Module declaration (moved common.rs → common/mod.rs) +2. `src/parser/declarations/box_def/members/methods.rs` - Replaced 12 lines with 1 call +3. `src/parser/declarations/box_def/members/constructors.rs` - Replaced 3 loops (init/pack/birth) +4. `src/parser/items/functions.rs` - Replaced 20 lines with 2 lines +5. `src/parser/items/static_items.rs` - Replaced 22 lines with 2 lines +6. `apps/tests/phase285_parser_param_type_annot_should_not_hang.hako` - Regression test + +**Net Change**: +90 new - 72 removed + 6 calls = **+24 lines** (with better error handling!) + +### Tests + +**Regression Test**: `apps/tests/phase285_parser_param_type_annot_should_not_hang.hako` +```hako +box TestNode { + value: IntegerBox + // ❌ Should error immediately (type annotation not supported) + setParent(p: Node) { + return 0 + } +} +``` + +**Smoke Test**: `tools/smokes/v2/profiles/quick/parser/phase285_param_type_annotation_nohang.sh` +- Timeout: 3 seconds (detects hang) +- Expected: Parse error within 1 second +- Validation: Error message mentions "Unexpected token COLON" and "Parameter type annotations not supported" + +**Result**: ✅ PASS +``` +✅ PASS: Parser correctly rejects param type annotations without hanging +``` + +### Error Message Examples + +**Before** (infinite hang): +``` +🚨 PARSER INFINITE LOOP DETECTED at method parameter parsing +``` + +**After** (clear error): +``` +❌ Parse error: Unexpected token COLON, expected ',' or ')' in method parameter list. +Note: Parameter type annotations are not supported. +``` + +### Architecture Benefits + +**DRY Principle**: +- **Before**: 6 identical vulnerable loops (72 lines total) +- **After**: 1 helper function (~90 lines) + 6 one-line calls + +**Maintainability**: +- Future fixes only need 1 location +- Unified error messages (single source of truth) +- Progress-zero detection guaranteed in one place + +**Safety**: +- No infinite loop possible (Fail-Fast guaranteed) +- All token types explicitly handled (no silent fallthrough) +- Context-aware error messages (better UX) + ## References - Phase 33: Box Theory Modularization ([phase-33-modularization.md](../../../architecture/phase-33-modularization.md)) - Phase 285: Box lifecycle ([phase-285/README.md](README.md)) -- `src/mir/builder/weak_field_validator.rs`: 実装本体 -- `src/mir/builder/fields.rs`: 呼び出し側 +- `src/mir/builder/weak_field_validator.rs`: A1.1 実装本体 +- `src/mir/builder/fields.rs`: A1.1 呼び出し側 +- `src/parser/common/params.rs`: A1.5 実装本体 +- `src/parser/declarations/box_def/members/fields.rs`: A1.2-A1.4 実装本体 diff --git a/docs/guides/testing-guide.md b/docs/guides/testing-guide.md index 5de80378..aec3a5e1 100644 --- a/docs/guides/testing-guide.md +++ b/docs/guides/testing-guide.md @@ -218,6 +218,42 @@ Nyash provides a macro-powered lightweight test runner in Phase 16 (MVP). - `nyash --run-tests apps/tests/my_tests.hako` - Discovers top-level `test_*` functions and Box `test_*` methods (static/instance). - Filtering: `--test-filter NAME` (substring match) or env `NYASH_TEST_FILTER`. + +--- + +## Phase 285: Lifecycle / WeakRef / Leak diagnostics(クロスバックエンド) + +言語SSOT: +- `docs/reference/language/lifecycle.md`(`fini` / weak / cleanup / GC方針) +- `docs/reference/language/types.md`(truthiness と `null`/`void`) + +目的: +- Rust VM と LLVM(harness)で、weak と lifecycle の挙動が一致しているかを短い fixture で固定する。 +- 強参照サイクル等で「終了時に強参照rootが残っている」状況を、default-off の診断で観測できるようにする(実装後)。 + +推奨の運用(ルート汚染防止): +- fixture は `local_tests/` に置く。 + +例(weakが効いていること): +```bash +mkdir -p local_tests +$EDITOR local_tests/phase285_weak_basic.hako + +./target/release/hakorune --backend vm local_tests/phase285_weak_basic.hako +NYASH_LLVM_USE_HARNESS=1 ./target/release/hakorune --backend llvm local_tests/phase285_weak_basic.hako +``` + +例(強参照サイクル + 終了時診断): +```bash +$EDITOR local_tests/phase285_cycle.hako + +# 実装後: leak report をONにして終了時に root を表示 +NYASH_LEAK_LOG=1 ./target/release/hakorune --backend vm local_tests/phase285_cycle.hako +NYASH_LLVM_USE_HARNESS=1 NYASH_LEAK_LOG=1 ./target/release/hakorune --backend llvm local_tests/phase285_cycle.hako +``` + +実装者向けの詳しい手順: +- `docs/development/current/main/phases/phase-285/CLAUDE_CODE_RUNBOOK.md` - Entry policy when a main exists: - `--test-entry wrap` → run tests then call original main - `--test-entry override` → replace entry with test harness only diff --git a/docs/reference/boxes-system/README.md b/docs/reference/boxes-system/README.md index cd4a145a..6e00130a 100644 --- a/docs/reference/boxes-system/README.md +++ b/docs/reference/boxes-system/README.md @@ -5,6 +5,10 @@ Nyashの核心哲学「**Everything is Box**」に関する完全な設計ドキュメント集。 言語設計の根幹から実装詳細まで、Boxシステムのすべてを網羅しています。 +注(`init { ... }` について): +- `init { a, b, c }` は legacy のフィールド宣言(slot)です(互換のために残っています)。 +- 新規コードでは、可能なら Unified Members(stored/computed/once/birth_once)での宣言を推奨します(SSOT: `docs/reference/language/EBNF.md` / ライフサイクルSSOT: `docs/reference/language/lifecycle.md`)。 + ## 📚 ドキュメント構成 ### 🌟 コア哲学 @@ -51,4 +55,4 @@ Nyashの核心哲学「Everything is Box」の解説。なぜすべてをBoxに --- **最終更新**: 2025年8月19日 - boxes-system統合整理完了 -**Phase 9.75g-0成果**: プラグインシステムとの完全統合 \ No newline at end of file +**Phase 9.75g-0成果**: プラグインシステムとの完全統合 diff --git a/docs/reference/boxes-system/delegation-system.md b/docs/reference/boxes-system/delegation-system.md index 5e3e8990..fabb159c 100644 --- a/docs/reference/boxes-system/delegation-system.md +++ b/docs/reference/boxes-system/delegation-system.md @@ -5,6 +5,8 @@ Nyashは継承の代わりに「完全明示デリゲーション」を採用しています。 これは「Everything is Box」哲学に基づく、より安全で明確な設計アプローチです。 +注: `init { ... }` は legacy のフィールド宣言(slot)です(互換のために残っています)。新しい宣言モデル(Unified Members)は `docs/reference/language/EBNF.md` を参照してください。 + ## 🎯 なぜデリゲーションか ### 継承の問題点 @@ -387,4 +389,4 @@ box Proxy delegates * to target { 関連ドキュメント: - [Everything is Box](everything-is-box.md) - [override/from構文詳細](../override-delegation-syntax.md) -- [言語リファレンス](../language-reference.md) \ No newline at end of file +- [言語リファレンス](../language-reference.md) diff --git a/docs/reference/boxes-system/everything-is-box.md b/docs/reference/boxes-system/everything-is-box.md index a90b303c..f3af92fb 100644 --- a/docs/reference/boxes-system/everything-is-box.md +++ b/docs/reference/boxes-system/everything-is-box.md @@ -112,7 +112,7 @@ local result = "Hello" + 42.toString() // OK: "Hello42" ### 特殊Box型 - **FutureBox** - 非同期処理 -- **WeakBox** - 弱参照 +- **WeakRef(WeakBox)** - 弱参照(観測は `weak_to_strong()`。言語SSOT: `docs/reference/language/lifecycle.md`) - **ExternBox** - 外部ライブラリ統合 ## 🔄 Boxの生成と利用 @@ -130,6 +130,7 @@ local num = 42 // 自動的にIntegerBox ### ユーザー定義Box ```nyash +// 注: `init { ... }` は legacy のフィールド宣言(slot)です(互換のために残っています)。 box Point { init { x, y } @@ -195,4 +196,4 @@ Everything is Box哲学により、Nyashは: 関連ドキュメント: - [Box型カタログ](box-types-catalog.md) - [デリゲーションシステム](delegation-system.md) -- [メモリ管理](memory-management.md) \ No newline at end of file +- [メモリ管理](memory-management.md) diff --git a/docs/reference/boxes-system/memory-finalization.md b/docs/reference/boxes-system/memory-finalization.md index ddabdac8..0d693000 100644 --- a/docs/reference/boxes-system/memory-finalization.md +++ b/docs/reference/boxes-system/memory-finalization.md @@ -2,6 +2,8 @@ **最終更新: 2025年8月19日 - 統合仕様書** +注: 言語レベルの SSOT は `docs/reference/language/lifecycle.md`。本書は設計ノートであり、SSOT と矛盾する記述があれば SSOT を優先する。 + ## 📋 概要 Nyashは「Everything is Box」哲学のもと、統一的なメモリ管理と予測可能なリソース解放を実現しています。 @@ -72,20 +74,23 @@ box MyResource { **重要**: `fini()`は「このオブジェクトをもう使わない」という宣言であり、物理的な即時破棄ではありません。 -### 実行順序(確定仕様) +### 実行順序(設計SSOTへの案内) + +最終的な順序・禁止事項の SSOT は `docs/reference/language/lifecycle.md` に集約する。 +本セクションの箇条書きは “目標像/設計メモ” として読む。 #### 自動カスケード解放 ```nyash box Pipeline { - init { r1, r2, r3, weak monitor } + init { r1, r2, r3, monitor_weak } fini() { // 1) ユーザー定義処理(柔軟な順序制御可能) me.r3.fini() // 依存関係でr3→r2の順 me.r2.fini() - // 2) 自動カスケード: 残りのr1がinit宣言順で自動解放 - // 3) weakフィールドは対象外(lazy nil化) + // 2) 自動カスケード: 残りのr1が自動解放(weak参照は対象外) + // 3) weak参照は weak_to_strong() で観測し、失効時は null(=void/none)を返す } } ``` @@ -94,7 +99,7 @@ box Pipeline { 1. **finalized チェック** - 既に解放済みなら何もしない(idempotent) 2. **再入防止** - `in_finalization`フラグで再帰呼び出し防止 3. **ユーザー定義fini()実行** - カスタムクリーンアップ処理 -4. **自動カスケード** - `init`宣言順で未処理フィールドを解放 +4. **自動カスケード** - strong-owned フィールドを決定的順序で解放(weakはスキップ) 5. **フィールドクリア** - 全フィールドを無効化 6. **finalized設定** - 以後の使用を禁止 @@ -102,25 +107,26 @@ box Pipeline { ```nyash box Node { - init { id, weak next } // 'next'は弱参照 + init { id, next_weak } // 弱参照は値として保持する(`weak(x)`) } local node1 = new Node("A", null) -local node2 = new Node("B", node1) // node2はnode1への弱参照を持つ -node1.next = node2 // node1はnode2への強参照を持つ +local node2 = new Node("B", null) +node2.next_weak = weak(node1) +node1.next_weak = weak(node2) // 循環参照を回避し、安全に解放される ``` #### weak参照の特性 - **所有権なし**: オブジェクトの生存期間に影響しない -- **自動nil化**: 参照先が解放されると自動的に`null`になる +- **観測はweak_to_strong**: 参照先が Dead/Freed の場合、`weak_to_strong()` は `null` を返す - **fini()対象外**: 弱参照フィールドはfini()カスケードでスキップ ### 不変条件(重要) - **weak参照**: `weak`フィールドに対して`fini()`を直接呼ぶことはできません - **finalized後禁止**: `fini()`呼び出し後は、そのオブジェクトの使用はすべて禁止 -- **カスケード順序**: `init`宣言順の**逆順**で実行、`weak`フィールドはスキップ +- **カスケード順序**: strong-owned フィールドに対して決定的に実行し、`weak`フィールドはスキップ(順序のSSOTは `docs/reference/language/lifecycle.md`)。 ## 🌟 実用例 @@ -130,7 +136,7 @@ box FileHandler { init { file, buffer } fini() { - // オブジェクト削除時に自動呼び出し + // 終了時に資源を解放(必要なら明示的に呼ぶ) if me.file != null { me.file.close() console.log("File closed automatically") @@ -177,4 +183,4 @@ box PluginResource { **関連ドキュメント**: - [Box型リファレンス](box-reference.md) - [プラグインシステム](../plugin-system/) -- [BID-FFI仕様](../plugin-system/ffi-abi-specification.md) \ No newline at end of file +- [BID-FFI仕様](../plugin-system/ffi-abi-specification.md) diff --git a/docs/reference/core-language/README.md b/docs/reference/core-language/README.md index 81b2feef..0f0e138a 100644 --- a/docs/reference/core-language/README.md +++ b/docs/reference/core-language/README.md @@ -11,7 +11,7 @@ - `design-philosophy.md` - Nyashの設計哲学 - `override-delegation-syntax.md` - オーバーライド・デリゲーション構文の詳細 -- `portability-contract.md` - 移植性に関する契約 +- `portability-contract.md` - 移植性に関する契約(アーカイブ。参照先: `docs/archive/core-language/portability-contract.md`) ## 📦 アーカイブ diff --git a/docs/reference/core-language/portability-contract.md b/docs/reference/core-language/portability-contract.md index d2c23b35..fba135ae 100644 --- a/docs/reference/core-language/portability-contract.md +++ b/docs/reference/core-language/portability-contract.md @@ -1,295 +1,8 @@ -# 🤝 Nyash Portability Contract v0 +# Portability Contract v0 (Archived) -*ChatGPT5アドバイス・全バックエンド互換性保証仕様* +This document has been archived. The canonical copy is: +- `docs/archive/core-language/portability-contract.md` -## 🎯 目的 - -**「nyash --target= interp / vm / wasm / aot-rust / jit-cranelift」で同一プログラムが同一結果を保証** - -全バックエンドでNyashプログラムが確実に動作し、最適化レベルに関係なく**決定的で予測可能な実行**を実現。 - -## 🔧 **Contract v0 仕様** - -### 1️⃣ **決定的破棄(Deterministic Finalization)** - -#### **強参照のみ伝播保証** -```rust -// ✅ 保証される動作 -box Parent { - child_strong: ChildBox // 強参照→破棄連鎖 -} - -parent.fini() // 必ずchild_strong.fini()も呼ばれる -``` - -#### **破棄順序の決定性** -```nyash -// 破棄順序: 最新→最古(スタック順序) -box Child from Parent { - init { data } - pack() { - from Parent.pack() // 1. Parent初期化 - me.data = "child" // 2. Child初期化 - } - // fini順序: 2→1(逆順破棄) -} -``` - -#### **例外安全性** -```rust -pub enum FinalizationGuarantee { - AlwaysExecuted, // fini()は例外時も必ず実行 - NoDoubleDestroy, // 同一オブジェクトの二重破棄禁止 - OrderPreserved, // 初期化と逆順での破棄保証 -} -``` - -### 2️⃣ **weak参照の非伝播+生存チェック** - -#### **非伝播保証** -```nyash -box Parent { - init { child_weak } - - pack() { - local child = new Child() - me.child_weak = weak(child) // weak参照生成 - // child がfini()されても Parent は影響なし - } -} -``` - -#### **生存チェック必須** -```mir -// MIR レベルでの生存チェック -%alive = weak_load %weak_ref -br %alive -> %use_bb, %null_bb - -%use_bb: - // weak参照が有効な場合の処理 - %value = /* weak_refの値使用 */ - jmp %continue_bb - -%null_bb: - // weak参照が無効な場合の処理 - %value = const null - jmp %continue_bb - -%continue_bb: - // 合流地点(Phi必須) - %result = phi [%value from %use_bb, %value from %null_bb] -``` - -#### **自動null化契約** -```rust -pub struct WeakContract { - auto_nullification: true, // 参照先fini()時に自動null - no_dangling_pointers: true, // ダングリングポインタ禁止 - thread_safe_access: true, // マルチスレッド安全アクセス -} -``` - -### 3️⃣ **Effect意味論(最適化可能性)** - -#### **Effect分類契約** -```rust -pub enum EffectLevel { - Pure, // 副作用なし→並び替え・除去・重複実行可能 - Mut, // メモリ変更→順序保証必要・並列化制限 - Io, // I/O操作→実行順序厳密保証・キャッシュ禁止 - Bus, // 分散通信→elision対象・ネットワーク最適化可能 -} -``` - -#### **最適化契約** -```mir -// Pure関数→最適化可能 -%result1 = call @pure_function(%arg) effects=[PURE] -%result2 = call @pure_function(%arg) effects=[PURE] -// → 最適化: %result2 = copy %result1 - -// Mut操作→順序保証 -store %value1 -> %ptr effects=[MUT] -store %value2 -> %ptr effects=[MUT] -// → 順序維持必須 - -// Bus操作→elision対象 -send %bus, %message effects=[BUS] -// → ネットワーク最適化・バッチ化可能 -``` - -### 4️⃣ **Bus-elision基盤契約** - -#### **elision ON/OFF同一結果保証** -```bash -# 最適化ON→高速実行 -nyash --elide-bus --target wasm program.hako - -# 最適化OFF→完全分散実行 -nyash --no-elide-bus --target vm program.hako - -# 結果は必ず同一(契約保証) -``` - -#### **Bus操作の意味保証** -```mir -// Bus送信の意味論 -send %bus, %message effects=[BUS] { - // elision OFF: 実際のネットワーク送信 - // elision ON: ローカル最適化(結果同一) -} - -// Bus受信の意味論 -%msg = recv %bus effects=[BUS] { - // elision OFF: ネットワーク受信待ち - // elision ON: ローカル値返却(結果同一) -} -``` - -## 🧪 **Contract検証システム** - -### **互換テストスイート** -```rust -// tests/portability_contract_tests.rs -#[test] -fn test_deterministic_finalization() { - let program = "/* fini順序テスト */"; - - let interp_result = run_interpreter(program); - let vm_result = run_vm(program); - let wasm_result = run_wasm(program); - - // 破棄順序・タイミングが全バックエンドで同一 - assert_eq!(interp_result.finalization_order, vm_result.finalization_order); - assert_eq!(vm_result.finalization_order, wasm_result.finalization_order); -} - -#[test] -fn test_weak_reference_semantics() { - let program = "/* weak参照テスト */"; - - // 生存チェック・null化が全バックエンドで同一動作 - let results = run_all_backends(program); - assert_all_equal(results.map(|r| r.weak_behavior)); -} - -#[test] -fn test_effect_optimization_equivalence() { - let program = "/* Effect最適化テスト */"; - - // PURE関数の最適化結果が同一 - let optimized = run_with_optimization(program); - let reference = run_without_optimization(program); - assert_eq!(optimized.output, reference.output); -} - -#[test] -fn test_bus_elision_equivalence() { - let program = "/* Bus通信テスト */"; - - let elision_on = run_with_flag(program, "--elide-bus"); - let elision_off = run_with_flag(program, "--no-elide-bus"); - - // Bus最適化ON/OFFで結果同一 - assert_eq!(elision_on.output, elision_off.output); -} -``` - -### **Golden Dump検証** -```bash -#!/bin/bash -# scripts/verify_portability_contract.sh - -echo "🧪 Portability Contract v0 検証中..." - -# 1. MIR出力一致検証 -nyash --dump-mir test.hako > golden.mir -nyash --dump-mir test.hako > current.mir -if ! diff golden.mir current.mir; then - echo "❌ MIR回帰エラー検出" - exit 1 -fi - -# 2. 全バックエンド同一出力 -declare -a backends=("interp" "vm" "wasm") -for backend in "${backends[@]}"; do - nyash --target $backend test.hako > ${backend}.out -done - -# 出力一致確認 -if diff interp.out vm.out && diff vm.out wasm.out; then - echo "✅ 全バックエンド出力一致" -else - echo "❌ バックエンド出力差異検出" - exit 1 -fi - -# 3. Bus-elision検証 -nyash --elide-bus test.hako > elision_on.out -nyash --no-elide-bus test.hako > elision_off.out -if diff elision_on.out elision_off.out; then - echo "✅ Bus-elision同一結果" -else - echo "❌ Bus-elision結果差異" - exit 1 -fi - -echo "🎉 Portability Contract v0 検証完了" -``` - -## 📊 **Contract適合レベル** - -### **Tier-0: 基本互換性** -- [ ] **決定的破棄**: fini()順序がバックエンド間で同一 -- [ ] **weak非伝播**: weak参照が親破棄に影響しない -- [ ] **基本Effect**: PURE/MUT/IO の意味論統一 -- [ ] **出力一致**: 同一プログラム→同一標準出力 - -### **Tier-1: 最適化互換性** -- [ ] **PURE最適化**: 純粋関数の除去・移動がバックエンド間で同等 -- [ ] **weak生存チェック**: 全バックエンドで同一タイミング -- [ ] **Bus-elision**: ON/OFF切り替えで結果同一 -- [ ] **性能予測**: 最適化レベル差が定量的 - -### **Tier-2: 高度互換性** -- [ ] **メモリレイアウト**: Box構造がバックエンド間で互換 -- [ ] **エラー処理**: 例外・パニックが同一動作 -- [ ] **並行性**: Future/awaitが同一意味論 -- [ ] **デバッグ**: スタックトレース・診断情報が同等 - -## ⚡ **実装優先順位** - -### **Phase 8.4(今すぐ)** -1. **Tier-0契約実装**: 基本互換性確保 -2. **Golden dump自動化**: CI/CDで回帰検出 -3. **Bus命令設計**: elision基盤構築 - -### **Phase 8.5(短期)** -1. **Tier-1契約実装**: 最適化互換性 -2. **性能ベンチマーク**: 契約準拠性測定 -3. **エラー契約**: 例外処理統一 - -### **Phase 9+(中長期)** -1. **Tier-2契約実装**: 高度互換性 -2. **形式検証**: 契約の数学的証明 -3. **認証システム**: 契約適合認定 - ---- - -## 🎯 **期待効果** - -### **開発者体験** -- **予測可能性**: どのバックエンドでも同一動作保証 -- **デバッグ容易性**: バックエンド切り替えで問題切り分け -- **最適化信頼性**: 高速化しても結果不変保証 - -### **Nyash言語価値** -- **差別化**: 「全バックエンド互換」言語として独自性 -- **信頼性**: エンタープライズ採用の技術的根拠 -- **拡張性**: 新バックエンド追加時の品質保証 - ---- - -*最終更新: 2025-08-14 - ChatGPT5アドバイス完全実装* - -*「Everything is Box」×「全バックエンド互換」= Nyashの技術的優位性* \ No newline at end of file +Language SSOT: +- `docs/reference/language/lifecycle.md` +- `docs/reference/language/types.md` diff --git a/docs/reference/environment-variables.md b/docs/reference/environment-variables.md index f953f63b..d4ac5054 100644 --- a/docs/reference/environment-variables.md +++ b/docs/reference/environment-variables.md @@ -251,6 +251,7 @@ NYASH_CLI_VERBOSE=2 \ | `NYASH_GC_METRICS=1` | OFF | Any | GC メトリクス (text) | | `NYASH_GC_METRICS_JSON=1` | OFF | Any | GC メトリクス (JSON) | | `NYASH_VM_TRACE=1` | OFF | Any | VM 実行トレース | +| `NYASH_LEAK_LOG={1\|2}` | OFF | Any | Exit-time leak report (Phase 285)。`1`=summary counts, `2`=verbose (names/entries) | --- diff --git a/docs/reference/language/EBNF.md b/docs/reference/language/EBNF.md index 3d9e3867..2d6bd841 100644 --- a/docs/reference/language/EBNF.md +++ b/docs/reference/language/EBNF.md @@ -72,13 +72,25 @@ This section adds a minimal grammar for Box members (a unified member model) wit ``` box_decl := 'box' IDENT '{' member* '}' -member := stored +member := visibility_block + | weak_stored + | stored | computed | once_decl | birth_once_decl | method_decl | block_as_role ; nyash-mode (block-first) equivalent +visibility_block := ( 'public' | 'private' ) '{' member* '}' + ; member visibility grouping (Phase 285A1.3). `weak` is allowed inside. + +weak_stored := 'weak' IDENT ( ':' TYPE )? + ; weak field declaration (Phase 285A1.2). Enforces WeakRef type at compile-time. + +visibility_weak_sugar := ('public'|'private') 'weak' IDENT ( ':' TYPE )? + ; sugar syntax (Phase 285A1.4). Equivalent to visibility block form. + ; e.g., `public weak parent` ≡ `public { weak parent }` + stored := IDENT ':' TYPE ( '=' expr )? ; stored property (read/write). No handlers supported. @@ -93,6 +105,10 @@ birth_once_decl:= 'birth_once' IDENT ':' TYPE ( '=>' expr | block ) handler_tail method_decl := IDENT '(' params? ')' ( ':' TYPE )? block handler_tail? +params := IDENT (',' IDENT)* + ; parameter name list (Phase 285A1.5) + ; Note: Parameter type annotations (e.g., `name: Type`) are not supported. + ; nyash-mode (block-first) variant — gated with NYASH_ENABLE_UNIFIED_MEMBERS=1 block_as_role := block 'as' ( 'once' | 'birth_once' )? IDENT ':' TYPE @@ -101,7 +117,7 @@ catch_block := 'catch' ( '(' ( IDENT IDENT | IDENT )? ')' )? block cleanup_block := 'cleanup' block ; Stage‑3 (Phase 1 via normalization gate NYASH_CATCH_NEW=1) -; Postfix handlers for expressions and calls +; Postfix handlers for expressions and calls (cleanup may appear without catch) postfix_catch := primary_expr 'catch' ( '(' ( IDENT IDENT | IDENT )? ')' )? block postfix_cleanup := primary_expr 'cleanup' block ``` @@ -120,12 +136,23 @@ Lowering (no JSON v0 change) - birth_once → add `__name: T` and insert initialization just before user `birth` in declaration order; handlers apply to each initializer - method → existing method forms; optional postfix handlers lower to try/catch/finally +## Legacy: `init { ... }` field list (compatibility) + +Some docs and older code use an `init { a, b, c }` list inside a `box` body. This is a legacy compatibility form to declare stored slots. + +Semantics (SSOT): +- `init { a, b, c }` declares **untyped stored slots** named `a`, `b`, `c` (equivalent to writing `a` / `b` / `c` as stored members without type). +- `init { weak x, weak y }` declares **weak fields** (equivalent to writing `weak x` / `weak y` as members). +- It does not execute code. Initialization logic belongs in `birth(...) { ... }` and assignments. +- **New code** should prefer the direct syntax: `weak field_name` (Phase 285A1.2) or the unified member model (`stored/computed/once/birth_once`). + - Legacy `init { weak field }` syntax still works for backward compatibility but is superseded by `weak field`. + ## Stage‑3 (Gated) Additions Enabled when `NYASH_PARSER_STAGE3=1` for the Rust parser (and via `--stage3`/`NYASH_NY_COMPILER_STAGE3=1` for the selfhost parser): - try/catch/cleanup - - `try_stmt := 'try' block ('catch' '(' (IDENT IDENT | IDENT | ε) ')' block) ('cleanup' block)?` + - `try_stmt := 'try' block ('catch' '(' (IDENT IDENT | IDENT | ε) ')' block)? ('cleanup' block)?` - MVP policy: single `catch` per `try`。 - `(Type var)` or `(var)` or `()` are accepted for the catch parameter。 diff --git a/docs/reference/language/LANGUAGE_REFERENCE_2025.md b/docs/reference/language/LANGUAGE_REFERENCE_2025.md index d48f85a9..336072b1 100644 --- a/docs/reference/language/LANGUAGE_REFERENCE_2025.md +++ b/docs/reference/language/LANGUAGE_REFERENCE_2025.md @@ -38,18 +38,18 @@ Rust製インタープリターによる高性能実行と、直感的な構文 | `override` | 明示的オーバーライド | `override speak() { }` | | `break` | ループ脱出 | `break` | | `catch` | 例外処理 | `catch (e) { }`(式/呼び出しの後置も可・Stage‑3) | -| `cleanup` | 最終処理(finally の後継) | `cleanup { }`(式/呼び出しの後置も可・Stage‑3) | +| `cleanup` | 最終処理(finally の後継) | `cleanup { }`(式/呼び出しの後置・Stage‑3。`catch` があればその後に実行) | | `throw` | 例外発生 | `throw error` | | `nowait` | 非同期実行 | `nowait future = task()` | | `await` | 待機・結果取得 | `result = await future` | | `include` | ファイル取り込み | `include "math.hako"` | | `print` | 出力(デバッグ用) | `print("Hello")` | | `function`/`fn` | 関数定義 | `fn add(a,b) { }` | -| `init` | 初期化ブロック | `init { field1, field2 }` | +| `init` | (legacy/互換)フィールド宣言(slot) | `init { field1, field2 }` | | `pack` | 旧コンストラクタ(互換性) | `pack(param) { }` | | `outbox` | 所有権移転変数 | `outbox result = compute()` | | `global` | グローバル変数 | `global CONFIG = "dev"` | -| `weak` | 弱参照修飾子 | `weak reference` | +| `weak` | 弱参照(生成) | `weak(x)` | | `using` | 名前空間インポート | `using namespace` | ### **演算子・論理** @@ -95,6 +95,16 @@ box ClassName { } ``` +注: `fini()` / strong・weak / スコープ終了 / GC の方針(cycle の扱い含む)の SSOT は `docs/reference/language/lifecycle.md`。 + +注(`init { ... }` について): +- `init { a, b, c }` は **互換のために残っているフィールド宣言(slot)**です(コード実行の「初期化ブロック」ではありません)。 +- これは「untyped な stored slot を宣言する糖衣」として扱います(例: `a` / `b` / `c` の stored を追加する)。 +- `init { weak field }` は弱フィールド宣言です(Phase 285A1.2 の直接構文 `weak field` に統一されました)。 +- 新規コードでは、可能なら以下を推奨します: + - **弱フィールド**: 直接構文 `weak field_name`(Phase 285A1.2) + - **その他**: `docs/reference/language/EBNF.md` の Unified Members(stored/computed/once/birth_once) + #### **デリゲーションBox** ```nyash box Child from Parent interface Comparable { @@ -739,6 +749,8 @@ array.map(fn(x) { x * x }) - Rust所有権システムによる完全なメモリ安全性 - Arcによるスレッドセーフな共有状態管理 - 自動参照カウント + 明示的デストラクタ(fini) + - SSOT: `docs/reference/language/lifecycle.md` + - GC(tracing/cycle collection)は意味論ではなく補助。OFFでも非循環は解放されるが、循環はリークしうる(仕様)。 ### **6.2 実行効率** - 統一されたBox型システムによる最適化 @@ -783,7 +795,6 @@ static box Main { ### **7.3 よくある間違いと対策** ```nyash # ❌ よくある間違い -public { field1 field2 } # 旧構文 → 使用不可 x = 42 # 変数未宣言 → ランタイムエラー while condition { } # 非対応構文 → パーサーエラー this.field # thisは使用不可 → me.fieldを使用 @@ -794,6 +805,10 @@ field2 # 型なしフィールド local x = 42 # 事前宣言必須 loop(condition) { } # 統一ループ構文 me.field # self参照はmeのみ + +# ✅ Weak field 構文(Phase 285A1.4対応) +public weak parent # 糖衣構文(Phase 285A1.4) +public { weak parent } # ブロック構文(Phase 285A1.3)- どちらも同義 ``` --- @@ -831,12 +846,12 @@ let [first, second, ...rest] = array 方針 - try は非推奨。postfix `catch` と `cleanup` を用いる。 - `catch` は直前の式/呼び出しで発生した例外を処理。 -- `cleanup` は常に実行(finally の後継)。 +- `cleanup` は常に実行(finally の後継)。`catch` の有無に関係なく付与できる。 例(式レベルの postfix) ``` -do_work() catch(Error e) { env.console.log(e) } -open(path) cleanup { env.console.log("close") } +do_work() cleanup { env.console.log("done") } +open(path) catch(Error e) { env.console.log(e) } cleanup { env.console.log("close") } connect(url) catch(NetworkError e) { env.console.warn(e) } cleanup { env.console.log("done") } diff --git a/docs/reference/language/README.md b/docs/reference/language/README.md index b2330d1a..15e2958c 100644 --- a/docs/reference/language/README.md +++ b/docs/reference/language/README.md @@ -18,6 +18,7 @@ Imports and namespaces Variables and scope - See: reference/language/variables-and-scope.md — Block-scoped locals, assignment resolution, and strong/weak reference guidance. +- See: reference/language/lifecycle.md — Box lifetime, ownership (strong/weak), and finalization (`fini`) SSOT. Type system (SSOT) - See: reference/language/types.md — runtime truthiness, `+`/compare/equality semantics, and the role/limits of MIR type facts. diff --git a/docs/reference/language/field-visibility-and-delegation.md b/docs/reference/language/field-visibility-and-delegation.md index 43d6b81c..24d99d71 100644 --- a/docs/reference/language/field-visibility-and-delegation.md +++ b/docs/reference/language/field-visibility-and-delegation.md @@ -2,6 +2,8 @@ 本書は Nyash 言語の「フィールド可視性」と「デリゲーション(from/override)」の設計をまとめた仕様草案です。実装は段階的に進めます。 +注: `init { ... }` は legacy のフィールド宣言(slot)です。新しい宣言モデル(stored/computed/once/birth_once)は `docs/reference/language/EBNF.md` を参照してください。 + ## 1. フィールド可視性(Blocks) - 構文 ```nyash @@ -61,4 +63,3 @@ - BoxRef/Handle 仕様: `docs/reference/plugin-system/boxref-behavior.md` - nyash.toml v2.1–v2.2: `docs/reference/plugin-system/nyash-toml-v2_1-spec.md` - 実装箇所(予定): `src/parser/declarations/box_definition.rs`, `src/core/model.rs`, `src/interpreter/expressions/access.rs`, `src/mir/*`, `src/backend/vm.rs` - diff --git a/docs/reference/language/lifecycle.md b/docs/reference/language/lifecycle.md index 055a784f..418386da 100644 --- a/docs/reference/language/lifecycle.md +++ b/docs/reference/language/lifecycle.md @@ -238,6 +238,18 @@ box Node { } ``` +**Legacy syntax** (still supported, Phase 285A1.2): +- `init { weak parent }` — old syntax; superseded by direct `weak parent` declaration +- Both syntaxes behave identically and populate the same weak_fields set +- New code should use `weak field_name` directly for clarity + +**Visibility blocks** (Phase 285A1.3): +- `weak` is allowed inside visibility blocks: `public { weak parent }` + +**Sugar syntax** (Phase 285A1.4): +- `public weak parent` is equivalent to `public { weak parent }` +- `private weak parent` is equivalent to `private { weak parent }` + ## 5) Cycles and GC (language-level policy) ### Cycles diff --git a/docs/reference/language/quick-reference.md b/docs/reference/language/quick-reference.md index a82ef845..acd93a99 100644 --- a/docs/reference/language/quick-reference.md +++ b/docs/reference/language/quick-reference.md @@ -42,6 +42,7 @@ Semicolons and ASI (Automatic Semicolon Insertion) Truthiness (boolean context) - SSOT: `reference/language/types.md`(runtime truthiness) - 実行上は `Bool/Integer/Float/String/Void` が中心。`BoxRef` は一部のコアBoxのみ許可され、その他は `TypeError`(Fail-Fast)。 +- `null` は `void` の別名(構文糖衣)。どちらも boolean context では `TypeError`。 Equality and Comparison - SSOT: `reference/language/types.md`(`==`/`!=` と `< <= > >=` の runtime 仕様) diff --git a/docs/reference/language/types.md b/docs/reference/language/types.md index ec9d61d6..4b3e40fa 100644 --- a/docs/reference/language/types.md +++ b/docs/reference/language/types.md @@ -23,6 +23,19 @@ Terminology (SSOT): - **Runtime type**: what the VM executes on (`VMValue`). - **MIR type facts**: builder annotations (`MirType`, `value_types`, `value_origin_newbox`, `TypeCertainty`). +### Null vs Void (SSOT) + +Nyash has two surface literals: `null` and `void`. + +SSOT policy: +- `null` is the source-level “none” literal used in APIs like `toIntOrNull()` and optional returns. +- `void` is the “no value” literal (and is also the value produced by expressions/statements that do not yield a value). +- At runtime, both are represented as the same “no value” concept (`Void`). Treat `null` as a syntax-level alias of `void` unless a backend explicitly documents a difference (differences are bugs). + +Practical consequence: +- `x == null` and `x == void` are equivalent checks. +- `WeakRef.weak_to_strong()` returns `null` on failure (i.e., `void` / none). + --- ## 2. Variables and Re-assignment @@ -49,7 +62,7 @@ Runtime rule (SSOT) is implemented by `to_bool_vm` (`src/backend/abi_util.rs`): - `Integer` → `0` is false; non-zero is true - `Float` → `0.0` is false; non-zero is true - `String` → empty string is false; otherwise true -- `Void` → **TypeError** (fail-fast) +- `Void` (`null` / `void`) → **TypeError** (fail-fast) - `BoxRef`: - bridge boxes only: - `BoolBox` / `IntegerBox` / `StringBox` are unboxed and coerced like their primitive equivalents diff --git a/docs/reference/language/variables-and-scope.md b/docs/reference/language/variables-and-scope.md index 46a78e67..464d43b1 100644 --- a/docs/reference/language/variables-and-scope.md +++ b/docs/reference/language/variables-and-scope.md @@ -4,13 +4,15 @@ Status: Stable (Stage‑3 surface for `local`), default strong references. This document defines the variable model used by Hakorune/Nyash and clarifies how locals interact with blocks, memory, and references across VMs (Rust VM, Hakorune VM, LLVM harness). +For the lifecycle/finalization SSOT, see: `docs/reference/language/lifecycle.md`. + ## Local Variables - Syntax: `local name = expr` - Scope: Block‑scoped. The variable is visible from its declaration to the end of the lexical block. - Redeclaration: Writing `local name = ...` inside a nested block creates a new shadowing binding. Writing `name = ...` without `local` updates the nearest existing binding in an enclosing scope. - Mutability: Locals are mutable unless future keywords specify otherwise (e.g., `const`). -- Lifetime: The variable binding is dropped at block end; any referenced objects live as long as at least one strong reference exists elsewhere. +- Lifetime: The variable binding is dropped at block end (`}`); object lifetime/finalization is defined separately in `docs/reference/language/lifecycle.md`. Notes: - Stage‑3 gate: Parsing `local` requires Stage‑3 to be enabled (`NYASH_PARSER_STAGE3=1` or equivalent runner profile). @@ -27,8 +29,8 @@ This matches intuitive block‑scoped semantics (Lua‑like), and differs from P ## Reference Semantics (Strong/Weak) -- Default: Locals hold strong references to boxes/collections. Implementation uses reference counting (strong = ownership) with internal synchronization. -- Weak references: Use `WeakBox` to hold a non‑owning (weak) reference. Weak refs do not keep the object alive; they can be upgraded to strong at use sites. Intended for back‑pointers and cache‑like links to avoid cycles. +- Default: Locals hold strong references to boxes/collections. +- Weak references: Use `weak(x)` (and fields that store `WeakRef`) to hold a non‑owning reference. Weak refs do not keep the object alive; they can be upgraded at use sites (see SSOT: `docs/reference/language/lifecycle.md`). - Typical guidance: - Locals and return values: strong references. - Object fields that create cycles (child→parent): weak references. diff --git a/docs/reference/plugin-system/plugin-system.md b/docs/reference/plugin-system/plugin-system.md index ebf3048c..33b81c5a 100644 --- a/docs/reference/plugin-system/plugin-system.md +++ b/docs/reference/plugin-system/plugin-system.md @@ -343,14 +343,16 @@ i32 filebox_read(u32 instance_id, i32 size, u8** result, size_t* result_len) { 3. **ライフサイクル保証** - `birth()` → 各メソッド呼び出し → `fini()` の順序を保証 - - `fini()`は必ず呼ばれる(GC時またはプログラム終了時) - - 循環参照による`fini()`遅延に注意 + - `fini()` は論理的終了(use-after-fini禁止)。自動呼び出しは実行経路/所有形態に依存しうるため、必要な資源(fd/socket等)は明示 `fini()` / `shutdown_plugins_v2()` で確実に解放する + - 循環参照や共有(複数スコープに跨る参照)では `fini()` タイミングが遅延/未観測になりうるため、weak/singleton/明示finiで設計する + - SSOT: `docs/reference/language/lifecycle.md` ### Nyash側の実装 ```rust impl Drop for PluginBox { fn drop(&mut self) { - // Boxが破棄される時、必ずfiniを呼ぶ + // 破棄時の best-effort cleanup(実行経路/所有形態によりタイミングは変わりうる)。 + // 言語仕様としては `fini()` は明示的に呼ぶ/`shutdown_plugins_v2()` で閉じるのが推奨。 let result = self.plugin.invoke( self.handle.type_id, FINI_METHOD_ID, // 最大値のmethod_id @@ -434,4 +436,4 @@ HTTPServerBox = "http-plugin" # SocketBoxが使えない! - [BID-FFI仕様](./ffi-abi-specification.md) - [Everything is Box哲学](./everything-is-box.md) -- [実装タスク](../../../予定/native-plan/issues/phase_9_75g_0_chatgpt_enhanced_final.md) \ No newline at end of file +- [実装タスク](../../../予定/native-plan/issues/phase_9_75g_0_chatgpt_enhanced_final.md) diff --git a/docs/reference/plugin-system/plugin_lifecycle.md b/docs/reference/plugin-system/plugin_lifecycle.md index dc685cf5..b4243dbb 100644 --- a/docs/reference/plugin-system/plugin_lifecycle.md +++ b/docs/reference/plugin-system/plugin_lifecycle.md @@ -6,13 +6,14 @@ NyashのBoxには「ユーザー定義Box」「ビルトインBox」「プラグインBox」があります。いずれもRAII(取得した資源は所有者の寿命で解放)に従いますが、プラグインBoxは共有やシングルトン運用があるため、追加ルールがあります。 ## 共通ライフサイクル(ユーザー/ビルトイン/プラグイン) -- インスタンスの寿命が尽きると、強参照フィールド(public/private)に対し順に `fini()` が呼ばれ解放(weak はスキップ) -- `local` 変数のスコープを抜けると、そのスコープで生成されたインスタンスは解放対象 -- 明示的に `fini()` が呼ばれた場合も同様に後処理を実施 +- `fini()` は論理的な終了(use-after-fini禁止)であり、外部資源(fd/socket/native handle など)を決定的に解放するための SSOT です。 +- `local` のスコープを抜けると、その binding は drop されます(= その binding が保持していた strong 参照が 1 つ減る)。 + - その時点で「最後の strong 参照」になれば物理的な解放が起きますが、タイミングは実装依存です。 +- 共有・循環参照がありうるため、スコープ終了“だけ”に `fini()` を期待しないでください。必要な資源は `fini()` / `cleanup` / `shutdown_plugins_v2()` で明示的に閉じます。 補足: -- これらは Nyash のスコープトラッカにより実施されます -- 解放順は生成の逆順(LIFO)で、カスケード `fini` を保証します +- 言語レベルの SSOT は `docs/reference/language/lifecycle.md` を参照してください(スコープ/所有/weak/`fini`/GC)。 +- `fini()` の中で「strong-owned フィールドを順に `fini()`」するカスケード設計は有用ですが、最終的な順序や禁止事項は SSOT に従います。 ## プラグインBoxの特則(シングルトン) - シングルトン(`nyash.toml`) @@ -21,8 +22,8 @@ NyashのBoxには「ユーザー定義Box」「ビルトインBox」「プラグ - シャットダウン時(`shutdown_plugins_v2()` など)に一括 `fini()` されます 補足: -- Nyashは参照カウントを採用しません。解放は「スコープ終了」または「明示的`fini`」のみで決まります(自己責任モデル)。 -- プラグインBoxも同じルールです。スコープ終了時に`fini`され、以後の利用はエラー(Use after fini)。 +- Nyashの実装は Box 値を参照(共有)として扱います。物理的な生存は strong 参照の有無に依存しうる一方、`fini()` は論理的な終了(use-after-fini禁止)です。 +- プラグインBoxも同じルールです。`fini` 後の利用はエラー(Use after fini)。 - 長寿命が必要なケースは「シングルトン」で運用してください(個別のBoxに特例は設けない)。 ### 例: `nyash.toml` 抜粋 diff --git a/docs/reference/testing-quality/golden-dump-testing.md b/docs/reference/testing-quality/golden-dump-testing.md index 6b127d17..895f8b1d 100644 --- a/docs/reference/testing-quality/golden-dump-testing.md +++ b/docs/reference/testing-quality/golden-dump-testing.md @@ -89,20 +89,21 @@ fn test_weak_reference_mir_stability() { let source = r#" box Parent { init { child_weak } } box Child { init { data } } - + static box Main { main() { local parent = new Parent() local child = new Child(42) parent.child_weak = weak(child) - - if parent.child_weak.isAlive() { - print(parent.child_weak.get().data) + + local c = parent.child_weak.weak_to_strong() + if c != null { + print(c.data) } } } "#; - + verify_mir_golden("weak_reference", source); } ``` @@ -404,4 +405,4 @@ fi *最終更新: 2025-08-14 - ChatGPT5推奨3点セット完成* -*Golden Dump Testing = Nyash品質保証の技術的基盤* \ No newline at end of file +*Golden Dump Testing = Nyash品質保証の技術的基盤* diff --git a/src/backend/abi_util.rs b/src/backend/abi_util.rs index ded70d93..dced870d 100644 --- a/src/backend/abi_util.rs +++ b/src/backend/abi_util.rs @@ -38,6 +38,8 @@ pub fn to_bool_vm(v: &VMValue) -> Result { Err(format!("cannot coerce BoxRef({}) to bool", b.type_name())) } VMValue::Future(_) => Err("cannot coerce Future to bool".to_string()), + // Phase 285A0: WeakRef in boolean context is TypeError + VMValue::WeakBox(_) => Err("WeakRef in boolean context - use upgrade() first".to_string()), } } @@ -80,6 +82,16 @@ pub fn eq_vm(a: &VMValue, b: &VMValue) -> bool { .downcast_ref::() .is_some() } + // Phase 285A0: WeakBox equality + (WeakBox(wa), WeakBox(wb)) => { + match (wa.upgrade(), wb.upgrade()) { + (Some(arc_a), Some(arc_b)) => Arc::ptr_eq(&arc_a, &arc_b), + (None, None) => true, // Both dropped + _ => false, + } + } + // WeakBox == Void when dropped + (WeakBox(w), Void) | (Void, WeakBox(w)) => w.upgrade().is_none(), _ => false, } } @@ -94,6 +106,7 @@ pub fn tag_of_vm(v: &VMValue) -> &'static str { VMValue::Future(_) => "Future", VMValue::Void => "Void", VMValue::BoxRef(_) => "BoxRef", + VMValue::WeakBox(_) => "WeakRef", // Phase 285A0 } } diff --git a/src/backend/mir_interpreter/handlers/boxes.rs b/src/backend/mir_interpreter/handlers/boxes.rs index e1293d9a..3f446f41 100644 --- a/src/backend/mir_interpreter/handlers/boxes.rs +++ b/src/backend/mir_interpreter/handlers/boxes.rs @@ -173,6 +173,7 @@ impl MirInterpreter { VMValue::Bool(_) => "BoolBox".to_string(), VMValue::Void => "".to_string(), VMValue::Future(_) => "".to_string(), + VMValue::WeakBox(_) => "".to_string(), // Phase 285A0 }; self.box_trace_emit_call(&cls, method, args.len()); } @@ -190,6 +191,7 @@ impl MirInterpreter { VMValue::String(_) => "String".to_string(), VMValue::Void => "Void".to_string(), VMValue::Future(_) => "Future".to_string(), + VMValue::WeakBox(_) => "WeakRef".to_string(), // Phase 285A0 }; eprintln!("[vm-trace] length dispatch recv_type={}", type_name); } diff --git a/src/backend/mir_interpreter/handlers/boxes_object_fields.rs b/src/backend/mir_interpreter/handlers/boxes_object_fields.rs index 4c0d9fe2..0326ad89 100644 --- a/src/backend/mir_interpreter/handlers/boxes_object_fields.rs +++ b/src/backend/mir_interpreter/handlers/boxes_object_fields.rs @@ -19,6 +19,7 @@ pub(super) fn try_handle_object_fields( VV::Void => NV::Void, VV::Future(_) => NV::Void, // not expected in fields VV::BoxRef(_) => NV::Void, // store minimal; complex object fields are not required here + VV::WeakBox(_) => NV::Void, // Phase 285A0: WeakBox not expected in this context } } fn nv_to_vm(v: &crate::value::NyashValue) -> VMValue { @@ -94,6 +95,7 @@ pub(super) fn try_handle_object_fields( Ok(VMValue::String(_)) => "String".to_string(), Ok(VMValue::Void) => "Void".to_string(), Ok(VMValue::Future(_)) => "Future".to_string(), + Ok(VMValue::WeakBox(_)) => "WeakRef".to_string(), // Phase 285A0 Err(_) => "".to_string(), }; eprintln!("[vm-trace] getField recv_kind={}", rk); @@ -229,6 +231,7 @@ pub(super) fn try_handle_object_fields( VMValue::BoxRef(_) => "BoxRef", VMValue::Void => "Void", VMValue::Future(_) => "Future", + VMValue::WeakBox(_) => "WeakRef", // Phase 285A0 }; this.box_trace_emit_get(&inst.class_name, &fname, kind); } @@ -339,6 +342,7 @@ pub(super) fn try_handle_object_fields( VMValue::BoxRef(b) => b.type_name(), VMValue::Void => "Void", VMValue::Future(_) => "Future", + VMValue::WeakBox(_) => "WeakRef", // Phase 285A0 }; // class name unknown here; use receiver type name if possible let cls = match this.reg_load(actual_box_val).unwrap_or(VMValue::Void) { @@ -438,6 +442,7 @@ pub(super) fn try_handle_object_fields( VMValue::BoxRef(b) => b.type_name(), VMValue::Void => "Void", VMValue::Future(_) => "Future", + VMValue::WeakBox(_) => "WeakRef", // Phase 285A0 }; let cls = match this.reg_load(actual_box_val).unwrap_or(VMValue::Void) { VMValue::BoxRef(b) => { diff --git a/src/backend/mir_interpreter/handlers/calls/method.rs b/src/backend/mir_interpreter/handlers/calls/method.rs index b9094942..22156024 100644 --- a/src/backend/mir_interpreter/handlers/calls/method.rs +++ b/src/backend/mir_interpreter/handlers/calls/method.rs @@ -458,6 +458,7 @@ impl MirInterpreter { VMValue::Void => "Void", VMValue::Future(_) => "Future", VMValue::BoxRef(bx) => bx.type_name(), + VMValue::WeakBox(_) => "WeakRef", // Phase 285A0 }; // 2. Lookup type in TypeRegistry and get slot diff --git a/src/backend/mir_interpreter/handlers/mod.rs b/src/backend/mir_interpreter/handlers/mod.rs index 62e7b50d..ed259ee2 100644 --- a/src/backend/mir_interpreter/handlers/mod.rs +++ b/src/backend/mir_interpreter/handlers/mod.rs @@ -24,6 +24,7 @@ mod externals; mod memory; mod misc; mod type_ops; +mod weak; // Phase 285A0: WeakRef handlers impl MirInterpreter { pub(super) fn execute_instruction(&mut self, inst: &MirInstruction) -> Result<(), VMError> { @@ -118,6 +119,17 @@ impl MirInterpreter { }; self.regs.insert(*dst, selected_val); } + // Phase 285A0: WeakRef handlers (delegated to weak.rs) + MirInstruction::WeakNew { dst, box_val } => { + self.handle_weak_new(*dst, *box_val)?; + } + MirInstruction::WeakLoad { dst, weak_ref } => { + self.handle_weak_load(*dst, *weak_ref)?; + } + MirInstruction::WeakRef { dst, op, value } => match op { + WeakRefOp::New => self.handle_weak_new(*dst, *value)?, + WeakRefOp::Load => self.handle_weak_load(*dst, *value)?, + } MirInstruction::BarrierRead { .. } | MirInstruction::BarrierWrite { .. } | MirInstruction::Barrier { .. } diff --git a/src/backend/mir_interpreter/handlers/weak.rs b/src/backend/mir_interpreter/handlers/weak.rs new file mode 100644 index 00000000..6e0035aa --- /dev/null +++ b/src/backend/mir_interpreter/handlers/weak.rs @@ -0,0 +1,59 @@ +//! Phase 285A0: WeakRef handlers - 弱参照の作成とアップグレード +//! +//! SSOT: docs/reference/language/lifecycle.md:179 +//! +//! WeakRef は強参照サイクルを避けるための非所有参照です。 +//! - `weak(x)` → WeakNew: BoxRef から WeakRef を作成 +//! - `w.weak_to_strong()` → WeakLoad: WeakRef から BoxRef へアップグレード(失敗時は null/Void) + +use super::*; + +impl MirInterpreter { + /// WeakNew: BoxRef → WeakRef 変換 + /// + /// # Arguments + /// * `dst` - 結果を格納する ValueId + /// * `box_val` - 変換元の Box ValueId + /// + /// # Returns + /// * `Result<(), VMError>` - 成功時は Ok、失敗時は Err + /// + /// # Errors + /// * `box_val` が BoxRef でない場合はエラー + pub(super) fn handle_weak_new( + &mut self, + dst: ValueId, + box_val: ValueId, + ) -> Result<(), VMError> { + let box_value = self.reg_load(box_val)?; + let weak_value = box_value + .downgrade_to_weak() + .ok_or_else(|| self.err_invalid("WeakNew: target is not a Box"))?; + self.regs.insert(dst, weak_value); + Ok(()) + } + + /// WeakLoad: WeakRef → BoxRef | null (= Void) アップグレード + /// + /// # Arguments + /// * `dst` - 結果を格納する ValueId + /// * `weak_ref` - WeakRef ValueId + /// + /// # Returns + /// * `Result<(), VMError>` - 成功時は Ok、失敗時は Err + /// + /// # Note + /// - SSOT: upgrade failure returns null (= Void in VM) - lifecycle.md:179 + /// - ターゲットが既に drop された場合や Dead 状態の場合は Void を返す + pub(super) fn handle_weak_load( + &mut self, + dst: ValueId, + weak_ref: ValueId, + ) -> Result<(), VMError> { + let weak_value = self.reg_load(weak_ref)?; + // upgrade_weak() が None を返した場合は Void(null) + let result = weak_value.upgrade_weak().unwrap_or(VMValue::Void); + self.regs.insert(dst, result); + Ok(()) + } +} diff --git a/src/backend/mir_interpreter/helpers.rs b/src/backend/mir_interpreter/helpers.rs index 5fedeb64..94c04807 100644 --- a/src/backend/mir_interpreter/helpers.rs +++ b/src/backend/mir_interpreter/helpers.rs @@ -436,6 +436,7 @@ impl MirInterpreter { ("BoxRef", b.type_name().to_string(), tag) } } + VMValue::WeakBox(_) => ("WeakRef", "".to_string(), None), // Phase 285A0 }; if let Some(tag) = nullish { eprintln!( diff --git a/src/backend/mir_interpreter/mod.rs b/src/backend/mir_interpreter/mod.rs index f0edfb5b..ec631a2d 100644 --- a/src/backend/mir_interpreter/mod.rs +++ b/src/backend/mir_interpreter/mod.rs @@ -14,7 +14,7 @@ pub(super) use crate::backend::abi_util::{eq_vm, to_bool_vm}; pub(super) use crate::backend::vm::{VMError, VMValue}; pub(super) use crate::mir::{ BasicBlockId, BinaryOp, Callee, CompareOp, ConstValue, MirFunction, MirInstruction, MirModule, - MirType, TypeOpKind, ValueId, + MirType, TypeOpKind, ValueId, WeakRefOp, }; mod exec; diff --git a/src/backend/mir_interpreter/utils/conversion_helpers.rs b/src/backend/mir_interpreter/utils/conversion_helpers.rs index 708fc8ec..7a73bdc9 100644 --- a/src/backend/mir_interpreter/utils/conversion_helpers.rs +++ b/src/backend/mir_interpreter/utils/conversion_helpers.rs @@ -61,6 +61,7 @@ impl MirInterpreter { )) } VMValue::Future(_) => "Future", + VMValue::WeakBox(_) => "WeakRef", // Phase 285A0 }; Err(self.err_type_mismatch("load_as_int", "Integer", type_name)) } @@ -93,6 +94,7 @@ impl MirInterpreter { return Err(self.err_type_mismatch("load_as_bool", "Bool", &b.type_name())) } VMValue::Future(_) => "Future", + VMValue::WeakBox(_) => "WeakRef", // Phase 285A0 }; Err(self.err_type_mismatch("load_as_bool", "Bool", type_name)) } diff --git a/src/backend/vm_types.rs b/src/backend/vm_types.rs index b6ea191b..cd5ba265 100644 --- a/src/backend/vm_types.rs +++ b/src/backend/vm_types.rs @@ -8,7 +8,7 @@ use crate::ast::Span; use crate::box_trait::{BoolBox, IntegerBox, NyashBox, StringBox, VoidBox}; use crate::mir::{BasicBlockId, ConstValue}; -use std::sync::Arc; +use std::sync::{Arc, Weak}; /// VM execution error #[derive(Debug)] @@ -90,7 +90,7 @@ impl std::fmt::Display for VMError { impl std::error::Error for VMError {} /// VM value representation -#[derive(Debug, Clone)] +#[derive(Clone)] pub enum VMValue { Integer(i64), Float(f64), @@ -99,6 +99,30 @@ pub enum VMValue { Future(crate::boxes::future::FutureBox), Void, BoxRef(Arc), + /// Phase 285A0: Weak reference to a Box (non-owning) + WeakBox(Weak), +} + +// Manual Debug implementation for WeakBox +impl std::fmt::Debug for VMValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + VMValue::Integer(i) => write!(f, "Integer({})", i), + VMValue::Float(v) => write!(f, "Float({})", v), + VMValue::Bool(b) => write!(f, "Bool({})", b), + VMValue::String(s) => write!(f, "String({:?})", s), + VMValue::Future(_) => write!(f, "Future(...)"), + VMValue::Void => write!(f, "Void"), + VMValue::BoxRef(arc) => write!(f, "BoxRef({})", arc.type_name()), + VMValue::WeakBox(weak) => { + if weak.upgrade().is_some() { + write!(f, "WeakBox(alive)") + } else { + write!(f, "WeakBox(dropped)") + } + } + } + } } // Manual PartialEq implementation to avoid requiring PartialEq on FutureBox @@ -112,6 +136,14 @@ impl PartialEq for VMValue { (VMValue::Void, VMValue::Void) => true, (VMValue::Future(_), VMValue::Future(_)) => false, (VMValue::BoxRef(_), VMValue::BoxRef(_)) => false, + // Phase 285A0: WeakBox equality (compare by pointer if both alive) + (VMValue::WeakBox(a), VMValue::WeakBox(b)) => { + match (a.upgrade(), b.upgrade()) { + (Some(arc_a), Some(arc_b)) => Arc::ptr_eq(&arc_a, &arc_b), + (None, None) => true, // Both dropped + _ => false, + } + } _ => false, } } @@ -128,6 +160,14 @@ impl VMValue { VMValue::Future(f) => Box::new(f.clone()), VMValue::Void => Box::new(VoidBox::new()), VMValue::BoxRef(arc_box) => arc_box.share_box(), + VMValue::WeakBox(weak) => { + // Upgrade or return void if dropped + if let Some(arc) = weak.upgrade() { + arc.share_box() + } else { + Box::new(VoidBox::new()) + } + } } } @@ -141,6 +181,32 @@ impl VMValue { VMValue::Future(f) => f.to_string_box().value, VMValue::Void => "void".to_string(), VMValue::BoxRef(arc_box) => arc_box.to_string_box().value, + VMValue::WeakBox(weak) => { + if weak.upgrade().is_some() { + "WeakRef(alive)".to_string() + } else { + "WeakRef(dropped)".to_string() + } + } + } + } + + /// Phase 285A0: Downgrade a strong BoxRef to a weak reference + /// Returns None if not a BoxRef + pub fn downgrade_to_weak(&self) -> Option { + match self { + VMValue::BoxRef(arc) => Some(VMValue::WeakBox(Arc::downgrade(arc))), + _ => None, + } + } + + /// Phase 285A0: Upgrade a weak reference to a strong BoxRef + /// Returns Some(BoxRef) if target is alive, None if dropped + pub fn upgrade_weak(&self) -> Option { + match self { + VMValue::WeakBox(weak) => weak.upgrade().map(VMValue::BoxRef), + // Non-weak values: return self (already strong) + _ => Some(self.clone()), } } @@ -186,6 +252,10 @@ impl VMValue { VMValue::Float(f) => Ok(*f != 0.0), VMValue::String(s) => Ok(!s.is_empty()), VMValue::Future(_) => Ok(true), + // Phase 285A0: WeakBox truthiness is TypeError (SSOT: types.md:26) + VMValue::WeakBox(_) => Err(VMError::TypeError( + "WeakRef cannot be used in boolean context; use upgrade() first".to_string(), + )), } } diff --git a/src/mir/builder/calls/build.rs b/src/mir/builder/calls/build.rs index 2b56bdb8..dc8cbb83 100644 --- a/src/mir/builder/calls/build.rs +++ b/src/mir/builder/calls/build.rs @@ -53,7 +53,13 @@ impl MirBuilder { return self.build_str_normalization(arg_values[0]); } - // 5. Determine call route (unified vs legacy) + // 5. Phase 285A0: weak(x) → WeakNew lowering + // SSOT: docs/reference/language/lifecycle.md:171 - weak(x) returns WeakRef + if name == "weak" && arg_values.len() == 1 { + return self.emit_weak_new(arg_values[0]); + } + + // 6. Determine call route (unified vs legacy) let use_unified = super::call_unified::is_unified_call_enabled() && (super::super::call_resolution::is_builtin_function(&name) || super::super::call_resolution::is_extern_function(&name)); diff --git a/src/mir/builder/control_flow/joinir/patterns/extractors/pattern3.rs b/src/mir/builder/control_flow/joinir/patterns/extractors/pattern3.rs index c3cfa80b..a3451fad 100644 --- a/src/mir/builder/control_flow/joinir/patterns/extractors/pattern3.rs +++ b/src/mir/builder/control_flow/joinir/patterns/extractors/pattern3.rs @@ -208,7 +208,7 @@ fn has_control_flow_recursive_p3(node: &ASTNode) -> bool { #[cfg(test)] mod tests { use super::*; - use crate::ast::{LiteralValue, Span}; + use crate::ast::{BinaryOperator, LiteralValue, Span}; #[test] fn test_extract_if_phi_success() { diff --git a/src/mir/builder/method_call_handlers.rs b/src/mir/builder/method_call_handlers.rs index 9c58e27c..7491c272 100644 --- a/src/mir/builder/method_call_handlers.rs +++ b/src/mir/builder/method_call_handlers.rs @@ -200,6 +200,17 @@ impl MirBuilder { method: String, arguments: &[ASTNode], ) -> Result { + // Phase 285A0.1: WeakRef.weak_to_strong() → WeakLoad + // SSOT: docs/reference/language/lifecycle.md:179 - weak_to_strong() returns Box | null + if method == "weak_to_strong" && arguments.is_empty() { + return self.emit_weak_load(object_value); + } + + // Phase 285A0.1: upgrade() is deprecated - Fail-Fast + if method == "upgrade" && arguments.is_empty() { + return Err("WeakRef uses weak_to_strong(), not upgrade()".to_string()); + } + // Build argument values let mut arg_values = Vec::new(); for arg in arguments { diff --git a/src/mir/join_ir_ops.rs b/src/mir/join_ir_ops.rs index 790e6e97..da417a92 100644 --- a/src/mir/join_ir_ops.rs +++ b/src/mir/join_ir_ops.rs @@ -340,6 +340,14 @@ impl JoinValue { crate::backend::VMValue::Future(_) => { Err(JoinIrOpError::new("Future not supported in JoinValue")) } + // Phase 285A0: Upgrade WeakBox before conversion + crate::backend::VMValue::WeakBox(w) => { + if let Some(arc) = w.upgrade() { + Ok(JoinValue::BoxRef(arc)) + } else { + Ok(JoinValue::Unit) // Dropped weak → Unit (null) + } + } } } } diff --git a/src/parser/common.rs b/src/parser/common/mod.rs similarity index 99% rename from src/parser/common.rs rename to src/parser/common/mod.rs index 033c87a4..3577ab7e 100644 --- a/src/parser/common.rs +++ b/src/parser/common/mod.rs @@ -5,6 +5,8 @@ * Extracted from parser/mod.rs as part of modularization */ +pub(crate) mod params; + use super::ParseError; use crate::ast::Span; use crate::tokenizer::{Token, TokenType}; diff --git a/src/parser/common/params.rs b/src/parser/common/params.rs new file mode 100644 index 00000000..d138e134 --- /dev/null +++ b/src/parser/common/params.rs @@ -0,0 +1,92 @@ +/*! + * Parameter Parsing Utilities + * + * Phase 285A1.5: Common helper for parameter name list parsing + * Prevents infinite parser hangs on unsupported parameter type annotations + */ + +use super::ParserUtils; +use crate::parser::{NyashParser, ParseError}; +use crate::tokenizer::TokenType; + +/// Parse parameter name list with Fail-Fast on unexpected tokens +/// +/// Parses: IDENT (',' IDENT)* +/// Rejects: Type annotations, unexpected tokens, malformed comma sequences +/// +/// # Arguments +/// * `p` - Mutable reference to NyashParser +/// * `context` - Context string for error messages ("method", "constructor", "function", etc.) +/// +/// # Returns +/// * `Ok(Vec)` - List of parameter names +/// * `Err(ParseError)` - Parse error with context-aware message +/// +/// # Features +/// * Progress-zero detection: Tracks token position, errors if stuck +/// * Explicit token handling: All token types explicitly matched +/// * Fail-Fast: Either advances or errors (no infinite loop possible) +/// * Unified error messages: Single source of truth for error text +pub(crate) fn parse_param_name_list( + p: &mut NyashParser, + context: &str, +) -> Result, ParseError> { + let mut params = Vec::new(); + let mut last_token_position: Option<(usize, usize)> = None; + + while !p.match_token(&TokenType::RPAREN) && !p.is_at_end() { + // Progress-zero detection: same position twice → infinite loop risk + let current_position = p.current_position(); + if let Some(last_pos) = last_token_position { + if current_position == last_pos { + return Err(ParseError::InfiniteLoop { + location: format!("{} parameter list", context), + token: p.current_token().token_type.clone(), + line: p.current_token().line, + }); + } + } + last_token_position = Some(current_position); + + match &p.current_token().token_type { + TokenType::IDENTIFIER(param) => { + params.push(param.clone()); + p.advance(); + + if p.match_token(&TokenType::COMMA) { + p.advance(); + } else if !p.match_token(&TokenType::RPAREN) { + return Err(ParseError::UnexpectedToken { + found: p.current_token().token_type.clone(), + expected: format!( + "',' or ')' in {} parameter list. \ + Note: Parameter type annotations are not supported.", + context + ), + line: p.current_token().line, + }); + } + } + TokenType::COMMA => { + return Err(ParseError::UnexpectedToken { + found: TokenType::COMMA, + expected: format!("parameter name in {} parameter list", context), + line: p.current_token().line, + }); + } + _ => { + return Err(ParseError::UnexpectedToken { + found: p.current_token().token_type.clone(), + expected: format!( + "parameter name or ')' in {} parameter list. \ + Note: Parameter type annotations are not supported.", + context + ), + line: p.current_token().line, + }); + } + } + } + + Ok(params) +} diff --git a/src/parser/declarations/box_def/members/constructors.rs b/src/parser/declarations/box_def/members/constructors.rs index 3db02a55..74bb7d59 100644 --- a/src/parser/declarations/box_def/members/constructors.rs +++ b/src/parser/declarations/box_def/members/constructors.rs @@ -23,17 +23,10 @@ pub(crate) fn try_parse_constructor( let name = "init".to_string(); p.advance(); // consume 'init' p.consume(TokenType::LPAREN)?; - let mut params = Vec::new(); - while !p.match_token(&TokenType::RPAREN) && !p.is_at_end() { - crate::must_advance!(p, _unused, "constructor parameter parsing"); - if let TokenType::IDENTIFIER(param) = &p.current_token().token_type { - params.push(param.clone()); - p.advance(); - } - if p.match_token(&TokenType::COMMA) { - p.advance(); - } - } + + // Phase 285A1.5: Use shared helper to prevent parser hangs on unsupported type annotations + let params = crate::parser::common::params::parse_param_name_list(p, "constructor (init)")?; + p.consume(TokenType::RPAREN)?; let mut body = p.parse_block_statements()?; // Optional postfix catch/cleanup (method-level gate) @@ -97,17 +90,10 @@ pub(crate) fn try_parse_constructor( let name = "pack".to_string(); p.advance(); // consume 'pack' p.consume(TokenType::LPAREN)?; - let mut params = Vec::new(); - while !p.match_token(&TokenType::RPAREN) && !p.is_at_end() { - crate::must_advance!(p, _unused, "pack parameter parsing"); - if let TokenType::IDENTIFIER(param) = &p.current_token().token_type { - params.push(param.clone()); - p.advance(); - } - if p.match_token(&TokenType::COMMA) { - p.advance(); - } - } + + // Phase 285A1.5: Use shared helper to prevent parser hangs on unsupported type annotations + let params = crate::parser::common::params::parse_param_name_list(p, "constructor (pack)")?; + p.consume(TokenType::RPAREN)?; let body = p.parse_block_statements()?; let node = ASTNode::FunctionDeclaration { @@ -134,17 +120,10 @@ pub(crate) fn try_parse_constructor( let name = "birth".to_string(); p.advance(); // consume 'birth' p.consume(TokenType::LPAREN)?; - let mut params = Vec::new(); - while !p.match_token(&TokenType::RPAREN) && !p.is_at_end() { - crate::must_advance!(p, _unused, "birth parameter parsing"); - if let TokenType::IDENTIFIER(param) = &p.current_token().token_type { - params.push(param.clone()); - p.advance(); - } - if p.match_token(&TokenType::COMMA) { - p.advance(); - } - } + + // Phase 285A1.5: Use shared helper to prevent parser hangs on unsupported type annotations + let params = crate::parser::common::params::parse_param_name_list(p, "constructor (birth)")?; + p.consume(TokenType::RPAREN)?; let body = p.parse_block_statements()?; let node = ASTNode::FunctionDeclaration { diff --git a/src/parser/declarations/box_def/members/fields.rs b/src/parser/declarations/box_def/members/fields.rs index a8242b49..36b32314 100644 --- a/src/parser/declarations/box_def/members/fields.rs +++ b/src/parser/declarations/box_def/members/fields.rs @@ -151,6 +151,35 @@ pub(crate) fn try_parse_visibility_block_or_single( p.consume(TokenType::RBRACE)?; return Ok(true); } + // Phase 285A1.4: Sugar syntax - public weak parent, private weak parent + if p.match_token(&TokenType::WEAK) { + p.advance(); // consume WEAK only + + // Read field name (reuse existing pattern) + if let TokenType::IDENTIFIER(fname) = &p.current_token().token_type { + let fname = fname.clone(); + p.advance(); // consume IDENTIFIER + + // Delegate to existing weak field parser (handles type annotation, etc.) + parse_weak_field(p, fname.clone(), methods, fields, weak_fields)?; + + // Register with visibility tracking + if visibility == "public" { + public_fields.push(fname); + } else { + private_fields.push(fname); + } + + *last_method_name = None; // Reset method context (Phase 285A1.4) + return Ok(true); + } else { + return Err(ParseError::UnexpectedToken { + expected: "field name after 'weak' in visibility context".to_string(), + found: p.current_token().token_type.clone(), + line: p.current_token().line, + }); + } + } if let TokenType::IDENTIFIER(n) = &p.current_token().token_type { let fname = n.clone(); p.advance(); diff --git a/src/parser/declarations/box_def/members/methods.rs b/src/parser/declarations/box_def/members/methods.rs index 1d6d0074..883b386f 100644 --- a/src/parser/declarations/box_def/members/methods.rs +++ b/src/parser/declarations/box_def/members/methods.rs @@ -17,17 +17,8 @@ pub(crate) fn try_parse_method( } p.advance(); // consume '(' - let mut params = Vec::new(); - while !p.match_token(&TokenType::RPAREN) && !p.is_at_end() { - crate::must_advance!(p, _unused, "method parameter parsing"); - if let TokenType::IDENTIFIER(param) = &p.current_token().token_type { - params.push(param.clone()); - p.advance(); - } - if p.match_token(&TokenType::COMMA) { - p.advance(); - } - } + // Phase 285A1.5: Use shared helper to prevent parser hangs on unsupported type annotations + let params = crate::parser::common::params::parse_param_name_list(p, "method")?; p.consume(TokenType::RPAREN)?; let mut body = p.parse_block_statements()?; diff --git a/src/parser/expr/primary.rs b/src/parser/expr/primary.rs index d70824f7..06e7288c 100644 --- a/src/parser/expr/primary.rs +++ b/src/parser/expr/primary.rs @@ -313,6 +313,19 @@ impl NyashParser { span: Span::unknown(), }) } + // Phase 285A0: weak(expr) → WeakRef creation + // SSOT: docs/reference/language/lifecycle.md:171 + TokenType::WEAK => { + self.advance(); + self.consume(TokenType::LPAREN)?; + let argument = self.parse_expression()?; + self.consume(TokenType::RPAREN)?; + Ok(ASTNode::FunctionCall { + name: "weak".to_string(), + arguments: vec![argument], + span: Span::unknown(), + }) + } _ => { let line = self.current_token().line; Err(ParseError::InvalidExpression { line }) diff --git a/src/parser/items/functions.rs b/src/parser/items/functions.rs index 64081cad..b67502ea 100644 --- a/src/parser/items/functions.rs +++ b/src/parser/items/functions.rs @@ -29,27 +29,9 @@ impl NyashParser { // パラメータリストをパース self.consume(TokenType::LPAREN)?; - let mut params = Vec::new(); - while !self.match_token(&TokenType::RPAREN) && !self.is_at_end() { - must_advance!(self, _unused, "function declaration parameter parsing"); - - if let TokenType::IDENTIFIER(param) = &self.current_token().token_type { - params.push(param.clone()); - self.advance(); - - if self.match_token(&TokenType::COMMA) { - self.advance(); - } - } else if !self.match_token(&TokenType::RPAREN) { - let line = self.current_token().line; - return Err(ParseError::UnexpectedToken { - found: self.current_token().token_type.clone(), - expected: "parameter name".to_string(), - line, - }); - } - } + // Phase 285A1.5: Use shared helper to prevent parser hangs on unsupported type annotations + let params = crate::parser::common::params::parse_param_name_list(self, "function")?; self.consume(TokenType::RPAREN)?; diff --git a/src/parser/items/static_items.rs b/src/parser/items/static_items.rs index 3f990c5b..60958938 100644 --- a/src/parser/items/static_items.rs +++ b/src/parser/items/static_items.rs @@ -67,27 +67,9 @@ impl NyashParser { // パラメータリストをパース self.consume(TokenType::LPAREN)?; - let mut params = Vec::new(); - while !self.match_token(&TokenType::RPAREN) && !self.is_at_end() { - must_advance!(self, _unused, "static function parameter parsing"); - - if let TokenType::IDENTIFIER(param) = &self.current_token().token_type { - params.push(param.clone()); - self.advance(); - - if self.match_token(&TokenType::COMMA) { - self.advance(); - } - } else if !self.match_token(&TokenType::RPAREN) { - let line = self.current_token().line; - return Err(ParseError::UnexpectedToken { - found: self.current_token().token_type.clone(), - expected: "parameter name".to_string(), - line, - }); - } - } + // Phase 285A1.5: Use shared helper to prevent parser hangs on unsupported type annotations + let params = crate::parser::common::params::parse_param_name_list(self, "static method")?; self.consume(TokenType::RPAREN)?; diff --git a/src/runner/modes/vm.rs b/src/runner/modes/vm.rs index dd1eb362..311631f2 100644 --- a/src/runner/modes/vm.rs +++ b/src/runner/modes/vm.rs @@ -642,6 +642,9 @@ impl NyashRunner { .debug(&format!("[runner/vm] exit_code={}", exit_code)); } + // Phase 285: Emit leak report before exit (if enabled) + crate::runtime::leak_tracker::emit_leak_report(); + // Exit with the return value as exit code process::exit(exit_code); } diff --git a/src/runtime/host_api.rs b/src/runtime/host_api.rs index 001999d4..a7188af7 100644 --- a/src/runtime/host_api.rs +++ b/src/runtime/host_api.rs @@ -511,6 +511,14 @@ pub extern "C" fn nyrt_host_call_slot( crate::backend::vm::VMValue::Void => { Box::new(crate::box_trait::VoidBox::new()) } + // Phase 285A0: WeakBox upgrade or void + crate::backend::vm::VMValue::WeakBox(w) => { + if let Some(arc) = w.upgrade() { + arc.share_box() + } else { + Box::new(crate::box_trait::VoidBox::new()) + } + } }; let out = map.has(key_box); let vmv = crate::backend::vm::VMValue::from_nyash_box(out); @@ -539,6 +547,14 @@ pub extern "C" fn nyrt_host_call_slot( crate::backend::vm::VMValue::Void => { Box::new(crate::box_trait::VoidBox::new()) } + // Phase 285A0: WeakBox upgrade or void + crate::backend::vm::VMValue::WeakBox(w) => { + if let Some(arc) = w.upgrade() { + arc.share_box() + } else { + Box::new(crate::box_trait::VoidBox::new()) + } + } }; let out = map.get(key_box); let vmv = crate::backend::vm::VMValue::from_nyash_box(out); @@ -567,6 +583,14 @@ pub extern "C" fn nyrt_host_call_slot( crate::backend::vm::VMValue::Void => { Box::new(crate::box_trait::VoidBox::new()) } + // Phase 285A0: WeakBox upgrade or void + crate::backend::vm::VMValue::WeakBox(w) => { + if let Some(arc) = w.upgrade() { + arc.share_box() + } else { + Box::new(crate::box_trait::VoidBox::new()) + } + } }; let val_box: Box = match argv[1].clone() { crate::backend::vm::VMValue::Integer(i) => { @@ -586,6 +610,14 @@ pub extern "C" fn nyrt_host_call_slot( crate::backend::vm::VMValue::Void => { Box::new(crate::box_trait::VoidBox::new()) } + // Phase 285A0: WeakBox upgrade or void + crate::backend::vm::VMValue::WeakBox(w) => { + if let Some(arc) = w.upgrade() { + arc.share_box() + } else { + Box::new(crate::box_trait::VoidBox::new()) + } + } }; let out = map.set(key_box, val_box); let vmv = crate::backend::vm::VMValue::from_nyash_box(out); diff --git a/src/runtime/leak_tracker.rs b/src/runtime/leak_tracker.rs index 42f26706..a2d9b26c 100644 --- a/src/runtime/leak_tracker.rs +++ b/src/runtime/leak_tracker.rs @@ -1,10 +1,41 @@ +//! Leak Tracker - Exit-time diagnostics for strong references still held +//! +//! Phase 285: Extended to report all global roots (modules, host_handles, plugin boxes). +//! +//! ## Environment Variable +//! +//! - `NYASH_LEAK_LOG=1` - Summary counts only +//! - `NYASH_LEAK_LOG=2` - Verbose (include names/entries, truncated to first 10) +//! +//! ## Output Format +//! +//! ```text +//! [lifecycle/leak] Roots still held at exit: +//! [lifecycle/leak] modules: 3 +//! [lifecycle/leak] host_handles: 5 +//! [lifecycle/leak] plugin_boxes: 2 +//! ``` + use crate::runtime::get_global_ring0; use once_cell::sync::Lazy; use std::collections::HashMap; use std::sync::Mutex; -static ENABLED: Lazy = - Lazy::new(|| std::env::var("NYASH_LEAK_LOG").unwrap_or_default() == "1"); +/// Leak log level: 0 = off, 1 = summary, 2 = verbose +static LEVEL: Lazy = Lazy::new(|| { + match std::env::var("NYASH_LEAK_LOG") + .unwrap_or_default() + .as_str() + { + "1" => 1, + "2" => 2, + _ => 0, + } +}); + +/// Backward compatibility: enabled if level >= 1 +static ENABLED: Lazy = Lazy::new(|| *LEVEL >= 1); + static LEAKS: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); @@ -38,21 +69,97 @@ impl Drop for Reporter { if !*ENABLED { return; } - let m = LEAKS.lock().unwrap(); - if m.is_empty() { - return; - } - get_global_ring0().log.warn(&format!( - "[leak] Detected {} non-finalized plugin boxes:", - m.len() - )); - for ((ty, id), _) in m.iter() { - get_global_ring0().log.warn(&format!( - " - {}(id={}) not finalized (missing fini or scope)", - ty, id - )); - } + emit_leak_report(); } } static REPORTER: Lazy = Lazy::new(|| Reporter); + +/// Emit exit-time leak report (Phase 285) +/// +/// Called automatically on program exit via Reporter::drop. +/// Can also be called manually for testing. +pub fn emit_leak_report() { + let level = *LEVEL; + if level == 0 { + return; + } + + let ring0 = get_global_ring0(); + + // Collect root counts + let modules = crate::runtime::modules_registry::snapshot_names_and_strings(); + let host_handles = crate::runtime::host_handles::snapshot(); + let plugin_boxes = LEAKS.lock().map(|m| m.len()).unwrap_or(0); + + let modules_count = modules.len(); + let host_handles_count = host_handles.len(); + + // Only print if there's something to report + if modules_count == 0 && host_handles_count == 0 && plugin_boxes == 0 { + return; + } + + // Summary header + ring0 + .log + .warn("[lifecycle/leak] Roots still held at exit:"); + + // Summary counts + if modules_count > 0 { + ring0 + .log + .warn(&format!("[lifecycle/leak] modules: {}", modules_count)); + } + if host_handles_count > 0 { + ring0.log.warn(&format!( + "[lifecycle/leak] host_handles: {}", + host_handles_count + )); + } + if plugin_boxes > 0 { + ring0 + .log + .warn(&format!("[lifecycle/leak] plugin_boxes: {}", plugin_boxes)); + } + + // Verbose details (level 2) + if level >= 2 { + const MAX_ENTRIES: usize = 10; + + // Module names + if !modules.is_empty() { + ring0.log.warn("[lifecycle/leak] module names:"); + for (i, (name, _value)) in modules.iter().take(MAX_ENTRIES).enumerate() { + ring0 + .log + .warn(&format!("[lifecycle/leak] [{}] {}", i, name)); + } + if modules.len() > MAX_ENTRIES { + ring0.log.warn(&format!( + "[lifecycle/leak] ... and {} more", + modules.len() - MAX_ENTRIES + )); + } + } + + // Plugin box details + if plugin_boxes > 0 { + ring0.log.warn("[lifecycle/leak] plugin box details:"); + if let Ok(m) = LEAKS.lock() { + for (i, ((ty, id), _)) in m.iter().take(MAX_ENTRIES).enumerate() { + ring0.log.warn(&format!( + "[lifecycle/leak] [{}] {}(id={}) not finalized", + i, ty, id + )); + } + if m.len() > MAX_ENTRIES { + ring0.log.warn(&format!( + "[lifecycle/leak] ... and {} more", + m.len() - MAX_ENTRIES + )); + } + } + } + } +} diff --git a/tools/smokes/v2/profiles/quick/lifecycle/phase285_weak_field_vm.sh b/tools/smokes/v2/profiles/quick/lifecycle/phase285_weak_field_vm.sh index 9d3cc439..2bab605c 100644 --- a/tools/smokes/v2/profiles/quick/lifecycle/phase285_weak_field_vm.sh +++ b/tools/smokes/v2/profiles/quick/lifecycle/phase285_weak_field_vm.sh @@ -31,5 +31,37 @@ for fixture in boxref primitive; do fi done -log_success "phase285_weak_field_vm: All weak field contract tests passed" +# Test 3: Phase 285A1.3 - Visibility block and mixed members +for fixture in visibility_block mixed_members; do + FIXTURE="$NYASH_ROOT/apps/tests/phase285_weak_${fixture}.hako" + + if ! output=$(NYASH_DISABLE_PLUGINS=1 "$NYASH_BIN" "$FIXTURE" 2>&1); then + log_error "phase285_weak_field_vm: A1.3 case '${fixture}' failed to compile" + echo "$output" + exit 1 + fi +done + +# Test 4: Phase 285A1.4 - Sugar syntax OK case +FIXTURE="$NYASH_ROOT/apps/tests/phase285_visibility_weak_sugar_ok.hako" +if ! output=$(NYASH_DISABLE_PLUGINS=1 "$NYASH_BIN" "$FIXTURE" 2>&1); then + log_error "phase285_weak_field_vm: A1.4 sugar syntax OK case failed to compile" + echo "$output" + exit 1 +fi + +# Test 5: Phase 285A1.4 - Sugar syntax NG case (should fail to compile) +FIXTURE="$NYASH_ROOT/apps/tests/phase285_visibility_weak_sugar_ng.hako" +if output=$(NYASH_DISABLE_PLUGINS=1 "$NYASH_BIN" "$FIXTURE" 2>&1); then + log_error "phase285_weak_field_vm: A1.4 sugar syntax NG case should have failed" + exit 1 +fi + +if ! echo "$output" | grep -q "weak"; then + log_error "phase285_weak_field_vm: A1.4 sugar syntax NG case missing 'weak' in error" + echo "$output" + exit 1 +fi + +log_success "phase285_weak_field_vm: All weak field contract tests passed (8 tests: 3 OK, 2 NG, 2 A1.3, 1 OK + 1 NG sugar)" exit 0 diff --git a/tools/smokes/v2/profiles/quick/parser/phase285_param_type_annotation_nohang.sh b/tools/smokes/v2/profiles/quick/parser/phase285_param_type_annotation_nohang.sh new file mode 100644 index 00000000..bd71ff71 --- /dev/null +++ b/tools/smokes/v2/profiles/quick/parser/phase285_param_type_annotation_nohang.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# Phase 285A1.5: Parser hang fix - Parameter type annotations +# Goal: Verify parser does not hang on unsupported param: Type syntax + +set -euo pipefail + +# Resolve PROJECT_ROOT +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../../../.." && pwd)" +HAKORUNE="$PROJECT_ROOT/target/release/hakorune" +TEST_FILE="$PROJECT_ROOT/apps/tests/phase285_parser_param_type_annot_should_not_hang.hako" + +# Check hakorune binary exists +if [[ ! -f "$HAKORUNE" ]]; then + echo "❌ FAIL: hakorune binary not found at $HAKORUNE" + exit 1 +fi + +# Check test file exists +if [[ ! -f "$TEST_FILE" ]]; then + echo "❌ FAIL: Test file not found at $TEST_FILE" + exit 1 +fi + +# Run with timeout (3 seconds) +set +e +OUTPUT=$(timeout 3s "$HAKORUNE" "$TEST_FILE" 2>&1) +EXIT_CODE=$? +set -e + +# Check 1: No timeout (exit code 124 means timeout) +if [[ $EXIT_CODE -eq 124 ]]; then + echo "❌ FAIL: Parser hung (timeout after 3s)" + exit 1 +fi + +# Check 2: Parse error (non-zero exit code expected) +if [[ $EXIT_CODE -eq 0 ]]; then + echo "❌ FAIL: Parser should error on unsupported syntax (got exit code 0)" + exit 1 +fi + +# Check 3: Error message mentions the COLON token issue +if ! echo "$OUTPUT" | grep -q "Unexpected token COLON"; then + echo "❌ FAIL: Wrong error message (should mention 'Unexpected token COLON')" + echo "Got: $OUTPUT" + exit 1 +fi + +# Check 4: Error message mentions parameter type annotations +if ! echo "$OUTPUT" | grep -q "Parameter type annotations"; then + echo "❌ FAIL: Error message should mention 'Parameter type annotations'" + echo "Got: $OUTPUT" + exit 1 +fi + +echo "✅ PASS: Parser correctly rejects param type annotations without hanging"