feat(weak): Phase 285A1 - Weak Field Contract (Strict Type Enforcement)

Remove automatic WeakNew conversion and enforce strict compile-time type
checking for weak field assignments. Only 3 assignment types allowed:
1. Result of weak(x) call (WeakRef type)
2. Existing WeakRef variable (e.g., me.parent = other.parent)
3. Void/null (clear operation)

**Implementation**:
- Added MirType::WeakRef to type system (src/mir/types.rs)
- Track WeakRef type in emit_weak_new() even in pure mode
- Weak field reads return WeakRef without auto-upgrade
- Removed automatic WeakNew conversion from field writes
- Implemented check_weak_field_assignment() with actionable errors
- Fixed null literal type tracking (Phase 285A1.1: Unknown → Void)

**Testing**:
- 5 test fixtures (3 OK, 2 NG cases) - all passing
- Smoke test: phase285_weak_field_vm.sh
- Error messages guide users to use weak() or null

**Documentation**:
- Updated lifecycle.md SSOT with weak field contract

🤖 Generated with Claude Code

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-24 03:17:30 +09:00
parent b0eeb14c54
commit cc8b27a1aa
20 changed files with 802 additions and 29 deletions

View File

@ -0,0 +1,143 @@
# Claude Code Runbook (Phase 285): VM↔LLVM lifecycle conformance
This file is an instruction sheet for an implementation agent (Claude Code) to make the language SSOT pass end-to-end across backends.
Language SSOT:
- Lifecycle/weak/fini/GC policy: `docs/reference/language/lifecycle.md`
- Truthiness + `null`/`void`: `docs/reference/language/types.md`
Non-goal: changing language semantics. Any backend drift must be fixed as an implementation bug or explicitly tracked as “unsupported”.
## What to implement (in order)
### 0) Preflight (must-pass before any weak smokes)
Confirm the following are implemented; if any are missing, do **not** run weak fixtures yet:
- `weak(x)` can be parsed and lowered into MIR (WeakRef/WeakNew).
- VM has a handler for MIR `WeakRef/WeakNew/WeakLoad` (no panic/unimplemented).
- `WeakRef.weak_to_strong()` exists at the language surface.
If any are missing, choose one:
- **Option A**: build the missing weak infrastructure first.
- **Option B**: temporarily scope to exit-time leak report only (skip weak smokes, document as “unsupported”).
### 1) WeakRef semantics (VM + LLVM)
Required behavior:
- `weak(x)` creates a non-owning WeakRef.
- `w.weak_to_strong()` returns a strong BoxRef when the target is usable; otherwise returns `null` (runtime `Void`).
- WeakRef does not auto-upgrade on field access (field read returns WeakRef).
- WeakRef equality uses a stable token (do not make `dropped==dropped` true for unrelated targets).
Conformance checks:
- VM: weak works in real execution (not just `toString()`).
- LLVM (harness): must match VM behavior for the same program output/exit code.
- WASM: if unsupported, keep it explicitly documented as unsupported; do not pretend it is correct by copying strong refs.
### 2) Exit-time “roots still held” report (diagnostic, default-off)
Goal: when a program ends while strong references are still held in global roots, print a report so developers can see leaks/cycles.
Requirements:
- Must be default-off.
- Must not change program meaning (only prints when enabled).
- Should report “what roots still hold strong references”, not attempt to “fix” them.
Suggested interface (choose one and document it):
- Env: `NYASH_LEAK_LOG={1|2}`
- `1`: summary counts
- `2`: verbose (print up to N names/entries, with truncation)
Root candidates to include (best-effort):
- `env.modules` registry
- plugin singletons / plugin registry
- host handles / external handles registry
Output stability:
- Use stable tags like `[leak]` or `[lifecycle/leak]` so smokes can match logs.
- Truncate long lists deterministically (e.g., first N sorted entries).
### 3) Cross-backend smokes (VM + LLVM)
Add smokes under `tools/smokes/v2/` (preferred) to lock behavior.
Keep tests fast and deterministic.
Recommended fixtures (as `.hako` or inline sources in the smoke):
**A. Weak weak_to_strong success/fail**
```nyash
box SomeBox { x }
static box Main {
main() {
local x = new SomeBox()
local w = weak(x)
x = null
local y = w.weak_to_strong()
if y == null { print("ok: dropped") }
return 0
}
}
```
Expected (VM and LLVM): prints `ok: dropped`, exit 0.
**B. Strong cycle + leak report**
```nyash
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
}
}
```
Expected:
- Program output stays `ok: cycle-created`.
- With leak report enabled, a report appears at exit (VM at minimum; LLVM if feasible).
**C. Weak breaks cycle (no strong-cycle leak)**
```nyash
box Node { other_weak }
static box Main {
main() {
local a = new Node()
local b = new Node()
a.other_weak = weak(b)
b.other_weak = weak(a)
print("ok: weak-cycle")
return 0
}
}
```
Expected:
- Program output stays `ok: weak-cycle`.
- Leak report should not claim an obvious strong-cycle root for these nodes (best-effort; depends on what is rooted globally).
### 4) Update docs after implementation
When the above is implemented:
- Add the chosen env var to `docs/reference/environment-variables.md` (avoid env var sprawl; keep it in the diagnostics table).
- If any backend remains unsupported, update `docs/reference/language/lifecycle.md` “Implementation status” with an explicit note and link.
## Commands (suggested)
Build:
- `cargo build --release --features llvm`
Run VM:
- `./target/release/hakorune --backend vm local_tests/phase285_weak_basic.hako`
Run LLVM:
- `NYASH_LLVM_USE_HARNESS=1 ./target/release/hakorune --backend llvm local_tests/phase285_weak_basic.hako`
Leak report (example if env var chosen):
- `NYASH_LEAK_LOG=1 ./target/release/hakorune --backend vm local_tests/phase285_cycle.hako`
## Done criteria (acceptance)
- VM and LLVM outputs match for weak fixtures (success/fail).
- Strong-cycle fixture produces a visible exit-time report when the diagnostic is enabled (and produces no report when disabled).
- Weak-cycle fixture does not falsely report a strong-cycle “leak” for the nodes (within the documented root scope).

View File

@ -5,23 +5,27 @@
## 1. このP0でやることコード変更なし
1) 仕様SSOTを 1 ファイルにまとめる
- `docs/development/current/main/phases/phase-285/README.md` を入口SSOTとして育てる。
- 言語レベルの SSOT は `docs/reference/language/lifecycle.md`lifecyle/weak/fini/GC`docs/reference/language/types.md`truthiness と `null`/`void`)に集約する。
- Phase 285 は「実装の棚卸し・差分追跡・受け入れ条件」を書く言語SSOTを書き換えない
2) 用語と境界を固定する
- strong/weak/roots/finalizer/collection の定義
- weakref の APIupgrade/生存判定)
- finalizer の禁止事項(再入・例外・順序)
2) 用語と境界を固定する
- strong/weak/roots/finalizer/collection の定義
- weakref の APIweak_to_strong/生存判定)
- finalizer の禁止事項(再入・例外・順序)
3) LLVM harness の扱いを明文化する
- 未対応なら “未対応” を仕様として書く(差分を隠さない)。
3) LLVM harness の扱いを明文化する
- 未対応なら “未対応” を差分として書く(差分を隠さない)。
- 差分は「仕様差」ではなく「未実装/バグ/保留」として分類する言語SSOTは揺らさない
## 2. README に必ず書く事項(チェックリスト)
- [ ] “roots” は何かstack/local/global/handle/plugin 等)
- [ ] strong/weak の意味(upgrade の成否条件)
- [ ] strong/weak の意味(weak_to_strong の成否条件)
- [ ] strong/weak の意味weak_to_strong の成否条件)
- [ ] finalizer はあるか/いつ発火するか/何が禁止か
- [ ] GC/解放のトリガ(自動/手動/閾値/テスト用)
- [ ] VM と LLVM harness の差分(未対応の場合の方針)
- 分類: (A) 仕様通り / (B) 未実装 / (C) 既知バグ / (D) 仕様外(禁止)
## 3. 次P1/P2への導線箇条書きでOK
@ -34,7 +38,7 @@
- `src/value.rs`
- `NyashValue::WeakBox` の生成箇所weak をどう作るか)
- `upgrade()` 失敗時の観測方法(文字列化/判定API
- `weak_to_strong()` 失敗時の観測方法(文字列化/判定API
- unit test: `test_weak_reference_drop` の仕様(何を固定しているか)
- `src/finalization.rs`
- finalizer の存在(あれば: 登録、呼び出しタイミング、順序)

View File

@ -1,4 +1,4 @@
# Phase 285: Box lifecycle / weakref / finalization / GC SSOT
# Phase 285: Box lifecycle / weakref / finalization / GC conformance
Status: Planned (design-first)
@ -6,6 +6,12 @@ Status: Planned (design-first)
Box の生存期間(強参照/弱参照/解放/最終化/GCを SSOT として固定し、移行期間でも意味論が割れない状態にする。
Language-level SSOT:
- Lifecycle/weak/fini/GC policy: `docs/reference/language/lifecycle.md`
- Truthiness + `null`/`void`: `docs/reference/language/types.md`
This Phase document is not the language SSOT; it tracks implementation status, backend gaps, and acceptance criteria.
## Why now
- JoinIR/Plan/compose の収束が進むほど、実行時の “値の寿命” の揺れが目立つ。
@ -22,26 +28,41 @@ Box の生存期間(強参照/弱参照/解放/最終化/GCを SSOT とし
## Snapshot今わかっていること
- weakref は `Weak<Mutex<dyn NyashBox>>` で保持される(`NyashValue::WeakBox`
- `WeakBox``to_string()``upgrade()` を試み、`WeakRef(null)` 表示になりうる(観測可能)
- `WeakBox``to_string()``weak_to_strong()` を試み、`WeakRef(null)` 表示になりうる(観測可能)
- `src/value.rs` に weakref の drop 挙動を固定する unit test がある(`test_weak_reference_drop`
## Responsibility Mapどこが仕様を決めるか
- **SSOT意味**: Rust VM 実装(`src/value.rs`, `src/finalization.rs` 周辺
- **SSOT観測**: fixture/smokePhase 285 P2 で作る)
- **LLVM harness**: まずは “差分を仕様として明文化” が優先(未対応なら SKIP を SSOT 化する)
- **SSOT意味**: `docs/reference/language/*`言語レベルのSSOT
- **Conformance**: Rust VM / LLVM harness / WASM / JIT など各バックエンド実装
- **観測の固定**: fixture/smokePhase 285 P2 で作る)
## 用語P0で固定する
- **Strong reference**: 所有参照(`Arc` 等で Box を保持)
- **Weak reference**: 非所有参照(`Weak` / `upgrade()` が失敗しうる)
- **Upgrade**: weak → strong の昇格(成功/失敗が意味論)
- **Weak reference**: 非所有参照(`Weak` / `weak_to_strong()` が失敗しうる)
- **Weak-to-strong**: weak → strong の昇格(成功/失敗が意味論)
- **Roots**: 解放/GC から保護される参照集合stack/local/global/handle/plugin
- **Finalizer**: 解放に伴う最終化処理(もし存在するなら)
## P0 decisions (docs-only)
- Weak の観測は `weak_to_strong()` で行い、失敗値は `null`= runtime `Void` の別名)。
- `cleanup`Stage3 block-postfixが「出口で必ず走る」決定的 cleanup を保証する(`catch` の有無に関係なく、常に実行)。
- GC は意味論ではなく補助GC off で cycle はリークしうる)。
- ByRef (`RefGet/RefSet`) は non-owning / non-escaping寿命・弱参照・GC の道具にしない)。
## RUNBOOK caveat (implementation reality)
The runbook assumes WeakRef infrastructure exists in the VM and lowering.
If any of the following are missing, treat weak smokes as **unsupported** and scope to exit-time leak report first:
- `weak(x)` parse/lower
- VM handler for MIR WeakRef/WeakNew/WeakLoad
- language-surface `weak_to_strong()` on WeakRef
## Questions to Answer (P0/P1)
- weakref の “生存判定” は何で観測できるか(`toString` / `is_alive` / `upgrade` API など)
- weakref の “生存判定” は何で観測できるか(`toString` / `is_alive` / `weak_to_strong` API など)
- finalizer は存在するか / いつ発火するかdrop 時GC 時?明示 API
- finalizer 内での禁止事項再入、例外、I/O、allocationをどうするか
- LLVM harness の扱い(現状未対応なら “未対応として SSOT 化”)
@ -51,7 +72,7 @@ Box の生存期間(強参照/弱参照/解放/最終化/GCを SSOT とし
### P0docs-only
- 用語の固定strong/weak/roots/finalizer/collection
- 仕様の固定weakref の upgrade 成否、finalizer の発火条件、禁止事項)
- 仕様の固定weakref の weak_to_strong 成否、finalizer の発火条件、禁止事項)
- “LLVM harness の扱い” を明文化(未対応なら未対応として SSOT に書く)
### P1investigation
@ -69,3 +90,10 @@ Box の生存期間(強参照/弱参照/解放/最終化/GCを SSOT とし
- GC アルゴリズム刷新RC→tracing 等の設計変更)
- LLVM harness に同等機能を “一気に” 実装(差分の記録→段階導入を優先)
## Acceptance criteria (P2+)
- VM と LLVM で、weak が仕様通り動作する(`weak_to_strong()` 成功/失敗が一致、失敗は `null`)。
- 強参照サイクルを意図的に作ったとき、GC off なら)回収されないことが観測できる。
- 終了時に「強参照が残っている root」をデバッグ出力できるdefault-off の診断フラグ)。
- これは意味論ではなく診断であり、ON/OFF でプログラムの意味を変えない。