feat(parser): Phase 285A1.4 & A1.5 - Weak field sugar + Parser hang fix
A1.4: Add sugar syntax `public weak parent` ≡ `public { weak parent }`
A1.5: Fix parser hang on unsupported `param: Type` syntax
Key changes:
- A1.4: Extend visibility parser to handle weak modifier (fields.rs)
- A1.5: Shared helper `parse_param_name_list()` with progress-zero detection
- A1.5: Fix 6 vulnerable parameter parsing loops (methods, constructors, functions)
- Tests: Sugar syntax (OK/NG), parser hang (timeout-based)
- Docs: lifecycle.md, EBNF.md, phase-285a1-boxification.md
Additional changes:
- weak() builtin implementation (handlers/weak.rs)
- Leak tracking improvements (leak_tracker.rs)
- Documentation updates (lifecycle, types, memory-finalization, etc.)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
16
apps/tests/phase285_leak_report.hako
Normal file
16
apps/tests/phase285_leak_report.hako
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
16
apps/tests/phase285_visibility_weak_sugar_ng.hako
Normal file
16
apps/tests/phase285_visibility_weak_sugar_ng.hako
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
18
apps/tests/phase285_visibility_weak_sugar_ok.hako
Normal file
18
apps/tests/phase285_visibility_weak_sugar_ok.hako
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
35
apps/tests/phase285_weak_basic.hako
Normal file
35
apps/tests/phase285_weak_basic.hako
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
15
apps/tests/phase285_weak_field_legacy_init_ok.hako
Normal file
15
apps/tests/phase285_weak_field_legacy_init_ok.hako
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
box Node {
|
||||
init { weak parent }
|
||||
weak parent
|
||||
}
|
||||
|
||||
static box Main {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
box Node {
|
||||
init { weak parent }
|
||||
weak parent
|
||||
}
|
||||
|
||||
static box Main {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
box Node {
|
||||
init { weak parent, weak sibling }
|
||||
weak parent
|
||||
weak sibling
|
||||
}
|
||||
|
||||
static box Main {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
box Node {
|
||||
init { weak parent }
|
||||
weak parent
|
||||
}
|
||||
|
||||
static box Main {
|
||||
|
||||
303
docs/archive/core-language/portability-contract.md
Normal file
303
docs/archive/core-language/portability-contract.md
Normal file
@ -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の技術的優位性*
|
||||
@ -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("<unknown>")
|
||||
)));
|
||||
```
|
||||
|
||||
### 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
|
||||
@ -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
|
||||
@ -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<Vec<String>, 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 実装本体
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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成果**: プラグインシステムとの完全統合
|
||||
**Phase 9.75g-0成果**: プラグインシステムとの完全統合
|
||||
|
||||
@ -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)
|
||||
- [言語リファレンス](../language-reference.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)
|
||||
- [メモリ管理](memory-management.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)
|
||||
- [BID-FFI仕様](../plugin-system/ffi-abi-specification.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`)
|
||||
|
||||
## 📦 アーカイブ
|
||||
|
||||
|
||||
@ -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の技術的優位性*
|
||||
Language SSOT:
|
||||
- `docs/reference/language/lifecycle.md`
|
||||
- `docs/reference/language/types.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) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -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。
|
||||
|
||||
|
||||
@ -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<Mutex>によるスレッドセーフな共有状態管理
|
||||
- 自動参照カウント + 明示的デストラクタ(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") }
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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`
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 仕様)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)
|
||||
- [実装タスク](../../../予定/native-plan/issues/phase_9_75g_0_chatgpt_enhanced_final.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` 抜粋
|
||||
|
||||
@ -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品質保証の技術的基盤*
|
||||
*Golden Dump Testing = Nyash品質保証の技術的基盤*
|
||||
|
||||
@ -38,6 +38,8 @@ pub fn to_bool_vm(v: &VMValue) -> Result<bool, String> {
|
||||
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::<crate::boxes::missing_box::MissingBox>()
|
||||
.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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -173,6 +173,7 @@ impl MirInterpreter {
|
||||
VMValue::Bool(_) => "BoolBox".to_string(),
|
||||
VMValue::Void => "<Void>".to_string(),
|
||||
VMValue::Future(_) => "<Future>".to_string(),
|
||||
VMValue::WeakBox(_) => "<WeakRef>".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);
|
||||
}
|
||||
|
||||
@ -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(_) => "<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) => {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 { .. }
|
||||
|
||||
59
src/backend/mir_interpreter/handlers/weak.rs
Normal file
59
src/backend/mir_interpreter/handlers/weak.rs
Normal file
@ -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(())
|
||||
}
|
||||
}
|
||||
@ -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!(
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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<dyn NyashBox>),
|
||||
/// Phase 285A0: Weak reference to a Box (non-owning)
|
||||
WeakBox(Weak<dyn NyashBox>),
|
||||
}
|
||||
|
||||
// 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<VMValue> {
|
||||
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<VMValue> {
|
||||
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(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -200,6 +200,17 @@ impl MirBuilder {
|
||||
method: String,
|
||||
arguments: &[ASTNode],
|
||||
) -> Result<ValueId, String> {
|
||||
// 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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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};
|
||||
92
src/parser/common/params.rs
Normal file
92
src/parser/common/params.rs
Normal file
@ -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<String>)` - 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<Vec<String>, 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)
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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()?;
|
||||
|
||||
|
||||
@ -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 })
|
||||
|
||||
@ -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)?;
|
||||
|
||||
|
||||
@ -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)?;
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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<dyn NyashBox> = 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);
|
||||
|
||||
@ -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<bool> =
|
||||
Lazy::new(|| std::env::var("NYASH_LEAK_LOG").unwrap_or_default() == "1");
|
||||
/// Leak log level: 0 = off, 1 = summary, 2 = verbose
|
||||
static LEVEL: Lazy<u8> = 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<bool> = Lazy::new(|| *LEVEL >= 1);
|
||||
|
||||
static LEAKS: Lazy<Mutex<HashMap<(String, u32), &'static str>>> =
|
||||
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<Reporter> = 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
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
Reference in New Issue
Block a user