feat(llvm): Phase 97 Box/Policy refactoring complete

Box化完了:
- CallRoutePolicyBox: Call routing SSoT
- PrintArgMarshallerBox: Print marshalling SSoT
- TypeFactsBox: Type propagation SSoT
- PhiSnapshotPolicyBox: PHI contract SSoT
- PluginErrorContext: Structured error reporting

📋 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-17 04:14:26 +09:00
parent 65763c1ed6
commit 6d73fc3404
16 changed files with 1861 additions and 64 deletions

View File

@ -0,0 +1,142 @@
# PHI Snapshot Contract - PHI値のSSA有効性契約
## 根本原則
**「PHIはSSA値として他blockでも有効」**
この原則は LLVM の SSA (Static Single Assignment) 形式の根幹であり、
破ってはならない契約です。
## 契約の詳細
### PHI値の有効性
PHI値は以下の条件で有効です
1. **Defining Block**: PHI値が定義されたblock
2. **Dominated Blocks**: PHI値のdefining blockがdominateする全てのblock
### Snapshot上のPHI
Block終端のsnapshot上でPHI値を参照する際
-**正しい**: snapshot miss時もPHI定義値を返す
-**誤り**: snapshot miss時にPHI値を「未定義」扱い
### 過去の破綻事例
この契約の破綻により以下の問題が発生しました:
#### 事例1: PHI値の消失
```python
# 誤った実装
def _value_at_end_i64(self, value_id, block_id):
snapshot = self.snapshots.get(block_id, {})
if value_id not in snapshot:
return None # ❌ PHI値もNone扱い
```
**問題**: PHI値がsnapshot missで消失
**修正**:
```python
def _value_at_end_i64(self, value_id, block_id):
snapshot = self.snapshots.get(block_id, {})
if value_id not in snapshot:
# ✅ PHI値はmiss扱いしない
if self.is_phi(value_id):
return self.get_phi_definition(value_id)
return None
```
#### 事例2: SSA不変条件の破綻
PHI値を「未定義」扱いすることで、SSA形式の基本原則が破綻
- **SSA不変条件**: 値は一度定義されたら変更されない
- **破綻現象**: PHI値が「定義済み」から「未定義」に変化
## 使用方法
### PhiSnapshotPolicyBox の使用
```python
from phi_snapshot_policy import PhiSnapshotPolicyBox
# PHI値の有効性判定
is_valid = PhiSnapshotPolicyBox.is_phi_valid_at(
phi_id, block_id, dominator_info
)
# Snapshot上でのPHI解決
phi_value = PhiSnapshotPolicyBox.resolve_phi_at_snapshot(
phi_id, snapshot, resolver
)
```
### Resolver での統合
```python
class Resolver:
def _value_at_end_i64(self, value_id, block_id):
snapshot = self.snapshots.get(block_id, {})
# PhiSnapshotPolicyBox を使用してPHI値を正しく解決
if PhiSnapshotPolicyBox.is_phi(value_id, self):
return PhiSnapshotPolicyBox.resolve_phi_at_snapshot(
value_id, snapshot, self
)
# 通常の値の解決
return snapshot.get(value_id)
```
## Fail-Fast
契約違反は即座にエラー:
- `AssertionError`: PHI値を「未定義」扱い
- `AssertionError`: snapshot miss時にPHI値を無視
これにより、契約違反を早期に検出し、バグの伝播を防ぎます。
## デバッグ
### PHI値の追跡
環境変数 `NYASH_PHI_ORDERING_DEBUG=1` でPHI値の処理を追跡
```bash
NYASH_PHI_ORDERING_DEBUG=1 ./target/release/hakorune --backend llvm program.hako
```
出力例:
```
[phi_wiring/create] v42 PHI created: phi.basic_block=bb3 expected=bb3
[phi_wiring] WARNING: Attempting to create PHI in bb5 after terminator already exists!
```
## 参考
- **LLVM SSA Form**: https://llvm.org/docs/LangRef.html#ssa-form
- **Dominator Tree**: https://llvm.org/docs/ProgrammersManual.html#dominators
- **Phase 97 Refactoring**: この契約のSSoT化
## 設計原則
### Monotonic Property
PHI値の状態は単調増加monotonic
- 「未定義」→「定義済み」: ✅ 許可
- 「定義済み」→「未定義」: ❌ 禁止
### SSA Invariant
SSA形式の不変条件
1. **Single Assignment**: 各値は一度だけ定義される
2. **Dominance**: 値の使用はdefining blockにdominateされる
3. **PHI Merge**: PHI命令は複数の定義をmergeする唯一の方法
この契約はSSA Invariantの維持に不可欠です。

View File

@ -0,0 +1,120 @@
"""Print Argument Marshaller Box - print引数の型変換SSoT
Phase 97 Refactoring: printの引数marshal処理を一箇所に集約。
"""
from typing import Any
class PrintArgMarshallerBox:
"""print引数のmarshall処理Box
責務:
- print引数の型判定stringish / non-stringish
- non-stringishの場合: box.from_i64 → to_i8p_h
- stringishの場合: そのまま渡す
契約:
- 入力: 引数ValueId、型情報stringish判定
- 出力: marshal後のi8*ポインタ
- 前提条件: ValueIdが解決可能
Fail-Fast:
- ValueIdが未定義 → KeyError
- 型情報が不正 → TypeError
重要な境界:
「printはstringish以外を box.from_i64 してから to_i8p_h」
これはLLVM FFI境界の契約であり、変更時は慎重に。
"""
@staticmethod
def marshal(arg_id: Any, type_info: dict, builder, resolver, module) -> Any:
"""print引数をi8*にmarshal
Args:
arg_id: 引数ValueId
type_info: 型情報("stringish": bool
builder: LLVM builder
resolver: Value resolver
module: LLVM module
Returns:
i8*ポインタLLVM Value
Raises:
KeyError: ValueIdが未定義
TypeError: 型情報が不正
"""
if "stringish" not in type_info:
raise TypeError("[PrintArgMarshallerBox] type_info must contain 'stringish'")
is_stringish = type_info["stringish"]
# Resolve argument value
arg_val = None
if resolver and hasattr(resolver, 'resolve_i64'):
try:
arg_val = resolver.resolve_i64(arg_id, builder.block, None, None, {}, {})
except:
pass
if arg_val is None:
raise KeyError(f"[PrintArgMarshallerBox] Cannot resolve ValueId: {arg_id}")
if is_stringish:
# stringishはそのまま渡す既にi8*として扱える)
# to_i8p_h を経由して変換
import llvmlite.ir as ir
i8p = ir.IntType(8).as_pointer()
to_i8p = None
for f in module.functions:
if f.name == "nyash.string.to_i8p_h":
to_i8p = f
break
if not to_i8p:
to_i8p_type = ir.FunctionType(i8p, [ir.IntType(64)])
to_i8p = ir.Function(module, to_i8p_type, name="nyash.string.to_i8p_h")
return builder.call(to_i8p, [arg_val])
else:
# non-stringish: box.from_i64 → to_i8p_h
import llvmlite.ir as ir
i8p = ir.IntType(8).as_pointer()
# Get or create box.from_i64
boxer = None
for f in module.functions:
if f.name == "nyash.box.from_i64":
boxer = f
break
if boxer is None:
boxer = ir.Function(module, ir.FunctionType(ir.IntType(64), [ir.IntType(64)]), name="nyash.box.from_i64")
# Get or create to_i8p_h
to_i8p = None
for f in module.functions:
if f.name == "nyash.string.to_i8p_h":
to_i8p = f
break
if not to_i8p:
to_i8p_type = ir.FunctionType(i8p, [ir.IntType(64)])
to_i8p = ir.Function(module, to_i8p_type, name="nyash.string.to_i8p_h")
# box.from_i64(arg_val)
box_val = builder.call(boxer, [arg_val])
# to_i8p_h(box_val)
i8p_val = builder.call(to_i8p, [box_val])
return i8p_val
@staticmethod
def is_stringish(type_info: dict) -> bool:
"""型がstringishか判定
Args:
type_info: 型情報dict
Returns:
stringishならTrue
"""
return type_info.get("stringish", False)

View File

@ -0,0 +1,130 @@
"""Call Routing Policy Box - Call種別判定のSSoT
Phase 97 Refactoring: static method / instance method / plugin invoke の
ルーティング判定を一箇所に集約。
"""
from enum import Enum
from typing import Optional, NamedTuple
class CallKind(Enum):
"""Call の種別"""
STATIC_METHOD = "static_method" # Box.method() 形式
INSTANCE_METHOD = "instance_method" # box.method() 形式
PLUGIN_INVOKE = "plugin_invoke" # Plugin経由の呼び出し
class RouteDecision(NamedTuple):
"""ルーティング判定結果"""
kind: CallKind
is_direct_call: bool # static method直呼びか
reason: str # 判定理由(デバッグ用)
class CallRoutePolicyBox:
"""Call ルーティングのPolicy Box
責務:
- Callee文字列から Call種別を判定
- static method直呼びの判定
- 判定理由の明示
契約:
- 入力: callee文字列例: "StringBox.concat", "box.method", "PluginBox.invoke"
- 出力: RouteDecisionkind, is_direct_call, reason
- 前提条件: callee文字列が非空
Fail-Fast:
- callee が空文字列 → ValueError
- 不明なcallee形式 → ValueError"unknown callee format"
"""
@staticmethod
def decide(callee: str, ctx: Optional[dict] = None) -> RouteDecision:
"""Call種別を判定
Args:
callee: Callee文字列例: "StringBox.concat"
ctx: 追加コンテキスト(将来拡張用)
Returns:
RouteDecisionkind, is_direct_call, reason
Raises:
ValueError: callee が空文字列または不明な形式
"""
if not callee:
raise ValueError("[CallRoutePolicyBox] callee must not be empty")
ctx = ctx or {}
# static method判定Box.method形式
if "." in callee and callee[0].isupper():
# 例: "StringBox.concat", "IntegerBox.create"
is_direct = CallRoutePolicyBox._is_direct_static_call(callee, ctx)
reason = f"static method: {callee}, direct={is_direct}"
return RouteDecision(
kind=CallKind.STATIC_METHOD,
is_direct_call=is_direct,
reason=reason
)
# instance method判定box.method形式
if "." in callee and not callee[0].isupper():
# 例: "receiver.substring", "obj.get"
reason = f"instance method: {callee}"
return RouteDecision(
kind=CallKind.INSTANCE_METHOD,
is_direct_call=False,
reason=reason
)
# plugin invoke判定
if "Plugin" in callee or ctx.get("is_plugin", False):
reason = f"plugin invoke: {callee}"
return RouteDecision(
kind=CallKind.PLUGIN_INVOKE,
is_direct_call=False,
reason=reason
)
# 不明な形式
raise ValueError(f"[CallRoutePolicyBox] unknown callee format: {callee}")
@staticmethod
def _is_direct_static_call(callee: str, ctx: dict) -> bool:
"""static method が直呼び可能か判定
Phase 97: builtin BoxStringBox, IntegerBox等は直呼び可能。
Plugin Boxは非直呼び。
Args:
callee: static method名例: "StringBox.concat"
ctx: コンテキストbuiltin_boxes等
Returns:
直呼び可能ならTrue
"""
builtin_boxes = ctx.get("builtin_boxes", [
"StringBox", "IntegerBox", "BoolBox", "ArrayBox", "MapBox"
])
box_name = callee.split(".")[0]
is_builtin = box_name in builtin_boxes
return is_builtin
# デバッグ用ヘルパー
def log_route_decision(decision: RouteDecision, verbose: bool = False):
"""ルーティング判定をログ出力
Args:
decision: RouteDecision
verbose: 詳細ログ有効化
"""
if not verbose:
return
print(f"[phase97/call-route] {decision.reason} (kind={decision.kind.value}, direct={decision.is_direct_call})")

View File

@ -0,0 +1,101 @@
"""PHI Snapshot Policy Box - PHI値のSSA有効性契約
Phase 97 Refactoring: PHI値のsnapshot処理とSSA有効性の契約をSSOT化。
"""
from typing import Any, Optional
class PhiSnapshotPolicyBox:
"""PHI Snapshot Policy Box
責務:
- PHI値のSSA有効性判定
- Snapshot上のPHI参照ポリシー
- PHI miss判定の統一
契約(重要):
「PHIはSSA値として他blockでも有効」
- PHI値はdefining blockのみでなく、dominate先でも有効
- snapshot上のPHIを「miss」扱いしてはならない
- PHI値は一度定義されたら変更されないSSA不変条件
Fail-Fast:
- PHI値を「未定義」扱い → AssertionError
- snapshot miss時にPHI値を無視 → AssertionError
この契約の破綻により過去に以下の問題が発生:
- PHI値が他blockで「未定義」扱いされる
- snapshot miss時にPHI値が消失
- SSA不変条件の破綻
"""
@staticmethod
def is_phi_valid_at(phi_id: Any, block_id: Any, dominator_info: dict) -> bool:
"""PHI値が指定blockで有効か判定
契約: PHI値は defining block および dominate先で有効
Args:
phi_id: PHI ValueId
block_id: 参照block
dominator_info: dominator情報
Returns:
有効ならTrue
"""
phi_block = dominator_info.get_defining_block(phi_id)
# PHI値のdefining blockまたはdominate先なら有効
if block_id == phi_block:
return True
if dominator_info.dominates(phi_block, block_id):
return True
return False
@staticmethod
def resolve_phi_at_snapshot(phi_id: Any, snapshot: dict,
resolver: Any) -> Optional[Any]:
"""Snapshot上でPHI値を解決
契約: snapshot miss時もPHI値を返すmiss扱いしない
Args:
phi_id: PHI ValueId
snapshot: block終端のsnapshot
resolver: Value resolver
Returns:
PHI値snapshot missでもPHI定義値を返す
"""
# まずsnapshotを確認
if phi_id in snapshot:
return snapshot[phi_id]
# snapshot miss時: PHI定義値を返すmiss扱いしない
if resolver and hasattr(resolver, 'get_phi_definition'):
return resolver.get_phi_definition(phi_id)
# PHI値が取得できない場合は契約違反
raise AssertionError(
f"[PhiSnapshotPolicyBox] Cannot resolve PHI value: {phi_id}"
)
@staticmethod
def is_phi(value_id: Any, resolver: Any) -> bool:
"""ValueIdがPHI値か判定
Args:
value_id: ValueId
resolver: Value resolver
Returns:
PHI値ならTrue
"""
if resolver and hasattr(resolver, 'is_phi'):
return resolver.is_phi(value_id)
# Fallback: PHI判定ができない場合はFalse
return False

111
src/llvm_py/type_facts.py Normal file
View File

@ -0,0 +1,111 @@
"""Type Facts Box - 型情報伝播のSSoT
Phase 97 Refactoring: stringish等のtype tag伝播を一箇所に集約。
"""
from typing import Dict, Set, Any
class TypeFactsBox:
"""型情報Type Factsの管理と伝播Box
責務:
- 型tagstringish等の登録・取得
- Copy命令での型伝播
- PHI命令での型伝播
- 伝播ルールのSSoT化
契約:
- 入力: ValueId、型情報
- 出力: 伝播後の型情報
- 不変条件: 一度tagが付いたValueIdは変更不可monotonic
Fail-Fast:
- 矛盾する型tag → AssertionError
設計原則:
- monotonic: 型情報は追加のみ、削除・変更は禁止
- explicit: 暗黙的な型推論は行わない、明示的なtagのみ
"""
def __init__(self):
self._facts: Dict[Any, Dict[str, Any]] = {}
def mark_string(self, value_id: Any, reason: str = "explicit"):
"""ValueIdをstringishとしてマーク
Args:
value_id: ValueId
reason: マーク理由(デバッグ用)
"""
if value_id not in self._facts:
self._facts[value_id] = {}
# monotonic check
if "stringish" in self._facts[value_id]:
assert self._facts[value_id]["stringish"], \
f"[TypeFactsBox] Cannot change stringish tag for {value_id}"
self._facts[value_id]["stringish"] = True
self._facts[value_id]["reason"] = reason
def propagate_copy(self, dst: Any, src: Any):
"""Copy命令での型伝播
契約: dst = copy src → dst inherits src's type facts
Args:
dst: コピー先ValueId
src: コピー元ValueId
"""
if src in self._facts:
src_facts = self._facts[src].copy()
src_facts["reason"] = f"copy from {src}"
self._facts[dst] = src_facts
def propagate_phi(self, phi_id: Any, incoming_ids: list):
"""PHI命令での型伝播
契約: phi = PHI [v1, v2, ...] → phi inherits common type facts
Args:
phi_id: PHI結果ValueId
incoming_ids: PHI入力ValueId list
"""
# 全入力が同じtype factを持つ場合のみ伝播
if not incoming_ids:
return
# 最初の入力の型情報を基準
first_facts = self._facts.get(incoming_ids[0], {})
# 全入力が同じstringish tagを持つか確認
all_stringish = all(
self._facts.get(vid, {}).get("stringish", False)
for vid in incoming_ids
)
if all_stringish and "stringish" in first_facts:
self.mark_string(phi_id, reason=f"phi from {incoming_ids}")
def is_stringish(self, value_id: Any) -> bool:
"""ValueIdがstringishか判定
Args:
value_id: ValueId
Returns:
stringishならTrue
"""
return self._facts.get(value_id, {}).get("stringish", False)
def get_facts(self, value_id: Any) -> dict:
"""ValueIdの型情報を取得
Args:
value_id: ValueId
Returns:
型情報dict存在しない場合は空dict
"""
return self._facts.get(value_id, {}).copy()