feat(anf): Phase 145 P0/P1/P2 - ANF (A-Normal Form) transformation
Implement ANF transformation for impure expressions to fix evaluation order: Phase 145 P0 (Skeleton): - Add anf/ module with contract/plan/execute 3-layer separation - AnfDiagnosticTag, AnfOutOfScopeReason, AnfPlan enums - Stub execute_box (always returns Ok(None)) - 11 unit tests pass Phase 145 P1 (Minimal success): - String.length() whitelist implementation - BinaryOp + MethodCall pattern: x + s.length() → t = s.length(); result = x + t - Exit code 12 verification (VM + LLVM EXE) - 17 unit tests pass Phase 145 P2 (Generalization): - Recursive ANF for compound expressions - Left-to-right, depth-first evaluation order - Patterns: x + s.length() + z, s1.length() + s2.length() - ANF strict mode (HAKO_ANF_STRICT=1) - Diagnostic tags (joinir/anf/*) - 21 unit tests pass, 0 regression Also includes Phase 143 P2 (else symmetry) completion. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@ -36,6 +36,10 @@ Scope: Repo root の旧リンク互換。現行の入口は `docs/development/cu
|
||||
- Historical context: `docs/development/current/main/investigations/joinir-generalization-study.md`
|
||||
- Phase 143-loopvocab P0/P1: loop 内 if/break/continue の語彙追加(DONE)
|
||||
- `docs/development/current/main/phases/phase-143-loopvocab/README.md`
|
||||
- Phase 143-loopvocab P2: else 対称化(B-C / C-B)(DONE)
|
||||
- `docs/development/current/main/phases/phase-143-loopvocab/README.md`
|
||||
- Phase 145-anf P0/P1/P2: ANF(impure hoist + 再帰的線形化)(DONE)
|
||||
- `docs/development/current/main/phases/phase-145-anf/README.md`
|
||||
|
||||
## Resolved (historical)
|
||||
|
||||
|
||||
28
apps/tests/phase143_loop_true_if_continue_min.hako
Normal file
28
apps/tests/phase143_loop_true_if_continue_min.hako
Normal file
@ -0,0 +1,28 @@
|
||||
// Phase 143 P1: loop(true) + if + continue minimal test
|
||||
//
|
||||
// Pattern: loop(true) { if(cond_pure) continue }
|
||||
// Expected: non-terminating (Phase 143 P1 scope)
|
||||
//
|
||||
// This test verifies:
|
||||
// - loop(true) is recognized
|
||||
// - Pure condition (counter < 1) is lowerable
|
||||
// - Continue path executes (jumps to loop_step instead of k_exit)
|
||||
// - Infinite loop with continue (never exits via condition)
|
||||
//
|
||||
// Smoke contract:
|
||||
// - VM/LLVM runs should time out (expected), not fail fast.
|
||||
|
||||
static box Main {
|
||||
main() {
|
||||
local counter
|
||||
counter = 0
|
||||
|
||||
loop(true) {
|
||||
if counter < 1 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return 100
|
||||
}
|
||||
}
|
||||
14
apps/tests/phase143_p2_loop_true_if_bc_min.hako
Normal file
14
apps/tests/phase143_p2_loop_true_if_bc_min.hako
Normal file
@ -0,0 +1,14 @@
|
||||
// Phase 143 P2: Break-Continue pattern (exit code 8)
|
||||
static box Main {
|
||||
main() {
|
||||
local flag = 1
|
||||
loop(true) {
|
||||
if flag == 1 {
|
||||
break
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return 8
|
||||
}
|
||||
}
|
||||
14
apps/tests/phase143_p2_loop_true_if_cb_min.hako
Normal file
14
apps/tests/phase143_p2_loop_true_if_cb_min.hako
Normal file
@ -0,0 +1,14 @@
|
||||
// Phase 143 P2: Continue-Break pattern (exit code 9)
|
||||
static box Main {
|
||||
main() {
|
||||
local flag = 0
|
||||
loop(true) {
|
||||
if flag == 1 {
|
||||
continue
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return 9
|
||||
}
|
||||
}
|
||||
22
apps/tests/phase145_p1_anf_length_min.hako
Normal file
22
apps/tests/phase145_p1_anf_length_min.hako
Normal file
@ -0,0 +1,22 @@
|
||||
// Phase 145 P1: ANF minimal test - String.length() hoist in BinaryOp
|
||||
//
|
||||
// Pattern: x + s.length()
|
||||
// ↓ ANF transformation
|
||||
// t = s.length()
|
||||
// result = x + t
|
||||
//
|
||||
// Expected: exit code 12 (5 + 3 + 4)
|
||||
|
||||
static box Main {
|
||||
main() {
|
||||
local s
|
||||
s = "abc" // String with length 3
|
||||
|
||||
local x
|
||||
x = 5
|
||||
|
||||
local result
|
||||
result = x + s.length() // 5 + 3 = 8
|
||||
return result + 4 // 8 + 4 = 12
|
||||
}
|
||||
}
|
||||
27
apps/tests/phase145_p2_compound_expr_binop_min.hako
Normal file
27
apps/tests/phase145_p2_compound_expr_binop_min.hako
Normal file
@ -0,0 +1,27 @@
|
||||
// Phase 145 P2: Compound expression with nested BinaryOp + MethodCall
|
||||
// Pattern: x + s.length() + z
|
||||
// Expected: t1 = s.length(); t2 = x + t1; result = t2 + z
|
||||
// Exit code: 18 (10 + 5 + 3 = 18)
|
||||
|
||||
static box Main {
|
||||
main() {
|
||||
local s
|
||||
s = "Hello" // length = 5
|
||||
|
||||
local x
|
||||
x = 10
|
||||
|
||||
local z
|
||||
z = 3
|
||||
|
||||
// Compound expression: x + s.length() + z
|
||||
// Should normalize to:
|
||||
// t1 = s.length() (= 5)
|
||||
// t2 = x + t1 (= 10 + 5 = 15)
|
||||
// result = t2 + z (= 15 + 3 = 18)
|
||||
local result
|
||||
result = x + s.length() + z
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
// Phase 145 P2: Compound expression with two MethodCall operands
|
||||
// Pattern: s1.length() + s2.length()
|
||||
// Expected: t1 = s1.length(); t2 = s2.length(); result = t1 + t2
|
||||
// Exit code: 5 (2 + 3 = 5)
|
||||
|
||||
static box Main {
|
||||
main() {
|
||||
local s1
|
||||
s1 = "Hi" // length = 2
|
||||
|
||||
local s2
|
||||
s2 = "Bye" // length = 3
|
||||
|
||||
// Compound expression: s1.length() + s2.length()
|
||||
// Should normalize to:
|
||||
// t1 = s1.length() (= 2)
|
||||
// t2 = s2.length() (= 3)
|
||||
// result = t1 + t2 (= 2 + 3 = 5)
|
||||
local result
|
||||
result = s1.length() + s2.length()
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
@ -2,12 +2,34 @@
|
||||
|
||||
## Next (planned)
|
||||
|
||||
- Phase 141 P2+: Call/MethodCall 対応(effects + typing を分離して段階投入)
|
||||
- Phase 143-loopvocab P2+: Loop-If-Exit パターン拡張(else/対称branch対応)
|
||||
- P2: else/対称branch 対応(continue/break の混合パターン)
|
||||
- P3+: 条件スコープ拡張(impure expressions 対応)
|
||||
- Phase 141 P2+: Call/MethodCall 対応(effects + typing を分離して段階投入、ANF を前提に順序固定)
|
||||
- Phase 143-loopvocab P3+: 条件スコープ拡張(impure conditions 対応)
|
||||
- Phase 146-147(planned): Loop/If condition への ANF 適用(順序固定と診断の横展開)
|
||||
- 詳細: `docs/development/current/main/30-Backlog.md`
|
||||
|
||||
## 2025-12-19:Phase 145-anf P0/P1/P2 完了 ✅
|
||||
|
||||
- SSOT docs:
|
||||
- `docs/development/current/main/phases/phase-145-anf/README.md`
|
||||
- `docs/development/current/main/phases/phase-144-anf/INSTRUCTIONS.md`
|
||||
- 実装 SSOT:
|
||||
- `src/mir/control_tree/normalized_shadow/anf/`
|
||||
- 入口(接続箇所 SSOT): `src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs`
|
||||
- 環境変数:
|
||||
- `HAKO_ANF_DEV=1`(dev-only: ANF 有効化)
|
||||
- `HAKO_ANF_STRICT=1`(dev-only: ANF fail-fast)
|
||||
- Fixtures & smokes(VM/LLVM EXE parity):
|
||||
- `apps/tests/phase145_p1_anf_length_min.hako` → exit 12
|
||||
- `apps/tests/phase145_p2_compound_expr_binop_min.hako` → exit 18
|
||||
- `apps/tests/phase145_p2_compound_expr_double_intrinsic_min.hako` → exit 5
|
||||
|
||||
## 2025-12-19:Phase 143-loopvocab P2 完了 ✅
|
||||
|
||||
- 対象: else 対称化(B-C / C-B)
|
||||
- Fixtures & smokes(VM/LLVM EXE parity):
|
||||
- `apps/tests/phase143_p2_loop_true_if_bc_min.hako` → exit 8
|
||||
- `apps/tests/phase143_p2_loop_true_if_cb_min.hako` → exit 9
|
||||
|
||||
## 2025-12-19:Phase 143 実行系契約修正 ✅
|
||||
|
||||
**問題**: normalized_helpers が env params を `ValueId(1,2...)` で割り当てていた(PHI Reserved 領域 0-99)。
|
||||
|
||||
@ -46,14 +46,12 @@ Related:
|
||||
- Smoke: VM + LLVM EXE
|
||||
- Out-of-scope は `Ok(None)` のまま
|
||||
|
||||
- **Phase 143-loopvocab P2(planned): else 対応(break/continue 対称化)**
|
||||
- 対象: `if(cond){break}else{continue}` と `if(cond){continue}else{break}` を追加
|
||||
- 実装:
|
||||
- LoopIfExitShape で `has_else=true` + symmetric `then/else_` を許可
|
||||
- Contract で 4パターンを明示(P0: no-else, P1: no-else+continue, P2: with-else)
|
||||
- Fixtures: 2本(対称ケース)
|
||||
- Smoke: VM/LLVM EXE
|
||||
- 完了で「語彙として完成」に寄せる
|
||||
(DONE)Phase 143-loopvocab P2: else 対称化(B-C / C-B)
|
||||
- 記録: `docs/development/current/main/10-Now.md`
|
||||
|
||||
- **Phase 143-loopvocab P3+(planned): impure conditions 対応**
|
||||
- 目的: `if(cond_impure) break/continue` を ANF/順序固定の上で段階投入する
|
||||
- 方針: Phase 145-anf の契約(hoist + left-to-right)を条件式にも適用
|
||||
|
||||
- **real-app loop regression の横展開(VM + LLVM EXE)**
|
||||
- ねらい: 実コード由来ループを 1 本ずつ最小抽出して fixture/smoke で固定する(段階投入)。
|
||||
|
||||
1967
docs/development/current/main/phases/phase-144-anf/INSTRUCTIONS.md
Normal file
1967
docs/development/current/main/phases/phase-144-anf/INSTRUCTIONS.md
Normal file
File diff suppressed because it is too large
Load Diff
255
docs/development/current/main/phases/phase-145-anf/README.md
Normal file
255
docs/development/current/main/phases/phase-145-anf/README.md
Normal file
@ -0,0 +1,255 @@
|
||||
# Phase 145 P0: ANF (A-Normal Form) Skeleton Implementation
|
||||
|
||||
**Status**: Complete
|
||||
**Date**: 2025-12-19
|
||||
**Purpose**: Establish 3-layer ANF architecture (contract/plan/execute) without changing existing behavior
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Phase 145 P0 implements the skeleton for ANF (A-Normal Form) transformation in Normalized JoinIR, following the Phase 143 pattern of 3-layer separation (contract/plan/execute). **Existing behavior is unchanged** (P0 is non-invasive).
|
||||
|
||||
**Key Constraint**: execute_box always returns `Ok(None)` (stub), ensuring 0 regression.
|
||||
|
||||
**Next Steps**: P1 (String.length() hoist), P2 (compound expression ANF).
|
||||
|
||||
---
|
||||
|
||||
## Implementation Summary
|
||||
|
||||
### Files Created (5 + 1 doc)
|
||||
|
||||
**New Module** (`src/mir/control_tree/normalized_shadow/anf/`):
|
||||
1. `mod.rs` (~30 lines) - Module entry point + re-exports
|
||||
2. `contract.rs` (~200 lines) - 3 enums + 2 tests
|
||||
- `AnfDiagnosticTag` (OrderViolation, PureRequired, HoistFailed)
|
||||
- `AnfOutOfScopeReason` (ContainsCall, ContainsMethodCall, ...)
|
||||
- `AnfPlan` (requires_anf, impure_count)
|
||||
3. `plan_box.rs` (~200 lines) - AST walk + 4 tests
|
||||
- `plan_expr()`: Detect impure subexpressions (Call/MethodCall)
|
||||
- `is_pure()`: Helper for quick pure/impure discrimination
|
||||
4. `execute_box.rs` (~80 lines) - Stub + 1 test
|
||||
- `try_execute()`: Always returns `Ok(None)` (P0 stub)
|
||||
5. `README.md` (~100 lines) - Module architecture documentation
|
||||
|
||||
**Documentation**:
|
||||
6. `docs/development/current/main/phases/phase-145-anf/README.md` (this file)
|
||||
|
||||
### Files Modified (3)
|
||||
|
||||
1. `src/mir/control_tree/normalized_shadow/mod.rs` (+1 line)
|
||||
- Added `pub mod anf;`
|
||||
|
||||
2. `src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs` (+23 lines)
|
||||
- Added ANF routing at Line 54-76 (before out_of_scope_reason check)
|
||||
- Dev-only (`HAKO_ANF_DEV=1`)
|
||||
- Fallback to legacy when execute_box returns None
|
||||
|
||||
3. `src/config/env/joinir_dev.rs` (+26 lines)
|
||||
- Added `anf_dev_enabled()` function
|
||||
- Environment variable: `HAKO_ANF_DEV=1`
|
||||
|
||||
---
|
||||
|
||||
## Architecture (Box-First, 3-layer separation)
|
||||
|
||||
### Layer 1: contract.rs - Diagnostic tags & plan structure (SSOT)
|
||||
|
||||
**Responsibility**:
|
||||
- Define `AnfDiagnosticTag` enum (future error categorization)
|
||||
- Define `AnfOutOfScopeReason` enum (graceful Ok(None) fallback)
|
||||
- Define `AnfPlan` struct (requires_anf, impure_count)
|
||||
|
||||
**Design Pattern**: Enum discrimination (prevents if-branch explosion)
|
||||
|
||||
### Layer 2: plan_box.rs - AST pattern detection
|
||||
|
||||
**Responsibility**:
|
||||
- Walk AST to detect impure subexpressions (Call/MethodCall)
|
||||
- Build `AnfPlan` indicating what transformation is needed
|
||||
- Does NOT perform transformation (separation of concerns)
|
||||
|
||||
**API**:
|
||||
```rust
|
||||
pub fn plan_expr(
|
||||
ast: &ASTNode,
|
||||
env: &BTreeMap<String, ValueId>,
|
||||
) -> Result<Option<AnfPlan>, AnfOutOfScopeReason>
|
||||
```
|
||||
|
||||
**Returns**:
|
||||
- `Ok(Some(plan))`: Expression in scope (plan.requires_anf indicates if ANF needed)
|
||||
- `Ok(None)`: Expression out-of-scope (unknown AST node type)
|
||||
- `Err(reason)`: Expression explicitly out-of-scope (ContainsCall/ContainsMethodCall)
|
||||
|
||||
### Layer 3: execute_box.rs - ANF transformation execution (P0: stub)
|
||||
|
||||
**Responsibility**:
|
||||
- Execute ANF transformation for expressions that require it (per AnfPlan)
|
||||
- P0: Always returns `Ok(None)` (existing behavior unchanged)
|
||||
- P1+: Implement hoist + rebuild AST + lower
|
||||
|
||||
**API**:
|
||||
```rust
|
||||
pub fn try_execute(
|
||||
plan: &AnfPlan,
|
||||
ast: &ASTNode,
|
||||
env: &mut BTreeMap<String, ValueId>,
|
||||
body: &mut Vec<JoinInst>,
|
||||
next_value_id: &mut u32,
|
||||
) -> Result<Option<ValueId>, String>
|
||||
```
|
||||
|
||||
**Returns**:
|
||||
- `Ok(Some(vid))`: ANF transformation succeeded (P1+)
|
||||
- `Ok(None)`: Transformation not attempted (P0 stub)
|
||||
- `Err(msg)`: Internal error (strict mode only, P1+)
|
||||
|
||||
---
|
||||
|
||||
## Integration with expr_lowerer_box.rs
|
||||
|
||||
**Location**: `src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs`
|
||||
|
||||
**Routing (Line 54-76)**:
|
||||
```rust
|
||||
// Phase 145 P0: ANF routing (dev-only)
|
||||
if crate::config::env::anf_dev_enabled() {
|
||||
use super::super::anf::{AnfPlanBox, AnfExecuteBox};
|
||||
match AnfPlanBox::plan_expr(ast, env) {
|
||||
Ok(Some(plan)) => {
|
||||
match AnfExecuteBox::try_execute(&plan, ast, &mut env.clone(), body, next_value_id)? {
|
||||
Some(vid) => return Ok(Some(vid)), // P1+: ANF succeeded
|
||||
None => {
|
||||
// P0: stub returns None, fallback to legacy
|
||||
if crate::config::env::joinir_dev_enabled() {
|
||||
eprintln!("[phase145/debug] ANF plan found but execute returned None (P0 stub)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => { /* out-of-scope, continue */ }
|
||||
Err(_reason) => { /* out-of-scope, continue */ }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Environment Variable**:
|
||||
- `HAKO_ANF_DEV=1`: Enable ANF routing
|
||||
- Default: ANF routing disabled (0 impact)
|
||||
|
||||
**Debug Logging**:
|
||||
- `[phase145/debug] ANF plan found but execute returned None (P0 stub)`
|
||||
- `[phase145/debug] ANF execute called (P0 stub, returning Ok(None))`
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests (7 total)
|
||||
|
||||
**contract.rs (2 tests)**:
|
||||
- `test_anf_plan_pure`: AnfPlan::pure() construction
|
||||
- `test_anf_plan_impure`: AnfPlan::impure(n) construction
|
||||
|
||||
**plan_box.rs (4 tests)**:
|
||||
- `test_plan_pure_variable`: Variable → pure plan
|
||||
- `test_plan_pure_literal`: Literal → pure plan
|
||||
- `test_plan_pure_binop`: BinaryOp (pure operands) → pure plan
|
||||
- `test_plan_call_out_of_scope`: Call → Err(ContainsCall)
|
||||
|
||||
**execute_box.rs (1 test)**:
|
||||
- `test_execute_stub_returns_none`: P0 stub always returns Ok(None)
|
||||
|
||||
**Regression Tests**:
|
||||
- All existing tests pass (0 regression)
|
||||
- Phase 97/131/143 smoke tests unchanged
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] 5 new files created (anf/ module)
|
||||
- [x] 3 existing files modified (mod.rs, expr_lowerer_box.rs, joinir_dev.rs)
|
||||
- [x] 7 unit tests pass
|
||||
- [x] cargo build --release passes
|
||||
- [x] 0 regression (existing tests unchanged)
|
||||
- [x] Debug log with `HAKO_ANF_DEV=1`
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Phase 145 P1: String.length() hoist (最小成功例)
|
||||
|
||||
**Goal**: Implement ANF transformation for 1 known intrinsic (String.length()).
|
||||
|
||||
**Pattern**:
|
||||
```hako
|
||||
x + s.length()
|
||||
↓ ANF
|
||||
t = s.length()
|
||||
result = x + t
|
||||
```
|
||||
|
||||
**Implementation**:
|
||||
- contract.rs: Add hoist_targets to AnfPlan (~50 lines)
|
||||
- plan_box.rs: Whitelist check + BinaryOp pattern detection (~100 lines)
|
||||
- execute_box.rs: Stub → implementation (~150 lines)
|
||||
|
||||
**Fixtures**:
|
||||
- `apps/tests/phase145_p1_anf_length_min.hako` (exit code 12)
|
||||
- `tools/smokes/.../phase145_p1_anf_length_vm.sh`
|
||||
- `tools/smokes/.../phase145_p1_anf_length_llvm_exe.sh`
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- Exit code 12 (VM + LLVM EXE parity)
|
||||
- String.length() hoisted (JoinInst::MethodCall emitted first)
|
||||
- BinaryOp uses temp variable (not direct MethodCall)
|
||||
- Whitelist enforcement (other methods → Ok(None))
|
||||
|
||||
### Phase 145 P2: Compound expression ANF (再帰的線形化)
|
||||
|
||||
**Goal**: Implement recursive ANF for compound expressions (multiple MethodCalls).
|
||||
|
||||
**Patterns**:
|
||||
```hako
|
||||
// Pattern 1: x + s.length() + z
|
||||
// → t1 = s.length(); t2 = x + t1; result = t2 + z
|
||||
|
||||
// Pattern 2: s1.length() + s2.length()
|
||||
// → t1 = s1.length(); t2 = s2.length(); result = t1 + t2
|
||||
```
|
||||
|
||||
**Implementation**:
|
||||
- execute_box.rs: Recursive processing (left-to-right, depth-first) (~80 lines)
|
||||
- Diagnostic tags: error_tags.rs integration (~30 lines)
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- 2 fixtures pass (exit codes 18, 5)
|
||||
- Left-to-right order preserved
|
||||
- Recursive ANF documented
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
**Design SSOT**:
|
||||
- `docs/development/current/main/phases/phase-144-anf/INSTRUCTIONS.md` - ANF contract definition
|
||||
- `docs/development/current/main/design/normalized-expr-lowering.md` - ExprLowererBox SSOT
|
||||
|
||||
**Related Phases**:
|
||||
- Phase 140: NormalizedExprLowererBox (pure expression lowering)
|
||||
- Phase 143: LoopIfExitContract pattern (3-layer separation inspiration)
|
||||
- Phase 144: ANF docs-only specification
|
||||
|
||||
**Implementation SSOT**:
|
||||
- `src/mir/control_tree/normalized_shadow/anf/README.md` - Module architecture
|
||||
- `src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs` - Integration point
|
||||
- `src/config/env/joinir_dev.rs` - Environment variable helpers
|
||||
|
||||
---
|
||||
|
||||
**Revision History**:
|
||||
- 2025-12-19: Phase 145 P0 skeleton implemented (contract/plan/execute separation)
|
||||
62
src/config/env/joinir_dev.rs
vendored
62
src/config/env/joinir_dev.rs
vendored
@ -204,3 +204,65 @@ pub fn phi_metrics_enabled() -> bool {
|
||||
pub fn legacy_loopbuilder_enabled() -> bool {
|
||||
env_bool("NYASH_LEGACY_LOOPBUILDER")
|
||||
}
|
||||
|
||||
/// Phase 145 P0: HAKO_ANF_DEV=1 - ANF (A-Normal Form) transformation development mode
|
||||
///
|
||||
/// Enables ANF transformation routing in NormalizedExprLowererBox.
|
||||
/// P0: Debug logging only (execute_box is stub, returns Ok(None)).
|
||||
/// P1+: Actual transformation (String.length() hoist, compound expression ANF).
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
/// ```bash
|
||||
/// HAKO_ANF_DEV=1 cargo test --release
|
||||
/// HAKO_ANF_DEV=1 ./target/release/hakorune program.hako
|
||||
/// ```
|
||||
///
|
||||
/// # Expected Behavior (P0)
|
||||
///
|
||||
/// - ANF routing enabled: AnfPlanBox detects impure expressions
|
||||
/// - Debug log: "[phase145/debug] ANF plan found but execute returned None (P0 stub)"
|
||||
/// - Existing behavior unchanged: execute_box returns Ok(None) → fallback to legacy
|
||||
///
|
||||
/// # Future Behavior (P1+)
|
||||
///
|
||||
/// - String.length() hoist: `x + s.length()` → ANF transformation
|
||||
/// - Compound expression ANF: Recursive left-to-right linearization
|
||||
pub fn anf_dev_enabled() -> bool {
|
||||
env_bool("HAKO_ANF_DEV")
|
||||
}
|
||||
|
||||
/// Phase 145 P2: ANF strict mode (fail-fast on violations)
|
||||
///
|
||||
/// When enabled, ANF transformation errors result in immediate failure
|
||||
/// instead of graceful fallback to legacy lowering.
|
||||
///
|
||||
/// # Environment Variable
|
||||
///
|
||||
/// `HAKO_ANF_STRICT=1` enables strict mode (default: OFF)
|
||||
///
|
||||
/// # Behavior
|
||||
///
|
||||
/// - **ON**: ANF violations return Err() with detailed error tags
|
||||
/// - **OFF**: ANF violations gracefully fallback to legacy lowering (Ok(None))
|
||||
///
|
||||
/// # Use Cases
|
||||
///
|
||||
/// - **Development**: Catch order violations, pure-required violations early
|
||||
/// - **Testing**: Verify ANF transformation correctness with fail-fast
|
||||
/// - **Production**: Keep OFF for backward compatibility
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```bash
|
||||
/// # Strict mode: Fail on `f() + g()` without ANF
|
||||
/// HAKO_ANF_STRICT=1 ./hakorune program.hako
|
||||
/// # Error: [joinir/anf/order_violation] f() + g(): both calls not hoisted
|
||||
///
|
||||
/// # Graceful mode (default): Fallback to legacy
|
||||
/// ./hakorune program.hako
|
||||
/// # OK: Legacy lowering used
|
||||
/// ```
|
||||
pub fn anf_strict_enabled() -> bool {
|
||||
env_bool("HAKO_ANF_STRICT")
|
||||
}
|
||||
|
||||
@ -7,6 +7,8 @@ use super::merge_result::MergeContracts;
|
||||
#[cfg(debug_assertions)]
|
||||
use super::LoopHeaderPhiInfo;
|
||||
#[cfg(debug_assertions)]
|
||||
use crate::mir::BasicBlockId;
|
||||
#[cfg(debug_assertions)]
|
||||
use crate::mir::join_ir::lowering::join_value_space::{LOCAL_MAX, PARAM_MAX, PARAM_MIN};
|
||||
#[cfg(debug_assertions)]
|
||||
use std::collections::HashMap;
|
||||
|
||||
210
src/mir/control_tree/normalized_shadow/anf/README.md
Normal file
210
src/mir/control_tree/normalized_shadow/anf/README.md
Normal file
@ -0,0 +1,210 @@
|
||||
# Phase 145 P0: ANF (A-Normal Form) Module
|
||||
|
||||
**Status**: Skeleton implemented (P0 complete)
|
||||
**Date**: 2025-12-19
|
||||
**Purpose**: Deterministic evaluation order for impure expressions (Call/MethodCall)
|
||||
|
||||
---
|
||||
|
||||
## Module Architecture (Box-First, 3-layer separation)
|
||||
|
||||
### contract.rs - Diagnostic tags, out-of-scope reasons, plan structure (SSOT)
|
||||
|
||||
**Responsibility**:
|
||||
- Defines `AnfDiagnosticTag` enum (OrderViolation, PureRequired, HoistFailed)
|
||||
- Defines `AnfOutOfScopeReason` enum (ContainsCall, ContainsMethodCall, ...)
|
||||
- Defines `AnfPlan` struct (requires_anf, impure_count)
|
||||
|
||||
**Phase Scope**:
|
||||
- **P0**: Enum definitions only (not yet used in execute_box)
|
||||
- **P1+**: Add hoist_targets, parent_kind to AnfPlan
|
||||
|
||||
**Design Pattern**: Enum discrimination (prevents if-branch explosion)
|
||||
|
||||
### plan_box.rs - AST pattern detection
|
||||
|
||||
**Responsibility**:
|
||||
- Walk AST expression to detect impure subexpressions (Call/MethodCall)
|
||||
- Build AnfPlan indicating what transformation is needed
|
||||
- Does NOT perform transformation (separation of concerns)
|
||||
|
||||
**Phase Scope**:
|
||||
- **P0**: Basic impure detection (Call/MethodCall presence)
|
||||
- **P1+**: Add whitelist check (e.g., String.length()), parent_kind detection
|
||||
|
||||
**API**:
|
||||
```rust
|
||||
pub fn plan_expr(
|
||||
ast: &ASTNode,
|
||||
env: &BTreeMap<String, ValueId>,
|
||||
) -> Result<Option<AnfPlan>, AnfOutOfScopeReason>
|
||||
```
|
||||
|
||||
**Returns**:
|
||||
- `Ok(Some(plan))`: Expression is in scope (plan.requires_anf indicates if ANF needed)
|
||||
- `Ok(None)`: Expression is out-of-scope (unknown AST node type)
|
||||
- `Err(reason)`: Expression is explicitly out-of-scope (e.g., nested impure)
|
||||
|
||||
### execute_box.rs - ANF transformation execution
|
||||
|
||||
**Responsibility**:
|
||||
- Execute ANF transformation for expressions that require it (per AnfPlan)
|
||||
- Hoist impure subexpressions to temporary variables
|
||||
- Emit transformed JoinInsts
|
||||
|
||||
**Phase Scope**:
|
||||
- **P0**: Stub only (always returns Ok(None), existing behavior unchanged)
|
||||
- **P1**: Implement String.length() hoist (whitelist 1 intrinsic)
|
||||
- **P2**: Implement recursive compound expression ANF
|
||||
|
||||
**API**:
|
||||
```rust
|
||||
pub fn try_execute(
|
||||
plan: &AnfPlan,
|
||||
ast: &ASTNode,
|
||||
env: &mut BTreeMap<String, ValueId>,
|
||||
body: &mut Vec<JoinInst>,
|
||||
next_value_id: &mut u32,
|
||||
) -> Result<Option<ValueId>, String>
|
||||
```
|
||||
|
||||
**Returns**:
|
||||
- `Ok(Some(vid))`: ANF transformation succeeded, result is ValueId (P1+)
|
||||
- `Ok(None)`: Transformation not attempted (P0 stub) or out-of-scope
|
||||
- `Err(msg)`: Internal error (strict mode only, P1+)
|
||||
|
||||
---
|
||||
|
||||
## Integration with expr_lowerer_box.rs
|
||||
|
||||
**Location**: `src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs`
|
||||
|
||||
**Routing (Line 54-57)**:
|
||||
```rust
|
||||
if crate::config::env::anf_dev_enabled() {
|
||||
match AnfPlanBox::plan_expr(ast, env)? {
|
||||
Ok(Some(plan)) => match AnfExecuteBox::try_execute(plan, ast, env, body, next_value_id)? {
|
||||
Ok(Some(vid)) => return Ok(Some(vid)), // P1+: ANF succeeded
|
||||
Ok(None) => { /* fallback to legacy */ } // P0: stub returns None
|
||||
},
|
||||
Ok(None) => { /* out-of-scope, continue */ }
|
||||
Err(reason) => { /* out-of-scope, continue */ }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Environment Variable**:
|
||||
- `HAKO_ANF_DEV=1`: Enable ANF routing (P0: debug logging only)
|
||||
- Default: ANF routing disabled (0 impact on existing behavior)
|
||||
|
||||
---
|
||||
|
||||
## Phase Scope Summary
|
||||
|
||||
### P0 (Skeleton) - Current Status
|
||||
|
||||
**Goal**: Establish 3-layer architecture without changing existing behavior.
|
||||
|
||||
**Implemented**:
|
||||
- ✅ contract.rs: 3 enums (AnfDiagnosticTag, AnfOutOfScopeReason, AnfPlan)
|
||||
- ✅ plan_box.rs: AST walk with basic impure detection
|
||||
- ✅ execute_box.rs: Stub (always returns Ok(None))
|
||||
- ✅ Integration: expr_lowerer_box.rs routing (dev-only, no impact)
|
||||
- ✅ Tests: 7 unit tests (contract: 2, plan: 4, execute: 1)
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- ✅ cargo build --release passes
|
||||
- ✅ 7 unit tests pass
|
||||
- ✅ 0 regression (existing tests unchanged)
|
||||
- ✅ HAKO_ANF_DEV=1 debug logging works
|
||||
|
||||
### P1 (String.length() hoist) - Next Phase
|
||||
|
||||
**Goal**: Implement ANF transformation for 1 known intrinsic (String.length()).
|
||||
|
||||
**Pattern**:
|
||||
```hako
|
||||
x + s.length()
|
||||
↓ ANF
|
||||
t = s.length()
|
||||
result = x + t
|
||||
```
|
||||
|
||||
**Implementation**:
|
||||
- contract.rs: Add hoist_targets to AnfPlan
|
||||
- plan_box.rs: Whitelist check + BinaryOp pattern detection
|
||||
- execute_box.rs: Stub → implementation (hoist + rebuild AST + lower)
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- Fixture: `phase145_p1_anf_length_min.hako` (exit code 12)
|
||||
- VM + LLVM EXE parity
|
||||
- Whitelist enforcement (other methods → Ok(None))
|
||||
|
||||
### P2 (Compound expression ANF) - Future Phase
|
||||
|
||||
**Goal**: Implement recursive ANF for compound expressions (multiple MethodCalls).
|
||||
|
||||
**Patterns**:
|
||||
```hako
|
||||
// Pattern 1: x + s.length() + z
|
||||
// → t1 = s.length(); t2 = x + t1; result = t2 + z
|
||||
|
||||
// Pattern 2: s1.length() + s2.length()
|
||||
// → t1 = s1.length(); t2 = s2.length(); result = t1 + t2
|
||||
```
|
||||
|
||||
**Implementation**:
|
||||
- execute_box.rs: Recursive processing (left-to-right, depth-first)
|
||||
- Diagnostic tags: error_tags.rs integration
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests (7 total)
|
||||
|
||||
**contract.rs (2 tests)**:
|
||||
- `test_anf_plan_pure`: AnfPlan::pure() construction
|
||||
- `test_anf_plan_impure`: AnfPlan::impure(n) construction
|
||||
|
||||
**plan_box.rs (4 tests)**:
|
||||
- `test_plan_pure_variable`: Variable → pure plan
|
||||
- `test_plan_pure_literal`: Literal → pure plan
|
||||
- `test_plan_pure_binop`: BinaryOp (pure operands) → pure plan
|
||||
- `test_plan_call_out_of_scope`: Call → Err(ContainsCall)
|
||||
|
||||
**execute_box.rs (1 test)**:
|
||||
- `test_execute_stub_returns_none`: P0 stub always returns Ok(None)
|
||||
|
||||
### Integration Tests (P1+)
|
||||
|
||||
**Fixtures**:
|
||||
- `apps/tests/phase145_p1_anf_length_min.hako` (P1)
|
||||
- `apps/tests/phase145_p2_compound_expr_binop_min.hako` (P2)
|
||||
- `apps/tests/phase145_p2_compound_expr_double_intrinsic_min.hako` (P2)
|
||||
|
||||
**Smoke Tests**:
|
||||
- VM: `tools/smokes/.../phase145_p*_vm.sh`
|
||||
- LLVM EXE: `tools/smokes/.../phase145_p*_llvm_exe.sh`
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
**Design SSOT**:
|
||||
- `docs/development/current/main/phases/phase-144-anf/INSTRUCTIONS.md` - ANF contract definition
|
||||
- `docs/development/current/main/design/normalized-expr-lowering.md` - ExprLowererBox SSOT
|
||||
|
||||
**Related Phases**:
|
||||
- Phase 140: NormalizedExprLowererBox (pure expression lowering)
|
||||
- Phase 143: LoopIfExitContract pattern (3-layer separation inspiration)
|
||||
- Phase 144: ANF docs-only specification
|
||||
|
||||
**Implementation SSOT**:
|
||||
- `src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs` - Integration point
|
||||
- `src/config/env/joinir_dev.rs` - Environment variable helpers
|
||||
|
||||
---
|
||||
|
||||
**Revision History**:
|
||||
- 2025-12-19: Phase 145 P0 skeleton implemented (contract/plan/execute separation)
|
||||
236
src/mir/control_tree/normalized_shadow/anf/contract.rs
Normal file
236
src/mir/control_tree/normalized_shadow/anf/contract.rs
Normal file
@ -0,0 +1,236 @@
|
||||
//! Phase 145 P0: ANF Contract (SSOT for diagnostic tags, out-of-scope reasons, plan structure)
|
||||
//!
|
||||
//! ## Purpose
|
||||
//!
|
||||
//! Defines the contract for ANF transformation in Normalized JoinIR:
|
||||
//! - **AnfDiagnosticTag**: Diagnostic categories for ANF violations
|
||||
//! - **AnfOutOfScopeReason**: Explicit out-of-scope cases (graceful Ok(None) fallback)
|
||||
//! - **AnfPlan**: What ANF transformation is needed (requires_anf?, impure_count?)
|
||||
//!
|
||||
//! ## Design Principle (Box-First)
|
||||
//!
|
||||
//! **Enum discrimination** prevents branching explosion:
|
||||
//! - P0: Skeleton only (no actual transformation)
|
||||
//! - P1: Add whitelist check + BinaryOp pattern detection
|
||||
//! - P2: Add recursive processing for compound expressions
|
||||
//! - **No nested if-statements**: Each out-of-scope case = enum variant
|
||||
//!
|
||||
//! ## Phase Scope
|
||||
//!
|
||||
//! - **P0**: Contract definition only (execute_box is stub)
|
||||
//! - **P1+**: Add hoist_targets, parent_kind to AnfPlan
|
||||
|
||||
/// Diagnostic tag for ANF-related errors (SSOT for error categorization)
|
||||
///
|
||||
/// Used to generate structured error messages in Phase 145+.
|
||||
/// Tags follow the format: `[joinir/anf/{tag}]`
|
||||
///
|
||||
/// ## Phase Scope
|
||||
///
|
||||
/// - **P0**: Enum definition only (not yet used in execute_box)
|
||||
/// - **P1+**: Used in error_tags.rs helper functions
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AnfDiagnosticTag {
|
||||
/// Order violation: Impure expression in immediate position
|
||||
///
|
||||
/// Example: `x = f() + g()` (both f() and g() not hoisted)
|
||||
/// Tag: `[joinir/anf/order_violation]`
|
||||
OrderViolation,
|
||||
|
||||
/// Pure required: Impure expression in pure-only scope
|
||||
///
|
||||
/// Example: `loop(iter.hasNext()) { ... }` (impure in loop condition)
|
||||
/// Tag: `[joinir/anf/pure_required]`
|
||||
PureRequired,
|
||||
|
||||
/// Hoist failed: Loop/If condition hoist failed
|
||||
///
|
||||
/// Example: `loop(f(g(), h())) { ... }` (complex nested call)
|
||||
/// Tag: `[joinir/anf/hoist_failed]`
|
||||
HoistFailed,
|
||||
}
|
||||
|
||||
/// Out-of-scope reason for ANF transformation (graceful Ok(None) fallback)
|
||||
///
|
||||
/// Each variant represents a specific case where ANF transformation is not applicable.
|
||||
/// Lowering code matches on these to determine whether to fall back to Ok(None) (graceful)
|
||||
/// or return an error (internal mistake).
|
||||
///
|
||||
/// ## Phase Scope
|
||||
///
|
||||
/// - **P0**: ContainsCall, ContainsMethodCall only (basic detection)
|
||||
/// - **P1+**: Add more granular reasons (e.g., IntrinsicNotWhitelisted)
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AnfOutOfScopeReason {
|
||||
/// Expression contains Call (function call)
|
||||
///
|
||||
/// Example: `f()` (P0 does not transform Call)
|
||||
ContainsCall,
|
||||
|
||||
/// Expression contains MethodCall
|
||||
///
|
||||
/// Example: `obj.method()` (P0 does not transform MethodCall)
|
||||
ContainsMethodCall,
|
||||
|
||||
/// Expression contains nested impure (P2+ feature)
|
||||
///
|
||||
/// Example: `f(g())` (nested call requires recursive ANF, out-of-scope for P0/P1)
|
||||
NestedImpure,
|
||||
|
||||
/// Condition lowering failed (impure in pure-only scope)
|
||||
///
|
||||
/// Captures lowering error detail.
|
||||
/// Example: `loop(s.length() > 0) { ... }` (impure in loop condition)
|
||||
CondLoweringFailed(String),
|
||||
|
||||
/// P0 catch-all: Unknown expression type
|
||||
///
|
||||
/// Used when AST node is not recognized by plan_box (safe fallback).
|
||||
/// Example: `new SomeBox()`, `field.access`, etc.
|
||||
UnknownExpressionType,
|
||||
}
|
||||
|
||||
/// Phase 145 P1: Hoist position in parent expression
|
||||
///
|
||||
/// Indicates where a MethodCall appears in its parent BinaryOp/UnaryOp.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum HoistPosition {
|
||||
/// MethodCall is the left operand of BinaryOp
|
||||
Left,
|
||||
/// MethodCall is the right operand of BinaryOp
|
||||
Right,
|
||||
/// MethodCall is the operand of UnaryOp (P2+)
|
||||
Operand,
|
||||
}
|
||||
|
||||
/// Phase 145 P1: Parent expression kind
|
||||
///
|
||||
/// Indicates the context where ANF transformation occurs.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AnfParentKind {
|
||||
/// Parent is BinaryOp (e.g., `x + s.length()`)
|
||||
BinaryOp,
|
||||
/// Parent is UnaryOp (e.g., `not s.isEmpty()`) (P2+)
|
||||
UnaryOp,
|
||||
/// Parent is MethodCall (chained, e.g., `s.trim().length()`) (P2+)
|
||||
MethodCall,
|
||||
/// Parent is Call (nested, e.g., `f(g())`) (P2+)
|
||||
Call,
|
||||
}
|
||||
|
||||
/// Phase 145 P1: Hoist target metadata
|
||||
///
|
||||
/// Describes a MethodCall that needs to be hoisted to a temporary variable.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AnfHoistTarget {
|
||||
/// The known intrinsic type (e.g., KnownIntrinsic::Length0)
|
||||
pub intrinsic: crate::mir::control_tree::normalized_shadow::common::expr_lowering_contract::KnownIntrinsic,
|
||||
|
||||
/// The AST node of the MethodCall to hoist
|
||||
pub ast_node: crate::ast::ASTNode,
|
||||
|
||||
/// Position in parent expression (Left/Right for BinaryOp)
|
||||
pub position: HoistPosition,
|
||||
}
|
||||
|
||||
/// ANF Plan: What ANF transformation is needed for an expression
|
||||
///
|
||||
/// Built by `AnfPlanBox::plan_expr()` to communicate what transformation is required.
|
||||
///
|
||||
/// ## Phase Scope
|
||||
///
|
||||
/// - **P0**: Minimal plan (requires_anf + impure_count only)
|
||||
/// - **P1**: Add hoist_targets (which MethodCalls to hoist), parent_kind (BinaryOp context)
|
||||
/// - **P2**: Add recursive processing for compound expressions
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AnfPlan {
|
||||
/// Whether ANF transformation is required
|
||||
///
|
||||
/// - `true`: Expression contains impure subexpressions (Call/MethodCall)
|
||||
/// - `false`: Expression is pure (variables, literals, arithmetic, comparisons)
|
||||
pub requires_anf: bool,
|
||||
|
||||
/// Number of impure subexpressions detected
|
||||
///
|
||||
/// Used for diagnostic logging (P0) and future optimization (P1+).
|
||||
/// Example: `f() + g()` → impure_count = 2
|
||||
pub impure_count: usize,
|
||||
|
||||
/// Phase 145 P1: Which MethodCalls to hoist
|
||||
///
|
||||
/// Contains metadata about each MethodCall that needs to be hoisted
|
||||
/// (intrinsic type, AST node, position in parent expression).
|
||||
pub hoist_targets: Vec<AnfHoistTarget>,
|
||||
|
||||
/// Phase 145 P1: Parent expression kind
|
||||
///
|
||||
/// Indicates the context where hoisting occurs (BinaryOp, UnaryOp, etc).
|
||||
pub parent_kind: AnfParentKind,
|
||||
}
|
||||
|
||||
impl AnfPlan {
|
||||
/// P0/P1 default: No ANF transformation needed (pure expression)
|
||||
///
|
||||
/// Used for constructing plans for pure expressions (variables, literals, etc).
|
||||
pub fn pure() -> Self {
|
||||
Self {
|
||||
requires_anf: false,
|
||||
impure_count: 0,
|
||||
hoist_targets: vec![],
|
||||
parent_kind: AnfParentKind::BinaryOp, // Default, will be overridden if needed
|
||||
}
|
||||
}
|
||||
|
||||
/// P0 constructor: ANF transformation needed (impure expression)
|
||||
///
|
||||
/// Used when plan_box detects impure subexpressions.
|
||||
/// P1: Use `with_hoists()` instead to specify hoist targets.
|
||||
pub fn impure(impure_count: usize) -> Self {
|
||||
Self {
|
||||
requires_anf: true,
|
||||
impure_count,
|
||||
hoist_targets: vec![],
|
||||
parent_kind: AnfParentKind::BinaryOp,
|
||||
}
|
||||
}
|
||||
|
||||
/// P1 constructor: ANF transformation with specific hoist targets
|
||||
///
|
||||
/// Used for BinaryOp patterns like `x + s.length()`.
|
||||
pub fn with_hoists(hoist_targets: Vec<AnfHoistTarget>, parent_kind: AnfParentKind) -> Self {
|
||||
let impure_count = hoist_targets.len();
|
||||
Self {
|
||||
requires_anf: !hoist_targets.is_empty(),
|
||||
impure_count,
|
||||
hoist_targets,
|
||||
parent_kind,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_anf_plan_pure() {
|
||||
let plan = AnfPlan::pure();
|
||||
assert!(!plan.requires_anf);
|
||||
assert_eq!(plan.impure_count, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_anf_plan_impure() {
|
||||
let plan = AnfPlan::impure(2);
|
||||
assert!(plan.requires_anf);
|
||||
assert_eq!(plan.impure_count, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_diagnostic_tag_eq() {
|
||||
assert_eq!(AnfDiagnosticTag::OrderViolation, AnfDiagnosticTag::OrderViolation);
|
||||
assert_ne!(AnfDiagnosticTag::OrderViolation, AnfDiagnosticTag::PureRequired);
|
||||
}
|
||||
|
||||
// P0: 2 contract tests (plan_pure + plan_impure)
|
||||
}
|
||||
470
src/mir/control_tree/normalized_shadow/anf/execute_box.rs
Normal file
470
src/mir/control_tree/normalized_shadow/anf/execute_box.rs
Normal file
@ -0,0 +1,470 @@
|
||||
//! Phase 145 P1: ANF Execute Box (BinaryOp + MethodCall hoist)
|
||||
//!
|
||||
//! ## Responsibility
|
||||
//!
|
||||
//! Execute ANF transformation for expressions that require it (per AnfPlan).
|
||||
//! P1: Hoist whitelisted MethodCalls (String.length()) from BinaryOp operands.
|
||||
//!
|
||||
//! ## Contract
|
||||
//!
|
||||
//! - Returns `Ok(Some(vid))` if ANF transformation succeeded (P1+)
|
||||
//! - Returns `Ok(None)` if transformation not attempted or failed gracefully
|
||||
//! - Returns `Err(msg)` only in strict mode for internal errors
|
||||
//!
|
||||
//! ## Phase Scope
|
||||
//!
|
||||
//! - **P0**: Stub only (always returns Ok(None), existing behavior unchanged)
|
||||
//! - **P1**: Implement String.length() hoist for BinaryOp (whitelist 1 intrinsic)
|
||||
//! - **P2**: Implement recursive compound expression ANF
|
||||
|
||||
use super::contract::{AnfPlan, AnfParentKind};
|
||||
use crate::ast::ASTNode;
|
||||
use crate::mir::join_ir::{JoinInst, MirLikeInst};
|
||||
use crate::mir::types::MirType;
|
||||
use crate::mir::ValueId;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// Box-First: ANF transformation executor
|
||||
pub struct AnfExecuteBox;
|
||||
|
||||
impl AnfExecuteBox {
|
||||
/// Try to execute ANF transformation for an expression
|
||||
///
|
||||
/// ## Arguments
|
||||
///
|
||||
/// - `plan`: ANF plan built by AnfPlanBox (indicates what transformation is needed)
|
||||
/// - `ast`: AST expression to transform
|
||||
/// - `env`: Current environment (variable → ValueId mapping)
|
||||
/// - `body`: JoinInst vector to emit transformed instructions
|
||||
/// - `next_value_id`: Mutable counter for allocating new ValueIds
|
||||
///
|
||||
/// ## Returns
|
||||
///
|
||||
/// - `Ok(Some(vid))`: ANF transformation succeeded, result is ValueId (P1+)
|
||||
/// - `Ok(None)`: Transformation not attempted or out-of-scope
|
||||
/// - `Err(msg)`: Internal error (strict mode only, P1+)
|
||||
///
|
||||
/// ## Phase Scope
|
||||
///
|
||||
/// - **P0**: Always returns Ok(None) (existing behavior unchanged)
|
||||
/// - **P1**: Implement String.length() hoist for BinaryOp
|
||||
/// - **P2**: Implement recursive ANF for compound expressions
|
||||
pub fn try_execute(
|
||||
plan: &AnfPlan,
|
||||
ast: &ASTNode,
|
||||
env: &mut BTreeMap<String, ValueId>,
|
||||
body: &mut Vec<JoinInst>,
|
||||
next_value_id: &mut u32,
|
||||
) -> Result<Option<ValueId>, String> {
|
||||
// DEBUG: Log attempt if HAKO_ANF_DEV=1
|
||||
if crate::config::env::anf_dev_enabled() {
|
||||
eprintln!("[phase145/debug] ANF execute called: requires_anf={}, targets={}",
|
||||
plan.requires_anf, plan.hoist_targets.len());
|
||||
}
|
||||
|
||||
// P1: No hoist targets → fallback to legacy
|
||||
if plan.hoist_targets.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// P1: Only BinaryOp is supported
|
||||
match plan.parent_kind {
|
||||
AnfParentKind::BinaryOp => {
|
||||
Self::execute_binary_op_hoist(plan, ast, env, body, next_value_id)
|
||||
}
|
||||
_ => Ok(None), // P2+: UnaryOp/MethodCall/Call
|
||||
}
|
||||
}
|
||||
|
||||
/// Phase 145 P2: Execute ANF transformation for BinaryOp with recursive normalization
|
||||
///
|
||||
/// Pattern: `x + s.length()` → `t = s.length(); result = x + t`
|
||||
/// Pattern: `s1.length() + s2.length()` → `t1 = s1.length(); t2 = s2.length(); result = t1 + t2`
|
||||
/// Pattern: `(x + s.length()) + z` → `t1 = s.length(); t2 = x + t1; result = t2 + z`
|
||||
///
|
||||
/// This function recursively normalizes left and right operands (depth-first, left-to-right)
|
||||
/// and then generates a pure BinaryOp instruction.
|
||||
fn execute_binary_op_hoist(
|
||||
plan: &AnfPlan,
|
||||
ast: &ASTNode,
|
||||
env: &mut BTreeMap<String, ValueId>,
|
||||
body: &mut Vec<JoinInst>,
|
||||
next_value_id: &mut u32,
|
||||
) -> Result<Option<ValueId>, String> {
|
||||
let ASTNode::BinaryOp { operator, left, right, .. } = ast else {
|
||||
return Err("ANF execute_binary_op_hoist: expected BinaryOp AST node".to_string());
|
||||
};
|
||||
|
||||
// P2: Use recursive normalization instead of single-level hoist
|
||||
Self::execute_binary_op_recursive(left, right, operator, env, body, next_value_id)
|
||||
}
|
||||
|
||||
/// Phase 145 P2: Recursively normalize BinaryOp operands (depth-first, left-to-right)
|
||||
///
|
||||
/// This is the core recursive ANF transformation algorithm:
|
||||
/// 1. Normalize LEFT operand recursively (depth-first)
|
||||
/// 2. Normalize RIGHT operand recursively (left-to-right)
|
||||
/// 3. Generate pure BinaryOp instruction
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// Input: `x + s.length()`
|
||||
/// - Step 1: Normalize `x` → ValueId(1)
|
||||
/// - Step 2: Normalize `s.length()` → ValueId(2) (emits MethodCall)
|
||||
/// - Step 3: Emit BinaryOp(ValueId(1), +, ValueId(2)) → ValueId(3)
|
||||
///
|
||||
/// Input: `s1.length() + s2.length()`
|
||||
/// - Step 1: Normalize `s1.length()` → ValueId(1) (emits MethodCall)
|
||||
/// - Step 2: Normalize `s2.length()` → ValueId(2) (emits MethodCall)
|
||||
/// - Step 3: Emit BinaryOp(ValueId(1), +, ValueId(2)) → ValueId(3)
|
||||
fn execute_binary_op_recursive(
|
||||
left: &ASTNode,
|
||||
right: &ASTNode,
|
||||
operator: &crate::ast::BinaryOperator,
|
||||
env: &mut BTreeMap<String, ValueId>,
|
||||
body: &mut Vec<JoinInst>,
|
||||
next_value_id: &mut u32,
|
||||
) -> Result<Option<ValueId>, String> {
|
||||
// Step 1: Recursively normalize LEFT (depth-first)
|
||||
let lhs_vid = Self::normalize_and_lower(left, env, body, next_value_id)?;
|
||||
|
||||
// Step 2: Recursively normalize RIGHT (left-to-right)
|
||||
let rhs_vid = Self::normalize_and_lower(right, env, body, next_value_id)?;
|
||||
|
||||
// Step 3: Generate pure BinOp instruction (wrapped in JoinInst::Compute)
|
||||
let dst = Self::alloc_value_id(next_value_id);
|
||||
|
||||
// Convert AST BinaryOperator to JoinIR BinOpKind
|
||||
let joinir_op = Self::ast_binop_to_joinir(operator)?;
|
||||
|
||||
body.push(JoinInst::Compute(MirLikeInst::BinOp {
|
||||
dst,
|
||||
op: joinir_op,
|
||||
lhs: lhs_vid,
|
||||
rhs: rhs_vid,
|
||||
}));
|
||||
|
||||
if crate::config::env::anf_dev_enabled() {
|
||||
eprintln!("[phase145/p2] Emitted BinOp: ValueId({}) = ValueId({}) {:?} ValueId({})",
|
||||
dst.as_u32(), lhs_vid.as_u32(), joinir_op, rhs_vid.as_u32());
|
||||
}
|
||||
|
||||
Ok(Some(dst))
|
||||
}
|
||||
|
||||
/// Phase 145 P2: Normalize and lower an expression recursively
|
||||
///
|
||||
/// This is the entry point for recursive ANF transformation.
|
||||
/// Handles:
|
||||
/// - MethodCall: Hoist to temporary
|
||||
/// - BinaryOp: Recursively normalize operands
|
||||
/// - Variable/Literal: Direct lowering
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// - `Ok(ValueId)`: Normalized result ValueId
|
||||
/// - `Err(String)`: Normalization failed
|
||||
fn normalize_and_lower(
|
||||
ast: &ASTNode,
|
||||
env: &mut BTreeMap<String, ValueId>,
|
||||
body: &mut Vec<JoinInst>,
|
||||
next_value_id: &mut u32,
|
||||
) -> Result<ValueId, String> {
|
||||
match ast {
|
||||
// Base case: Variable (already in env)
|
||||
ASTNode::Variable { name, .. } => {
|
||||
env.get(name).copied()
|
||||
.ok_or_else(|| format!("normalize_and_lower: undefined variable '{}'", name))
|
||||
}
|
||||
|
||||
// Base case: Literal (needs lowering)
|
||||
ASTNode::Literal { value, .. } => {
|
||||
let dst = Self::alloc_value_id(next_value_id);
|
||||
body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst,
|
||||
value: Self::literal_to_joinir_const(value)?,
|
||||
}));
|
||||
Ok(dst)
|
||||
}
|
||||
|
||||
// Recursive case: MethodCall (hoist to temporary)
|
||||
ASTNode::MethodCall { .. } => {
|
||||
Self::hoist_method_call(ast, env, body, next_value_id)
|
||||
}
|
||||
|
||||
// Recursive case: BinaryOp (normalize operands recursively)
|
||||
ASTNode::BinaryOp { operator, left, right, .. } => {
|
||||
let result_vid = Self::execute_binary_op_recursive(left, right, operator, env, body, next_value_id)?;
|
||||
result_vid.ok_or_else(|| "normalize_and_lower: BinaryOp returned None".to_string())
|
||||
}
|
||||
|
||||
// TODO P3+: UnaryOp, Call, etc.
|
||||
_ => Err(format!("normalize_and_lower: unsupported AST node type: {:?}", ast))
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert AST BinaryOperator to JoinIR BinOpKind
|
||||
fn ast_binop_to_joinir(op: &crate::ast::BinaryOperator) -> Result<crate::mir::join_ir::BinOpKind, String> {
|
||||
use crate::ast::BinaryOperator as AstOp;
|
||||
use crate::mir::join_ir::BinOpKind;
|
||||
|
||||
Ok(match op {
|
||||
AstOp::Add => BinOpKind::Add,
|
||||
AstOp::Subtract => BinOpKind::Sub,
|
||||
AstOp::Multiply => BinOpKind::Mul,
|
||||
AstOp::Divide => BinOpKind::Div,
|
||||
AstOp::Modulo => BinOpKind::Mod,
|
||||
_ => return Err(format!("ast_binop_to_joinir: unsupported operator: {:?}", op)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert AST LiteralValue to JoinIR ConstValue
|
||||
fn literal_to_joinir_const(lit: &crate::ast::LiteralValue) -> Result<crate::mir::join_ir::ConstValue, String> {
|
||||
use crate::ast::LiteralValue as AstLit;
|
||||
use crate::mir::join_ir::ConstValue;
|
||||
|
||||
Ok(match lit {
|
||||
AstLit::Integer(i) => ConstValue::Integer(*i),
|
||||
AstLit::String(s) => ConstValue::String(s.clone()),
|
||||
AstLit::Bool(b) => ConstValue::Bool(*b),
|
||||
AstLit::Void => ConstValue::Null, // JoinIR uses Null instead of Void
|
||||
AstLit::Null => ConstValue::Null,
|
||||
AstLit::Float(_) => return Err("literal_to_joinir_const: Float not yet supported in P2".to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
/// Phase 145 P1: Hoist a MethodCall to a temporary variable
|
||||
///
|
||||
/// Emits JoinInst::MethodCall and returns the result ValueId.
|
||||
fn hoist_method_call(
|
||||
ast: &ASTNode,
|
||||
env: &BTreeMap<String, ValueId>,
|
||||
body: &mut Vec<JoinInst>,
|
||||
next_value_id: &mut u32,
|
||||
) -> Result<ValueId, String> {
|
||||
let ASTNode::MethodCall { object, method, arguments, .. } = ast else {
|
||||
return Err("hoist_method_call: expected MethodCall AST node".to_string());
|
||||
};
|
||||
|
||||
// Get receiver ValueId
|
||||
let receiver = match object.as_ref() {
|
||||
ASTNode::Variable { name, .. } => {
|
||||
env.get(name).copied()
|
||||
.ok_or_else(|| format!("hoist_method_call: undefined variable '{}'", name))?
|
||||
}
|
||||
_ => return Err("hoist_method_call: receiver is not a variable".to_string()),
|
||||
};
|
||||
|
||||
// Validate arguments (P1: only 0-arity intrinsics supported)
|
||||
if !arguments.is_empty() {
|
||||
return Err("hoist_method_call: P1 only supports 0-arity intrinsics".to_string());
|
||||
}
|
||||
|
||||
// Allocate result ValueId
|
||||
let dst = Self::alloc_value_id(next_value_id);
|
||||
|
||||
// Emit MethodCall instruction
|
||||
body.push(JoinInst::MethodCall {
|
||||
dst,
|
||||
receiver,
|
||||
method: method.clone(),
|
||||
args: vec![],
|
||||
type_hint: Some(MirType::Integer), // P1: String.length() returns Integer
|
||||
});
|
||||
|
||||
Ok(dst)
|
||||
}
|
||||
|
||||
/// Allocate a new ValueId
|
||||
fn alloc_value_id(next_value_id: &mut u32) -> ValueId {
|
||||
let id = *next_value_id;
|
||||
*next_value_id += 1;
|
||||
ValueId(id)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ast::{ASTNode, LiteralValue};
|
||||
use crate::mir::join_ir::JoinInst;
|
||||
use std::collections::BTreeMap;
|
||||
use super::super::contract::AnfPlan;
|
||||
|
||||
fn span() -> crate::ast::Span {
|
||||
crate::ast::Span::unknown()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_execute_stub_returns_none() {
|
||||
// P0: execute_box is stub, always returns Ok(None)
|
||||
let plan = AnfPlan::pure();
|
||||
let ast = ASTNode::Literal {
|
||||
value: LiteralValue::Integer(42),
|
||||
span: span(),
|
||||
};
|
||||
let mut env = BTreeMap::new();
|
||||
let mut body = vec![];
|
||||
let mut next_value_id = 1000u32;
|
||||
|
||||
let result = AnfExecuteBox::try_execute(&plan, &ast, &mut env, &mut body, &mut next_value_id);
|
||||
assert!(result.is_ok());
|
||||
assert!(result.unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_p2_normalize_variable() {
|
||||
// P2: normalize_and_lower should handle variables
|
||||
let mut env = BTreeMap::new();
|
||||
env.insert("x".to_string(), ValueId(100));
|
||||
let mut body = vec![];
|
||||
let mut next_value_id = 1000u32;
|
||||
|
||||
let ast = ASTNode::Variable {
|
||||
name: "x".to_string(),
|
||||
span: span(),
|
||||
};
|
||||
|
||||
let result = AnfExecuteBox::normalize_and_lower(&ast, &mut env, &mut body, &mut next_value_id);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), ValueId(100));
|
||||
assert!(body.is_empty()); // Variable lookup doesn't emit instructions
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_p2_normalize_literal() {
|
||||
// P2: normalize_and_lower should emit Const for literals
|
||||
let mut env = BTreeMap::new();
|
||||
let mut body = vec![];
|
||||
let mut next_value_id = 1000u32;
|
||||
|
||||
let ast = ASTNode::Literal {
|
||||
value: LiteralValue::Integer(42),
|
||||
span: span(),
|
||||
};
|
||||
|
||||
let result = AnfExecuteBox::normalize_and_lower(&ast, &mut env, &mut body, &mut next_value_id);
|
||||
assert!(result.is_ok());
|
||||
let vid = result.unwrap();
|
||||
assert_eq!(vid, ValueId(1000));
|
||||
assert_eq!(body.len(), 1);
|
||||
|
||||
// Check emitted Const instruction
|
||||
match &body[0] {
|
||||
JoinInst::Compute(MirLikeInst::Const { dst, value }) => {
|
||||
assert_eq!(*dst, ValueId(1000));
|
||||
match value {
|
||||
crate::mir::join_ir::ConstValue::Integer(v) => assert_eq!(*v, 42),
|
||||
_ => panic!("Expected Integer constant"),
|
||||
}
|
||||
}
|
||||
_ => panic!("Expected Compute(Const) instruction"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_p2_nested_binop() {
|
||||
// P2: Nested BinaryOp should recursively normalize
|
||||
// Pattern: 10 + 5 + 3
|
||||
let mut env = BTreeMap::new();
|
||||
let mut body = vec![];
|
||||
let mut next_value_id = 1000u32;
|
||||
|
||||
// Build AST: (10 + 5) + 3
|
||||
let ast = ASTNode::BinaryOp {
|
||||
operator: crate::ast::BinaryOperator::Add,
|
||||
left: Box::new(ASTNode::BinaryOp {
|
||||
operator: crate::ast::BinaryOperator::Add,
|
||||
left: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(10),
|
||||
span: span(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(5),
|
||||
span: span(),
|
||||
}),
|
||||
span: span(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(3),
|
||||
span: span(),
|
||||
}),
|
||||
span: span(),
|
||||
};
|
||||
|
||||
let result = AnfExecuteBox::normalize_and_lower(&ast, &mut env, &mut body, &mut next_value_id);
|
||||
assert!(result.is_ok());
|
||||
|
||||
// Should emit:
|
||||
// 1. Const(1000) = 10
|
||||
// 2. Const(1001) = 5
|
||||
// 3. BinOp(1002) = 1000 + 1001
|
||||
// 4. Const(1003) = 3
|
||||
// 5. BinOp(1004) = 1002 + 1003
|
||||
assert_eq!(body.len(), 5);
|
||||
assert_eq!(result.unwrap(), ValueId(1004));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_p2_double_hoist_left_to_right() {
|
||||
// P2: Double MethodCall should hoist left-to-right
|
||||
// Pattern: s1.length() + s2.length()
|
||||
// This test verifies order preservation
|
||||
let mut env = BTreeMap::new();
|
||||
env.insert("s1".to_string(), ValueId(100));
|
||||
env.insert("s2".to_string(), ValueId(101));
|
||||
let mut body = vec![];
|
||||
let mut next_value_id = 1000u32;
|
||||
|
||||
// Build AST: s1.length() + s2.length()
|
||||
let ast = ASTNode::BinaryOp {
|
||||
operator: crate::ast::BinaryOperator::Add,
|
||||
left: Box::new(ASTNode::MethodCall {
|
||||
object: Box::new(ASTNode::Variable {
|
||||
name: "s1".to_string(),
|
||||
span: span(),
|
||||
}),
|
||||
method: "length".to_string(),
|
||||
arguments: vec![],
|
||||
span: span(),
|
||||
}),
|
||||
right: Box::new(ASTNode::MethodCall {
|
||||
object: Box::new(ASTNode::Variable {
|
||||
name: "s2".to_string(),
|
||||
span: span(),
|
||||
}),
|
||||
method: "length".to_string(),
|
||||
arguments: vec![],
|
||||
span: span(),
|
||||
}),
|
||||
span: span(),
|
||||
};
|
||||
|
||||
let result = AnfExecuteBox::normalize_and_lower(&ast, &mut env, &mut body, &mut next_value_id);
|
||||
assert!(result.is_ok());
|
||||
|
||||
// Should emit:
|
||||
// 1. MethodCall(1000) = s1.length() (LEFT first)
|
||||
// 2. MethodCall(1001) = s2.length() (RIGHT second)
|
||||
// 3. BinOp(1002) = 1000 + 1001
|
||||
assert_eq!(body.len(), 3);
|
||||
assert_eq!(result.unwrap(), ValueId(1002));
|
||||
|
||||
// Verify left-to-right order
|
||||
match &body[0] {
|
||||
JoinInst::MethodCall { receiver, .. } => {
|
||||
assert_eq!(*receiver, ValueId(100)); // s1 first
|
||||
}
|
||||
_ => panic!("Expected first instruction to be MethodCall for s1"),
|
||||
}
|
||||
|
||||
match &body[1] {
|
||||
JoinInst::MethodCall { receiver, .. } => {
|
||||
assert_eq!(*receiver, ValueId(101)); // s2 second
|
||||
}
|
||||
_ => panic!("Expected second instruction to be MethodCall for s2"),
|
||||
}
|
||||
}
|
||||
|
||||
// P2: 5 execute tests (normalize_variable, normalize_literal, nested_binop, double_hoist, left_to_right_order)
|
||||
}
|
||||
32
src/mir/control_tree/normalized_shadow/anf/mod.rs
Normal file
32
src/mir/control_tree/normalized_shadow/anf/mod.rs
Normal file
@ -0,0 +1,32 @@
|
||||
//! Phase 145 P0: ANF (A-Normal Form) module
|
||||
//!
|
||||
//! ## Responsibility
|
||||
//!
|
||||
//! Provides ANF transformation for impure expressions (Call/MethodCall) in Normalized JoinIR.
|
||||
//! Ensures deterministic evaluation order (left-to-right, depth-first) for side-effecting expressions.
|
||||
//!
|
||||
//! ## Architecture (Box-First, 3-layer separation)
|
||||
//!
|
||||
//! - **contract.rs**: Diagnostic tags, out-of-scope reasons, plan structure (SSOT)
|
||||
//! - **plan_box.rs**: AST pattern detection (requires_anf?, impure_count?)
|
||||
//! - **execute_box.rs**: ANF transformation execution (P0: stub, P1+: implementation)
|
||||
//!
|
||||
//! ## Phase Scope
|
||||
//!
|
||||
//! - **P0**: Skeleton only (execute_box always returns Ok(None))
|
||||
//! - **P1**: String.length() hoist (whitelist 1 intrinsic)
|
||||
//! - **P2**: Compound expression ANF (recursive left-to-right linearization)
|
||||
//!
|
||||
//! ## Contract
|
||||
//!
|
||||
//! - Out-of-scope returns Ok(None) (graceful fallback)
|
||||
//! - Default behavior unchanged (P0 is non-invasive skeleton)
|
||||
//! - Strict mode (HAKO_ANF_DEV=1): Debug logging only (P0 has no fail-fast)
|
||||
|
||||
pub mod contract;
|
||||
pub mod execute_box;
|
||||
pub mod plan_box;
|
||||
|
||||
pub use contract::{AnfDiagnosticTag, AnfOutOfScopeReason, AnfPlan};
|
||||
pub use execute_box::AnfExecuteBox;
|
||||
pub use plan_box::AnfPlanBox;
|
||||
474
src/mir/control_tree/normalized_shadow/anf/plan_box.rs
Normal file
474
src/mir/control_tree/normalized_shadow/anf/plan_box.rs
Normal file
@ -0,0 +1,474 @@
|
||||
//! Phase 145 P1: ANF Plan Box (AST pattern detection with whitelist)
|
||||
//!
|
||||
//! ## Responsibility
|
||||
//!
|
||||
//! Walk AST expression to detect impure subexpressions (Call/MethodCall) and build AnfPlan.
|
||||
//! Does NOT perform transformation (that's execute_box's job).
|
||||
//!
|
||||
//! ## Contract
|
||||
//!
|
||||
//! - Returns `Ok(Some(plan))` if expression is in scope (pure or impure)
|
||||
//! - Returns `Ok(None)` if expression is out-of-scope (unknown AST node type)
|
||||
//! - Returns `Err(reason)` if expression is explicitly out-of-scope (e.g., nested impure in P0)
|
||||
//!
|
||||
//! ## Phase Scope
|
||||
//!
|
||||
//! - **P0**: Detect Call/MethodCall presence (basic impure detection)
|
||||
//! - **P1**: Add whitelist check (String.length() only), BinaryOp pattern detection
|
||||
//! - **P2**: Add recursive compound expression detection
|
||||
|
||||
use super::contract::{AnfOutOfScopeReason, AnfPlan, AnfHoistTarget, HoistPosition, AnfParentKind};
|
||||
use crate::ast::ASTNode;
|
||||
use crate::mir::control_tree::normalized_shadow::common::expr_lowering_contract::KnownIntrinsic;
|
||||
use crate::mir::control_tree::normalized_shadow::common::known_intrinsics::KnownIntrinsicRegistryBox;
|
||||
use std::collections::BTreeMap;
|
||||
use crate::mir::ValueId;
|
||||
|
||||
/// Phase 145 P1: Whitelist of intrinsics allowed for ANF transformation
|
||||
///
|
||||
/// Initially: String.length() only (KnownIntrinsic::Length0).
|
||||
/// P2+: Will expand to more intrinsics.
|
||||
const P1_WHITELIST: &[KnownIntrinsic] = &[KnownIntrinsic::Length0];
|
||||
|
||||
/// Box-First: ANF plan builder (AST pattern detection)
|
||||
pub struct AnfPlanBox;
|
||||
|
||||
impl AnfPlanBox {
|
||||
/// Phase 145 P1: Check if intrinsic is whitelisted for ANF transformation
|
||||
fn is_whitelisted(intrinsic: KnownIntrinsic) -> bool {
|
||||
P1_WHITELIST.contains(&intrinsic)
|
||||
}
|
||||
|
||||
/// Phase 145 P1: Try to match MethodCall to a whitelisted intrinsic
|
||||
///
|
||||
/// Returns Some(intrinsic) if the MethodCall matches a whitelisted known intrinsic.
|
||||
fn try_match_whitelisted_method_call(
|
||||
object: &ASTNode,
|
||||
method: &str,
|
||||
arguments: &[ASTNode],
|
||||
_env: &BTreeMap<String, ValueId>,
|
||||
) -> Option<KnownIntrinsic> {
|
||||
// Match using KnownIntrinsicRegistryBox
|
||||
let arity = arguments.len();
|
||||
let intrinsic = KnownIntrinsicRegistryBox::lookup(method, arity)?;
|
||||
|
||||
// Check whitelist
|
||||
if !Self::is_whitelisted(intrinsic) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Additional validation: object must be a pure expression (variable or literal)
|
||||
// P1: Only support simple receivers (not chained calls)
|
||||
match object {
|
||||
ASTNode::Variable { .. } | ASTNode::Literal { .. } => Some(intrinsic),
|
||||
_ => None, // Nested MethodCall (e.g., s.trim().length()) is P2+
|
||||
}
|
||||
}
|
||||
/// Plan ANF transformation for an expression
|
||||
///
|
||||
/// Walks AST to detect impure subexpressions (Call/MethodCall) and builds AnfPlan.
|
||||
///
|
||||
/// ## Returns
|
||||
///
|
||||
/// - `Ok(Some(plan))`: Expression is in scope (plan.requires_anf indicates if ANF needed)
|
||||
/// - `Ok(None)`: Expression is out-of-scope (unknown AST node type, graceful fallback)
|
||||
/// - `Err(reason)`: Expression is explicitly out-of-scope (e.g., nested impure)
|
||||
///
|
||||
/// ## Phase Scope
|
||||
///
|
||||
/// - **P0**: Detect Call/MethodCall only (no transformation yet)
|
||||
/// - **P1+**: Add whitelist check, BinaryOp pattern detection
|
||||
pub fn plan_expr(
|
||||
ast: &ASTNode,
|
||||
_env: &BTreeMap<String, ValueId>, // P0: unused, P1+ for intrinsic detection
|
||||
) -> Result<Option<AnfPlan>, AnfOutOfScopeReason> {
|
||||
// P0: Basic impure detection (Call/MethodCall presence)
|
||||
match ast {
|
||||
// Pure expressions (no ANF transformation needed)
|
||||
ASTNode::Variable { .. } => Ok(Some(AnfPlan::pure())),
|
||||
ASTNode::Literal { .. } => Ok(Some(AnfPlan::pure())),
|
||||
|
||||
// Unary: Check operand recursively
|
||||
ASTNode::UnaryOp { operand, .. } => {
|
||||
match Self::plan_expr(operand, _env)? {
|
||||
Some(operand_plan) => {
|
||||
Ok(Some(AnfPlan {
|
||||
requires_anf: operand_plan.requires_anf,
|
||||
impure_count: operand_plan.impure_count,
|
||||
hoist_targets: vec![],
|
||||
parent_kind: AnfParentKind::UnaryOp,
|
||||
}))
|
||||
}
|
||||
None => Ok(None), // Operand out-of-scope → propagate
|
||||
}
|
||||
}
|
||||
|
||||
// Binary: Check left and right recursively
|
||||
ASTNode::BinaryOp { left, right, .. } => {
|
||||
// Phase 145 P1: Detect whitelisted MethodCall in operands
|
||||
let mut hoist_targets = vec![];
|
||||
|
||||
// Check left operand for whitelisted MethodCall
|
||||
if let ASTNode::MethodCall { object, method, arguments, .. } = left.as_ref() {
|
||||
if let Some(intrinsic) = Self::try_match_whitelisted_method_call(object, method, arguments, _env) {
|
||||
hoist_targets.push(AnfHoistTarget {
|
||||
intrinsic,
|
||||
ast_node: left.as_ref().clone(),
|
||||
position: HoistPosition::Left,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check right operand for whitelisted MethodCall
|
||||
if let ASTNode::MethodCall { object, method, arguments, .. } = right.as_ref() {
|
||||
if let Some(intrinsic) = Self::try_match_whitelisted_method_call(object, method, arguments, _env) {
|
||||
hoist_targets.push(AnfHoistTarget {
|
||||
intrinsic,
|
||||
ast_node: right.as_ref().clone(),
|
||||
position: HoistPosition::Right,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If we found whitelisted MethodCalls, return a plan with hoist targets
|
||||
if !hoist_targets.is_empty() {
|
||||
return Ok(Some(AnfPlan::with_hoists(hoist_targets, AnfParentKind::BinaryOp)));
|
||||
}
|
||||
|
||||
// P0 fallback: Recursively check operands for pure/impure
|
||||
let left_plan = match Self::plan_expr(left, _env)? {
|
||||
Some(p) => p,
|
||||
None => return Ok(None), // Left out-of-scope → propagate
|
||||
};
|
||||
let right_plan = match Self::plan_expr(right, _env)? {
|
||||
Some(p) => p,
|
||||
None => return Ok(None), // Right out-of-scope → propagate
|
||||
};
|
||||
|
||||
// Combine: ANF needed if either operand requires it
|
||||
let combined_impure_count = left_plan.impure_count + right_plan.impure_count;
|
||||
let requires_anf = left_plan.requires_anf || right_plan.requires_anf;
|
||||
|
||||
Ok(Some(AnfPlan {
|
||||
requires_anf,
|
||||
impure_count: combined_impure_count,
|
||||
hoist_targets: vec![],
|
||||
parent_kind: AnfParentKind::BinaryOp,
|
||||
}))
|
||||
}
|
||||
|
||||
// Impure expressions (ANF transformation candidates)
|
||||
ASTNode::FunctionCall { .. } | ASTNode::Call { .. } => {
|
||||
// P0: Detect presence but do not transform (execute_box is stub)
|
||||
Err(AnfOutOfScopeReason::ContainsCall)
|
||||
}
|
||||
|
||||
ASTNode::MethodCall { .. } => {
|
||||
// P0: Detect presence but do not transform (execute_box is stub)
|
||||
Err(AnfOutOfScopeReason::ContainsMethodCall)
|
||||
}
|
||||
|
||||
// Out-of-scope (unknown AST node types)
|
||||
_ => {
|
||||
// P0: Unknown expression type → graceful fallback
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if expression is pure (no impure subexpressions)
|
||||
///
|
||||
/// Helper function for quick pure/impure discrimination.
|
||||
///
|
||||
/// ## Returns
|
||||
///
|
||||
/// - `true`: Expression is pure (no Call/MethodCall)
|
||||
/// - `false`: Expression contains impure subexpressions
|
||||
///
|
||||
/// ## Phase Scope
|
||||
///
|
||||
/// - **P0**: Basic Call/MethodCall detection
|
||||
/// - **P1+**: Consider whitelist (e.g., String.length() may be treated as pure)
|
||||
pub fn is_pure(ast: &ASTNode, env: &BTreeMap<String, ValueId>) -> bool {
|
||||
match Self::plan_expr(ast, env) {
|
||||
Ok(Some(plan)) => !plan.requires_anf,
|
||||
Ok(None) => true, // Unknown → assume pure (conservative fallback)
|
||||
Err(_) => false, // Contains Call/MethodCall → impure
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ast::{ASTNode, BinaryOperator, LiteralValue, UnaryOperator};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
fn span() -> crate::ast::Span {
|
||||
crate::ast::Span::unknown()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plan_pure_variable() {
|
||||
let ast = ASTNode::Variable {
|
||||
name: "x".to_string(),
|
||||
span: span(),
|
||||
};
|
||||
let env = BTreeMap::new();
|
||||
let plan = AnfPlanBox::plan_expr(&ast, &env).unwrap().unwrap();
|
||||
assert!(!plan.requires_anf);
|
||||
assert_eq!(plan.impure_count, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plan_pure_literal() {
|
||||
let ast = ASTNode::Literal {
|
||||
value: LiteralValue::Integer(42),
|
||||
span: span(),
|
||||
};
|
||||
let env = BTreeMap::new();
|
||||
let plan = AnfPlanBox::plan_expr(&ast, &env).unwrap().unwrap();
|
||||
assert!(!plan.requires_anf);
|
||||
assert_eq!(plan.impure_count, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plan_pure_binop() {
|
||||
// x + 2 (pure)
|
||||
let ast = ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Add,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: "x".to_string(),
|
||||
span: span(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(2),
|
||||
span: span(),
|
||||
}),
|
||||
span: span(),
|
||||
};
|
||||
let env = BTreeMap::new();
|
||||
let plan = AnfPlanBox::plan_expr(&ast, &env).unwrap().unwrap();
|
||||
assert!(!plan.requires_anf);
|
||||
assert_eq!(plan.impure_count, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plan_call_out_of_scope() {
|
||||
// f() → P0 out-of-scope
|
||||
let ast = ASTNode::FunctionCall {
|
||||
name: "f".to_string(),
|
||||
arguments: vec![],
|
||||
span: span(),
|
||||
};
|
||||
let env = BTreeMap::new();
|
||||
let result = AnfPlanBox::plan_expr(&ast, &env);
|
||||
assert!(matches!(result, Err(AnfOutOfScopeReason::ContainsCall)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plan_method_call_out_of_scope() {
|
||||
// obj.method() → P0 out-of-scope
|
||||
let ast = ASTNode::MethodCall {
|
||||
object: Box::new(ASTNode::Variable {
|
||||
name: "obj".to_string(),
|
||||
span: span(),
|
||||
}),
|
||||
method: "method".to_string(),
|
||||
arguments: vec![],
|
||||
span: span(),
|
||||
};
|
||||
let env = BTreeMap::new();
|
||||
let result = AnfPlanBox::plan_expr(&ast, &env);
|
||||
assert!(matches!(result, Err(AnfOutOfScopeReason::ContainsMethodCall)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_pure_variable() {
|
||||
let ast = ASTNode::Variable {
|
||||
name: "x".to_string(),
|
||||
span: span(),
|
||||
};
|
||||
let env = BTreeMap::new();
|
||||
assert!(AnfPlanBox::is_pure(&ast, &env));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_pure_call_false() {
|
||||
let ast = ASTNode::FunctionCall {
|
||||
name: "f".to_string(),
|
||||
arguments: vec![],
|
||||
span: span(),
|
||||
};
|
||||
let env = BTreeMap::new();
|
||||
assert!(!AnfPlanBox::is_pure(&ast, &env));
|
||||
}
|
||||
|
||||
// P0: 4 plan tests (pure_variable, pure_literal, pure_binop, call_out_of_scope)
|
||||
|
||||
// ========== Phase 145 P1 Tests ==========
|
||||
|
||||
#[test]
|
||||
fn test_p1_whitelist_length0() {
|
||||
// String.length() is whitelisted in P1
|
||||
use crate::mir::control_tree::normalized_shadow::common::expr_lowering_contract::KnownIntrinsic;
|
||||
assert!(AnfPlanBox::is_whitelisted(KnownIntrinsic::Length0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_p1_binop_with_whitelisted_method_call_right() {
|
||||
// x + s.length() → hoist target detected
|
||||
let mut env = BTreeMap::new();
|
||||
env.insert("x".to_string(), ValueId(100));
|
||||
env.insert("s".to_string(), ValueId(200));
|
||||
|
||||
let ast = ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Add,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: "x".to_string(),
|
||||
span: span(),
|
||||
}),
|
||||
right: Box::new(ASTNode::MethodCall {
|
||||
object: Box::new(ASTNode::Variable {
|
||||
name: "s".to_string(),
|
||||
span: span(),
|
||||
}),
|
||||
method: "length".to_string(),
|
||||
arguments: vec![],
|
||||
span: span(),
|
||||
}),
|
||||
span: span(),
|
||||
};
|
||||
|
||||
let plan = AnfPlanBox::plan_expr(&ast, &env).unwrap().unwrap();
|
||||
assert!(plan.requires_anf, "Should require ANF transformation");
|
||||
assert_eq!(plan.hoist_targets.len(), 1, "Should have 1 hoist target");
|
||||
assert_eq!(plan.impure_count, 1);
|
||||
assert_eq!(plan.parent_kind, AnfParentKind::BinaryOp);
|
||||
assert_eq!(plan.hoist_targets[0].position, HoistPosition::Right);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_p1_binop_with_whitelisted_method_call_left() {
|
||||
// s.length() + x → hoist target detected (left position)
|
||||
let mut env = BTreeMap::new();
|
||||
env.insert("x".to_string(), ValueId(100));
|
||||
env.insert("s".to_string(), ValueId(200));
|
||||
|
||||
let ast = ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Add,
|
||||
left: Box::new(ASTNode::MethodCall {
|
||||
object: Box::new(ASTNode::Variable {
|
||||
name: "s".to_string(),
|
||||
span: span(),
|
||||
}),
|
||||
method: "length".to_string(),
|
||||
arguments: vec![],
|
||||
span: span(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Variable {
|
||||
name: "x".to_string(),
|
||||
span: span(),
|
||||
}),
|
||||
span: span(),
|
||||
};
|
||||
|
||||
let plan = AnfPlanBox::plan_expr(&ast, &env).unwrap().unwrap();
|
||||
assert!(plan.requires_anf);
|
||||
assert_eq!(plan.hoist_targets.len(), 1);
|
||||
assert_eq!(plan.hoist_targets[0].position, HoistPosition::Left);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_p1_binop_with_pure_operands() {
|
||||
// x + y (pure) → no hoist targets
|
||||
let mut env = BTreeMap::new();
|
||||
env.insert("x".to_string(), ValueId(100));
|
||||
env.insert("y".to_string(), ValueId(200));
|
||||
|
||||
let ast = ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Add,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: "x".to_string(),
|
||||
span: span(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Variable {
|
||||
name: "y".to_string(),
|
||||
span: span(),
|
||||
}),
|
||||
span: span(),
|
||||
};
|
||||
|
||||
let plan = AnfPlanBox::plan_expr(&ast, &env).unwrap().unwrap();
|
||||
assert!(!plan.requires_anf);
|
||||
assert_eq!(plan.hoist_targets.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_p1_method_call_not_whitelisted() {
|
||||
// s.unknown() is not whitelisted → no hoist (falls back to ContainsMethodCall)
|
||||
let mut env = BTreeMap::new();
|
||||
env.insert("s".to_string(), ValueId(200));
|
||||
|
||||
let ast = ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Add,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: "s".to_string(),
|
||||
span: span(),
|
||||
}),
|
||||
right: Box::new(ASTNode::MethodCall {
|
||||
object: Box::new(ASTNode::Variable {
|
||||
name: "s".to_string(),
|
||||
span: span(),
|
||||
}),
|
||||
method: "unknown".to_string(),
|
||||
arguments: vec![],
|
||||
span: span(),
|
||||
}),
|
||||
span: span(),
|
||||
};
|
||||
|
||||
// Should fall back to recursive plan which detects MethodCall as out-of-scope
|
||||
let result = AnfPlanBox::plan_expr(&ast, &env);
|
||||
// MethodCall not whitelisted → falls back to recursive check → Err(ContainsMethodCall)
|
||||
assert!(matches!(result, Err(AnfOutOfScopeReason::ContainsMethodCall)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_p1_binop_with_two_whitelisted_method_calls() {
|
||||
// s1.length() + s2.length() → 2 hoist targets (P1 supports this!)
|
||||
let mut env = BTreeMap::new();
|
||||
env.insert("s1".to_string(), ValueId(100));
|
||||
env.insert("s2".to_string(), ValueId(200));
|
||||
|
||||
let ast = ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Add,
|
||||
left: Box::new(ASTNode::MethodCall {
|
||||
object: Box::new(ASTNode::Variable {
|
||||
name: "s1".to_string(),
|
||||
span: span(),
|
||||
}),
|
||||
method: "length".to_string(),
|
||||
arguments: vec![],
|
||||
span: span(),
|
||||
}),
|
||||
right: Box::new(ASTNode::MethodCall {
|
||||
object: Box::new(ASTNode::Variable {
|
||||
name: "s2".to_string(),
|
||||
span: span(),
|
||||
}),
|
||||
method: "length".to_string(),
|
||||
arguments: vec![],
|
||||
span: span(),
|
||||
}),
|
||||
span: span(),
|
||||
};
|
||||
|
||||
let plan = AnfPlanBox::plan_expr(&ast, &env).unwrap().unwrap();
|
||||
assert!(plan.requires_anf);
|
||||
assert_eq!(plan.hoist_targets.len(), 2, "Should have 2 hoist targets");
|
||||
assert_eq!(plan.impure_count, 2);
|
||||
}
|
||||
|
||||
// P1: 6 new tests (whitelist, binop_right, binop_left, pure, not_whitelisted, two_method_calls)
|
||||
}
|
||||
@ -51,6 +51,30 @@ impl NormalizedExprLowererBox {
|
||||
body: &mut Vec<JoinInst>,
|
||||
next_value_id: &mut u32,
|
||||
) -> Result<Option<ValueId>, String> {
|
||||
// Phase 145 P0: ANF routing (dev-only)
|
||||
if crate::config::env::anf_dev_enabled() {
|
||||
use super::super::anf::{AnfPlanBox, AnfExecuteBox};
|
||||
match AnfPlanBox::plan_expr(ast, env) {
|
||||
Ok(Some(plan)) => {
|
||||
match AnfExecuteBox::try_execute(&plan, ast, &mut env.clone(), body, next_value_id)? {
|
||||
Some(vid) => return Ok(Some(vid)), // P1+: ANF succeeded
|
||||
None => {
|
||||
// P0: stub returns None, fallback to legacy
|
||||
if crate::config::env::joinir_dev_enabled() {
|
||||
eprintln!("[phase145/debug] ANF plan found but execute returned None (P0 stub)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
// Out-of-scope for ANF, continue with legacy lowering
|
||||
}
|
||||
Err(_reason) => {
|
||||
// Explicitly out-of-scope (ContainsCall/ContainsMethodCall), continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if Self::out_of_scope_reason(scope, ast, env).is_some() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
@ -28,7 +28,7 @@ use super::common::loop_if_exit_contract::{LoopIfExitShape, LoopIfExitThen, OutO
|
||||
use super::common::normalized_helpers::NormalizedHelperBox;
|
||||
use crate::mir::control_tree::step_tree::{StepNode, StepStmtKind, StepTree};
|
||||
use crate::mir::join_ir::lowering::carrier_info::JoinFragmentMeta;
|
||||
use crate::mir::join_ir::{JoinFunction, JoinFuncId, JoinInst, JoinModule};
|
||||
use crate::mir::join_ir::{JoinFunction, JoinFuncId, JoinInst, JoinModule, MirLikeInst, UnaryOp};
|
||||
use crate::mir::ValueId;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@ -71,10 +71,10 @@ impl LoopTrueIfBreakContinueBuilderBox {
|
||||
}
|
||||
};
|
||||
|
||||
// Validate that shape is P1-compatible (supports both break and continue)
|
||||
if let Err(reason) = shape.validate_for_p1() {
|
||||
// Validate that shape is P2-compatible (supports break/continue with optional else)
|
||||
if let Err(reason) = shape.validate_for_p2() {
|
||||
if crate::config::env::joinir_dev_enabled() {
|
||||
eprintln!("[phase143/debug] P1 validation failed: {:?}", reason);
|
||||
eprintln!("[phase143/debug] P2 validation failed: {:?}", reason);
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
@ -194,13 +194,14 @@ impl LoopTrueIfBreakContinueBuilderBox {
|
||||
}
|
||||
};
|
||||
|
||||
// P1: Emit conditional Jump based on exit action (break vs continue)
|
||||
// P2: Emit conditional Jump/Call based on then/else actions (4-way match)
|
||||
let k_exit_args = NormalizedHelperBox::collect_env_args(&env_fields, &env_loop_cond_check)?;
|
||||
let loop_step_args = NormalizedHelperBox::collect_env_args(&env_fields, &env_loop_cond_check)?;
|
||||
|
||||
match shape.then {
|
||||
LoopIfExitThen::Break => {
|
||||
// P0: If cond_vid is true: jump to k_exit (BREAK)
|
||||
match (shape.then, shape.else_) {
|
||||
// P0: Break-only (no else)
|
||||
(LoopIfExitThen::Break, None) => {
|
||||
// If cond_vid is true: jump to k_exit (BREAK)
|
||||
f.body.push(JoinInst::Jump {
|
||||
cont: k_exit_id.as_cont(),
|
||||
args: k_exit_args,
|
||||
@ -214,15 +215,74 @@ impl LoopTrueIfBreakContinueBuilderBox {
|
||||
dst: None,
|
||||
});
|
||||
}
|
||||
LoopIfExitThen::Continue => {
|
||||
// P1: `if(cond){continue}` with a single-if loop body.
|
||||
//
|
||||
// Phase 143 P1 scope is intentionally conservative (no else branch, no in-loop state updates).
|
||||
// In this shape, `continue` means "proceed to next iteration", and the loop never terminates.
|
||||
//
|
||||
// We still lower the condition as a pure expression to validate scope, but we do not
|
||||
// emit a conditional Jump here (Jump has "early return" semantics in the bridge).
|
||||
// Instead, we tail-call loop_step unconditionally.
|
||||
|
||||
// P1: Continue-only (no else)
|
||||
(LoopIfExitThen::Continue, None) => {
|
||||
// Unconditional continue (condition ignored)
|
||||
let _ = cond_vid;
|
||||
f.body.push(JoinInst::Call {
|
||||
func: loop_step_id,
|
||||
args: loop_step_args,
|
||||
k_next: None,
|
||||
dst: None,
|
||||
});
|
||||
}
|
||||
|
||||
// P2: Break-Continue (then=break, else=continue)
|
||||
(LoopIfExitThen::Break, Some(LoopIfExitThen::Continue)) => {
|
||||
// If cond true: jump to k_exit (break)
|
||||
f.body.push(JoinInst::Jump {
|
||||
cont: k_exit_id.as_cont(),
|
||||
args: k_exit_args,
|
||||
cond: Some(cond_vid),
|
||||
});
|
||||
// If cond false: call loop_step (continue)
|
||||
f.body.push(JoinInst::Call {
|
||||
func: loop_step_id,
|
||||
args: loop_step_args,
|
||||
k_next: None,
|
||||
dst: None,
|
||||
});
|
||||
}
|
||||
|
||||
// P2: Continue-Break (then=continue, else=break)
|
||||
(LoopIfExitThen::Continue, Some(LoopIfExitThen::Break)) => {
|
||||
// If cond true: call loop_step (continue)
|
||||
// If cond false: jump to k_exit (break)
|
||||
// Strategy: Invert condition
|
||||
let inverted_cond = NormalizedHelperBox::alloc_value_id(&mut next_value_id);
|
||||
f.body.push(JoinInst::Compute(MirLikeInst::UnaryOp {
|
||||
dst: inverted_cond,
|
||||
op: UnaryOp::Not,
|
||||
operand: cond_vid,
|
||||
}));
|
||||
f.body.push(JoinInst::Jump {
|
||||
cont: k_exit_id.as_cont(),
|
||||
args: k_exit_args,
|
||||
cond: Some(inverted_cond),
|
||||
});
|
||||
f.body.push(JoinInst::Call {
|
||||
func: loop_step_id,
|
||||
args: loop_step_args,
|
||||
k_next: None,
|
||||
dst: None,
|
||||
});
|
||||
}
|
||||
|
||||
// P2: Break-Break (both branches break)
|
||||
(LoopIfExitThen::Break, Some(LoopIfExitThen::Break)) => {
|
||||
// Unconditional jump to k_exit
|
||||
let _ = cond_vid;
|
||||
f.body.push(JoinInst::Jump {
|
||||
cont: k_exit_id.as_cont(),
|
||||
args: k_exit_args,
|
||||
cond: None, // Unconditional
|
||||
});
|
||||
}
|
||||
|
||||
// P2: Continue-Continue (both branches continue)
|
||||
(LoopIfExitThen::Continue, Some(LoopIfExitThen::Continue)) => {
|
||||
// Unconditional call to loop_step
|
||||
let _ = cond_vid;
|
||||
f.body.push(JoinInst::Call {
|
||||
func: loop_step_id,
|
||||
@ -368,21 +428,21 @@ impl LoopTrueIfBreakContinueBuilderBox {
|
||||
..
|
||||
} = if_node
|
||||
{
|
||||
// P1: No else branch allowed
|
||||
if else_branch.is_some() {
|
||||
return Err(OutOfScopeReason::ElseNotSupported(
|
||||
LoopIfExitThen::Break,
|
||||
));
|
||||
}
|
||||
|
||||
// Extract then action (P1: Break OR Continue)
|
||||
// Extract then action (P0/P1/P2: Break OR Continue)
|
||||
let then_action = Self::extract_exit_action(then_branch)?;
|
||||
|
||||
// P2: Extract else action if present
|
||||
let else_action = if let Some(else_node) = else_branch {
|
||||
Some(Self::extract_exit_action(else_node)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Build contract shape
|
||||
let shape = LoopIfExitShape {
|
||||
has_else: false,
|
||||
has_else: else_branch.is_some(),
|
||||
then: then_action,
|
||||
else_: None,
|
||||
else_: else_action,
|
||||
cond_scope: ExprLoweringScope::PureOnly,
|
||||
};
|
||||
|
||||
|
||||
@ -43,6 +43,7 @@ pub mod parity_contract;
|
||||
pub mod available_inputs_collector; // Phase 126: available_inputs SSOT
|
||||
pub mod exit_reconnector; // Phase 131 P1.5: Direct variable_map reconnection (Option B)
|
||||
pub mod common; // Phase 138: Common utilities (ReturnValueLowererBox)
|
||||
pub mod anf; // Phase 145 P0: ANF (A-Normal Form) transformation
|
||||
|
||||
pub use builder::StepTreeNormalizedShadowLowererBox;
|
||||
pub use contracts::{CapabilityCheckResult, UnsupportedCapability};
|
||||
|
||||
@ -115,6 +115,32 @@ pub fn lowering_error(subsystem: &str, detail: &str) -> String {
|
||||
format!("[joinir/lowering/{}] {}", subsystem, detail)
|
||||
}
|
||||
|
||||
/// Phase 145 P2: ANF order violation error
|
||||
///
|
||||
/// Used when impure expressions appear in immediate position without hoisting.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust,ignore
|
||||
/// return Err(anf_order_violation("f() + g()", "both calls not hoisted"));
|
||||
/// // Output: "[joinir/anf/order_violation] f() + g(): both calls not hoisted"
|
||||
/// ```
|
||||
pub fn anf_order_violation(expr: &str, reason: &str) -> String {
|
||||
format!("[joinir/anf/order_violation] {}: {}", expr, reason)
|
||||
}
|
||||
|
||||
/// Phase 145 P2: ANF pure required error
|
||||
///
|
||||
/// Used when impure expression appears in pure-only scope (e.g., loop condition).
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust,ignore
|
||||
/// return Err(anf_pure_required("loop(iter.hasNext())", "impure in condition"));
|
||||
/// // Output: "[joinir/anf/pure_required] loop(iter.hasNext()): impure in condition"
|
||||
/// ```
|
||||
pub fn anf_pure_required(expr: &str, reason: &str) -> String {
|
||||
format!("[joinir/anf/pure_required] {}: {}", expr, reason)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@ -172,4 +198,20 @@ mod tests {
|
||||
fn test_freeze_with_hint_empty_hint_panics() {
|
||||
freeze_with_hint("test/tag", "message", "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_anf_order_violation_tag() {
|
||||
let err = anf_order_violation("f() + g()", "both calls not hoisted");
|
||||
assert!(err.contains("[joinir/anf/order_violation]"));
|
||||
assert!(err.contains("f() + g()"));
|
||||
assert!(err.contains("both calls not hoisted"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_anf_pure_required_tag() {
|
||||
let err = anf_pure_required("loop(iter.hasNext())", "impure in condition");
|
||||
assert!(err.contains("[joinir/anf/pure_required]"));
|
||||
assert!(err.contains("loop(iter.hasNext())"));
|
||||
assert!(err.contains("impure in condition"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
# Phase 143 P1: loop(true) + if + continue Normalized lowering (LLVM EXE parity)
|
||||
#
|
||||
# Verifies LLVM EXE build+run for Phase 143 P1 pattern.
|
||||
#
|
||||
# This fixture is intentionally non-terminating in Phase 143 P1, so we use
|
||||
# a timeout-based contract (expected timeout exit code 124).
|
||||
#
|
||||
# Dev-only: NYASH_JOINIR_DEV=1 HAKO_JOINIR_STRICT=1
|
||||
|
||||
source "$(dirname "$0")/../../../lib/test_runner.sh"
|
||||
source "$(dirname "$0")/../../../lib/llvm_exe_runner.sh"
|
||||
export SMOKES_USE_PYVM=0
|
||||
require_env || exit 2
|
||||
|
||||
# Preflight check (SKIP gate)
|
||||
llvm_exe_preflight_or_skip || exit 0
|
||||
|
||||
# JoinIR dev mode (Phase 130+ gate)
|
||||
require_joinir_dev
|
||||
|
||||
# Minimal plugins (Integer ops for comparisons)
|
||||
INTEGERBOX_SO="$NYASH_ROOT/plugins/nyash-integer-plugin/libnyash_integer_plugin.so"
|
||||
LLVM_REQUIRED_PLUGINS=(
|
||||
"IntegerBox|$INTEGERBOX_SO|nyash-integer-plugin"
|
||||
)
|
||||
LLVM_PLUGIN_BUILD_LOG="/tmp/phase143_loop_true_if_continue_llvm_plugin_build.log"
|
||||
llvm_exe_ensure_plugins_or_fail || exit 1
|
||||
|
||||
# Test configuration
|
||||
INPUT_HAKO="$NYASH_ROOT/apps/tests/phase143_loop_true_if_continue_min.hako"
|
||||
OUTPUT_EXE="$NYASH_ROOT/tmp/phase143_loop_true_if_continue_llvm_exe"
|
||||
|
||||
# Execute (timeout contract)
|
||||
RUN_TIMEOUT_SECS="${SMOKES_P143_CONTINUE_TIMEOUT_SECS:-1}"
|
||||
EXPECTED_EXIT_CODE=124
|
||||
LLVM_BUILD_LOG="/tmp/phase143_loop_true_if_continue_llvm_build.log"
|
||||
if llvm_exe_build_and_run_expect_exit_code; then
|
||||
test_pass "phase143_loop_true_if_continue_llvm_exe: timed out as expected (${RUN_TIMEOUT_SECS}s)"
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
# Phase 143 P1: loop(true) + if + continue Normalized lowering (VM)
|
||||
#
|
||||
# Verifies that loop(true) { if(cond_pure) continue } pattern is correctly lowered
|
||||
# to Normalized JoinModule with Jump/Call instructions.
|
||||
# Expected: exit code 54 (sum of: 1+3+5+6+7+8+9+10, skipping 2 and 4)
|
||||
#
|
||||
# Dev-only: NYASH_JOINIR_DEV=1 HAKO_JOINIR_STRICT=1
|
||||
|
||||
source "$(dirname "$0")/../../../lib/test_runner.sh"
|
||||
require_env || exit 2
|
||||
|
||||
# JoinIR dev mode (Phase 130+ gate)
|
||||
require_joinir_dev
|
||||
|
||||
# Test configuration
|
||||
INPUT_HAKO="$NYASH_ROOT/apps/tests/phase143_loop_true_if_continue_min.hako"
|
||||
|
||||
# Execute (timeout contract)
|
||||
#
|
||||
# This fixture is intentionally non-terminating in Phase 143 P1:
|
||||
# - loop(true) with a single `if(cond){continue}` has no in-loop state update and no break path.
|
||||
#
|
||||
# We verify that:
|
||||
# - Normalized lowering + VM execution starts successfully
|
||||
# - It doesn't fail fast immediately
|
||||
# - It times out (expected)
|
||||
timeout_secs="${SMOKES_P143_CONTINUE_TIMEOUT_SECS:-1}"
|
||||
# Disable VM step budget so the test is controlled by external timeout.
|
||||
export HAKO_VM_MAX_STEPS=0
|
||||
timeout "$timeout_secs" "$NYASH_BIN" --backend vm "$INPUT_HAKO" > /dev/null 2>&1
|
||||
actual_exit=$?
|
||||
EXPECTED_TIMEOUT_EXIT=124
|
||||
if [ "$actual_exit" -eq "$EXPECTED_TIMEOUT_EXIT" ]; then
|
||||
test_pass "phase143_loop_true_if_continue: timed out as expected (${timeout_secs}s)"
|
||||
else
|
||||
test_fail "phase143_loop_true_if_continue: expected timeout exit $EXPECTED_TIMEOUT_EXIT, got $actual_exit"
|
||||
exit 1
|
||||
fi
|
||||
@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
# Phase 143 P2: loop(true) if break-continue (B-C, Normalized shadow, LLVM EXE parity)
|
||||
#
|
||||
# Verifies Phase 143 P2 else symmetry:
|
||||
# - loop(true) { if flag==1 {break} else {continue} } ; return 8 → exit code 8
|
||||
#
|
||||
# Dev-only: NYASH_JOINIR_DEV=1 HAKO_JOINIR_STRICT=1
|
||||
|
||||
source "$(dirname "$0")/../../../lib/test_runner.sh"
|
||||
source "$(dirname "$0")/../../../lib/llvm_exe_runner.sh"
|
||||
export SMOKES_USE_PYVM=0
|
||||
require_env || exit 2
|
||||
|
||||
# Preflight check (SKIP gate)
|
||||
llvm_exe_preflight_or_skip || exit 0
|
||||
|
||||
# JoinIR dev mode (Phase 130+ gate)
|
||||
require_joinir_dev
|
||||
|
||||
# Minimal plugins (String + Integer for comparisons and arithmetic)
|
||||
STRINGBOX_SO="$NYASH_ROOT/plugins/nyash-string-plugin/libnyash_string_plugin.so"
|
||||
INTEGERBOX_SO="$NYASH_ROOT/plugins/nyash-integer-plugin/libnyash_integer_plugin.so"
|
||||
LLVM_REQUIRED_PLUGINS=(
|
||||
"StringBox|$STRINGBOX_SO|nyash-string-plugin"
|
||||
"IntegerBox|$INTEGERBOX_SO|nyash-integer-plugin"
|
||||
)
|
||||
LLVM_PLUGIN_BUILD_LOG="/tmp/phase143_p2_loop_true_if_bc_llvm_plugin_build.log"
|
||||
llvm_exe_ensure_plugins_or_fail || exit 1
|
||||
|
||||
INPUT_HAKO="$NYASH_ROOT/apps/tests/phase143_p2_loop_true_if_bc_min.hako"
|
||||
OUTPUT_EXE="$NYASH_ROOT/tmp/phase143_p2_loop_true_if_bc_llvm_exe"
|
||||
|
||||
EXPECTED_EXIT_CODE=8
|
||||
LLVM_BUILD_LOG="/tmp/phase143_p2_loop_true_if_bc_llvm_build.log"
|
||||
|
||||
if llvm_exe_build_and_run_expect_exit_code; then
|
||||
test_pass "phase143_p2_loop_true_if_bc_llvm_exe: exit code matches (8)"
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@ -0,0 +1,51 @@
|
||||
#!/bin/bash
|
||||
# Phase 143 P2: loop(true) if break-continue (B-C, Normalized shadow, VM)
|
||||
#
|
||||
# Verifies Phase 143 P2 else symmetry:
|
||||
# - loop(true) { if flag==1 {break} else {continue} } return 8 → exit code 8
|
||||
# - Dev-only: NYASH_JOINIR_DEV=1 HAKO_JOINIR_STRICT=1
|
||||
|
||||
source "$(dirname "$0")/../../../lib/test_runner.sh"
|
||||
export SMOKES_USE_PYVM=0
|
||||
require_env || exit 2
|
||||
|
||||
PASS_COUNT=0
|
||||
FAIL_COUNT=0
|
||||
RUN_TIMEOUT_SECS=${RUN_TIMEOUT_SECS:-10}
|
||||
|
||||
echo "[INFO] Phase 143 P2: loop(true) if break-continue (B-C, VM)"
|
||||
|
||||
echo "[INFO] Test 1: phase143_p2_loop_true_if_bc_min.hako"
|
||||
INPUT="$NYASH_ROOT/apps/tests/phase143_p2_loop_true_if_bc_min.hako"
|
||||
|
||||
set +e
|
||||
OUTPUT=$(timeout "$RUN_TIMEOUT_SECS" env \
|
||||
NYASH_DISABLE_PLUGINS=1 \
|
||||
HAKO_JOINIR_STRICT=1 \
|
||||
NYASH_JOINIR_DEV=1 \
|
||||
"$NYASH_BIN" --backend vm "$INPUT" 2>&1)
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
|
||||
if [ "$EXIT_CODE" -eq 124 ]; then
|
||||
echo "[FAIL] hakorune timed out (>${RUN_TIMEOUT_SECS}s)"
|
||||
FAIL_COUNT=$((FAIL_COUNT + 1))
|
||||
elif [ "$EXIT_CODE" -eq 8 ]; then
|
||||
echo "[PASS] exit code verified: 8"
|
||||
PASS_COUNT=$((PASS_COUNT + 1))
|
||||
else
|
||||
echo "[FAIL] hakorune failed with exit code $EXIT_CODE (expected 8)"
|
||||
echo "[INFO] output (tail):"
|
||||
echo "$OUTPUT" | tail -n 50 || true
|
||||
FAIL_COUNT=$((FAIL_COUNT + 1))
|
||||
fi
|
||||
|
||||
echo "[INFO] PASS: $PASS_COUNT, FAIL: $FAIL_COUNT"
|
||||
|
||||
if [ "$FAIL_COUNT" -eq 0 ]; then
|
||||
test_pass "phase143_p2_loop_true_if_bc_vm: All tests passed"
|
||||
exit 0
|
||||
else
|
||||
test_fail "phase143_p2_loop_true_if_bc_vm: $FAIL_COUNT test(s) failed"
|
||||
exit 1
|
||||
fi
|
||||
@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
# Phase 143 P2: loop(true) if continue-break (C-B, Normalized shadow, LLVM EXE parity)
|
||||
#
|
||||
# Verifies Phase 143 P2 else symmetry:
|
||||
# - loop(true) { if flag==1 {continue} else {break} } ; return 9 → exit code 9
|
||||
#
|
||||
# Dev-only: NYASH_JOINIR_DEV=1 HAKO_JOINIR_STRICT=1
|
||||
|
||||
source "$(dirname "$0")/../../../lib/test_runner.sh"
|
||||
source "$(dirname "$0")/../../../lib/llvm_exe_runner.sh"
|
||||
export SMOKES_USE_PYVM=0
|
||||
require_env || exit 2
|
||||
|
||||
# Preflight check (SKIP gate)
|
||||
llvm_exe_preflight_or_skip || exit 0
|
||||
|
||||
# JoinIR dev mode (Phase 130+ gate)
|
||||
require_joinir_dev
|
||||
|
||||
# Minimal plugins (String + Integer for comparisons and arithmetic)
|
||||
STRINGBOX_SO="$NYASH_ROOT/plugins/nyash-string-plugin/libnyash_string_plugin.so"
|
||||
INTEGERBOX_SO="$NYASH_ROOT/plugins/nyash-integer-plugin/libnyash_integer_plugin.so"
|
||||
LLVM_REQUIRED_PLUGINS=(
|
||||
"StringBox|$STRINGBOX_SO|nyash-string-plugin"
|
||||
"IntegerBox|$INTEGERBOX_SO|nyash-integer-plugin"
|
||||
)
|
||||
LLVM_PLUGIN_BUILD_LOG="/tmp/phase143_p2_loop_true_if_cb_llvm_plugin_build.log"
|
||||
llvm_exe_ensure_plugins_or_fail || exit 1
|
||||
|
||||
INPUT_HAKO="$NYASH_ROOT/apps/tests/phase143_p2_loop_true_if_cb_min.hako"
|
||||
OUTPUT_EXE="$NYASH_ROOT/tmp/phase143_p2_loop_true_if_cb_llvm_exe"
|
||||
|
||||
EXPECTED_EXIT_CODE=9
|
||||
LLVM_BUILD_LOG="/tmp/phase143_p2_loop_true_if_cb_llvm_build.log"
|
||||
|
||||
if llvm_exe_build_and_run_expect_exit_code; then
|
||||
test_pass "phase143_p2_loop_true_if_cb_llvm_exe: exit code matches (9)"
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@ -0,0 +1,51 @@
|
||||
#!/bin/bash
|
||||
# Phase 143 P2: loop(true) if continue-break (C-B, Normalized shadow, VM)
|
||||
#
|
||||
# Verifies Phase 143 P2 else symmetry:
|
||||
# - loop(true) { if flag==1 {continue} else {break} } return 9 → exit code 9
|
||||
# - Dev-only: NYASH_JOINIR_DEV=1 HAKO_JOINIR_STRICT=1
|
||||
|
||||
source "$(dirname "$0")/../../../lib/test_runner.sh"
|
||||
export SMOKES_USE_PYVM=0
|
||||
require_env || exit 2
|
||||
|
||||
PASS_COUNT=0
|
||||
FAIL_COUNT=0
|
||||
RUN_TIMEOUT_SECS=${RUN_TIMEOUT_SECS:-10}
|
||||
|
||||
echo "[INFO] Phase 143 P2: loop(true) if continue-break (C-B, VM)"
|
||||
|
||||
echo "[INFO] Test 1: phase143_p2_loop_true_if_cb_min.hako"
|
||||
INPUT="$NYASH_ROOT/apps/tests/phase143_p2_loop_true_if_cb_min.hako"
|
||||
|
||||
set +e
|
||||
OUTPUT=$(timeout "$RUN_TIMEOUT_SECS" env \
|
||||
NYASH_DISABLE_PLUGINS=1 \
|
||||
HAKO_JOINIR_STRICT=1 \
|
||||
NYASH_JOINIR_DEV=1 \
|
||||
"$NYASH_BIN" --backend vm "$INPUT" 2>&1)
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
|
||||
if [ "$EXIT_CODE" -eq 124 ]; then
|
||||
echo "[FAIL] hakorune timed out (>${RUN_TIMEOUT_SECS}s)"
|
||||
FAIL_COUNT=$((FAIL_COUNT + 1))
|
||||
elif [ "$EXIT_CODE" -eq 9 ]; then
|
||||
echo "[PASS] exit code verified: 9"
|
||||
PASS_COUNT=$((PASS_COUNT + 1))
|
||||
else
|
||||
echo "[FAIL] hakorune failed with exit code $EXIT_CODE (expected 9)"
|
||||
echo "[INFO] output (tail):"
|
||||
echo "$OUTPUT" | tail -n 50 || true
|
||||
FAIL_COUNT=$((FAIL_COUNT + 1))
|
||||
fi
|
||||
|
||||
echo "[INFO] PASS: $PASS_COUNT, FAIL: $FAIL_COUNT"
|
||||
|
||||
if [ "$FAIL_COUNT" -eq 0 ]; then
|
||||
test_pass "phase143_p2_loop_true_if_cb_vm: All tests passed"
|
||||
exit 0
|
||||
else
|
||||
test_fail "phase143_p2_loop_true_if_cb_vm: $FAIL_COUNT test(s) failed"
|
||||
exit 1
|
||||
fi
|
||||
@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
# Phase 145 P1: ANF String.length() hoist (LLVM EXE parity)
|
||||
|
||||
source "$(dirname "$0")/../../../lib/test_runner.sh"
|
||||
source "$(dirname "$0")/../../../lib/llvm_exe_runner.sh"
|
||||
export SMOKES_USE_PYVM=0
|
||||
require_env || exit 2
|
||||
|
||||
llvm_exe_preflight_or_skip || exit 0
|
||||
|
||||
# Minimal plugins (String + Integer for s.length() and arithmetic)
|
||||
STRINGBOX_SO="$NYASH_ROOT/plugins/nyash-string-plugin/libnyash_string_plugin.so"
|
||||
INTEGERBOX_SO="$NYASH_ROOT/plugins/nyash-integer-plugin/libnyash_integer_plugin.so"
|
||||
LLVM_REQUIRED_PLUGINS=(
|
||||
"StringBox|$STRINGBOX_SO|nyash-string-plugin"
|
||||
"IntegerBox|$INTEGERBOX_SO|nyash-integer-plugin"
|
||||
)
|
||||
LLVM_PLUGIN_BUILD_LOG="/tmp/phase145_p1_anf_length_llvm_plugin_build.log"
|
||||
llvm_exe_ensure_plugins_or_fail || exit 1
|
||||
|
||||
INPUT_HAKO="$NYASH_ROOT/apps/tests/phase145_p1_anf_length_min.hako"
|
||||
OUTPUT_EXE="$NYASH_ROOT/tmp/phase145_p1_anf_length_llvm_exe"
|
||||
|
||||
# Expected exit code: 12 (5 + 3 + 4)
|
||||
EXPECTED_EXIT_CODE=12
|
||||
LLVM_BUILD_LOG="/tmp/phase145_p1_anf_length_build.log"
|
||||
|
||||
# ANF transformation enabled during compilation
|
||||
export HAKO_ANF_DEV=1
|
||||
|
||||
if llvm_exe_build_and_run_expect_exit_code; then
|
||||
test_pass "phase145_p1_anf_length_llvm_exe: ANF transformation verified (exit 12)"
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
# Phase 145 P1: ANF String.length() hoist (VM)
|
||||
#
|
||||
# Verifies ANF transformation: x + s.length() → t = s.length(); result = x + t
|
||||
# Expected exit code: 12 (5 + 3 + 4)
|
||||
|
||||
source "$(dirname "$0")/../../../lib/test_runner.sh"
|
||||
export SMOKES_USE_PYVM=0
|
||||
require_env || exit 2
|
||||
|
||||
PASS_COUNT=0
|
||||
FAIL_COUNT=0
|
||||
RUN_TIMEOUT_SECS=${RUN_TIMEOUT_SECS:-10}
|
||||
|
||||
INPUT="$NYASH_ROOT/apps/tests/phase145_p1_anf_length_min.hako"
|
||||
|
||||
echo "[INFO] Phase 145 P1: ANF length() hoist (VM) - $INPUT"
|
||||
|
||||
set +e
|
||||
OUTPUT=$(timeout "$RUN_TIMEOUT_SECS" env \
|
||||
HAKO_ANF_DEV=1 \
|
||||
NYASH_DISABLE_PLUGINS=1 \
|
||||
"$NYASH_BIN" --backend vm "$INPUT" 2>&1)
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
|
||||
if [ "$EXIT_CODE" -eq 124 ]; then
|
||||
echo "[FAIL] hakorune timed out (>${RUN_TIMEOUT_SECS}s)"
|
||||
FAIL_COUNT=$((FAIL_COUNT + 1))
|
||||
elif [ "$EXIT_CODE" -eq 12 ]; then
|
||||
echo "[PASS] Exit code verified: 12 (5 + 3 + 4)"
|
||||
PASS_COUNT=$((PASS_COUNT + 1))
|
||||
else
|
||||
echo "[FAIL] Expected exit code 12, got $EXIT_CODE"
|
||||
echo "[INFO] output (tail):"
|
||||
echo "$OUTPUT" | tail -n 50 || true
|
||||
FAIL_COUNT=$((FAIL_COUNT + 1))
|
||||
fi
|
||||
|
||||
echo "[INFO] PASS: $PASS_COUNT, FAIL: $FAIL_COUNT"
|
||||
|
||||
if [ "$FAIL_COUNT" -eq 0 ]; then
|
||||
test_pass "phase145_p1_anf_length_vm: ANF transformation verified (exit 12)"
|
||||
exit 0
|
||||
else
|
||||
test_fail "phase145_p1_anf_length_vm: $FAIL_COUNT test(s) failed"
|
||||
exit 1
|
||||
fi
|
||||
@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
# Phase 145 P2: Recursive ANF with nested BinaryOp + MethodCall (LLVM EXE parity)
|
||||
#
|
||||
# Pattern: x + s.length() + z → exit code 18 (10 + 5 + 3)
|
||||
# Dev-only: HAKO_ANF_DEV=1
|
||||
|
||||
source "$(dirname "$0")/../../../lib/test_runner.sh"
|
||||
source "$(dirname "$0")/../../../lib/llvm_exe_runner.sh"
|
||||
export SMOKES_USE_PYVM=0
|
||||
require_env || exit 2
|
||||
|
||||
# Preflight check (SKIP gate)
|
||||
llvm_exe_preflight_or_skip || exit 0
|
||||
|
||||
# Minimal plugins (String + Integer for s.length() and arithmetic)
|
||||
STRINGBOX_SO="$NYASH_ROOT/plugins/nyash-string-plugin/libnyash_string_plugin.so"
|
||||
INTEGERBOX_SO="$NYASH_ROOT/plugins/nyash-integer-plugin/libnyash_integer_plugin.so"
|
||||
LLVM_REQUIRED_PLUGINS=(
|
||||
"StringBox|$STRINGBOX_SO|nyash-string-plugin"
|
||||
"IntegerBox|$INTEGERBOX_SO|nyash-integer-plugin"
|
||||
)
|
||||
LLVM_PLUGIN_BUILD_LOG="/tmp/phase145_p2_compound_expr_binop_llvm_plugin_build.log"
|
||||
llvm_exe_ensure_plugins_or_fail || exit 1
|
||||
|
||||
INPUT_HAKO="$NYASH_ROOT/apps/tests/phase145_p2_compound_expr_binop_min.hako"
|
||||
OUTPUT_EXE="$NYASH_ROOT/tmp/phase145_p2_compound_expr_binop_llvm_exe"
|
||||
|
||||
EXPECTED_EXIT_CODE=18
|
||||
LLVM_BUILD_LOG="/tmp/phase145_p2_compound_expr_binop_llvm_build.log"
|
||||
|
||||
export HAKO_ANF_DEV=1
|
||||
|
||||
if llvm_exe_build_and_run_expect_exit_code; then
|
||||
test_pass "phase145_p2_compound_expr_binop_llvm_exe: exit code matches (18)"
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
# Phase 145 P2: Recursive ANF with nested BinaryOp + MethodCall (VM)
|
||||
#
|
||||
# Pattern: x + s.length() + z → exit code 18 (10 + 5 + 3)
|
||||
# Dev-only: HAKO_ANF_DEV=1
|
||||
|
||||
source "$(dirname "$0")/../../../lib/test_runner.sh"
|
||||
export SMOKES_USE_PYVM=0
|
||||
require_env || exit 2
|
||||
|
||||
PASS_COUNT=0
|
||||
FAIL_COUNT=0
|
||||
RUN_TIMEOUT_SECS=${RUN_TIMEOUT_SECS:-10}
|
||||
|
||||
echo "[INFO] Phase 145 P2: Recursive ANF - nested BinaryOp + MethodCall (VM)"
|
||||
|
||||
echo "[INFO] Test 1: phase145_p2_compound_expr_binop_min.hako"
|
||||
INPUT="$NYASH_ROOT/apps/tests/phase145_p2_compound_expr_binop_min.hako"
|
||||
|
||||
set +e
|
||||
OUTPUT=$(timeout "$RUN_TIMEOUT_SECS" env \
|
||||
NYASH_DISABLE_PLUGINS=1 \
|
||||
HAKO_ANF_DEV=1 \
|
||||
"$NYASH_BIN" --backend vm "$INPUT" 2>&1)
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
|
||||
if [ "$EXIT_CODE" -eq 124 ]; then
|
||||
echo "[FAIL] hakorune timed out (>${RUN_TIMEOUT_SECS}s)"
|
||||
FAIL_COUNT=$((FAIL_COUNT + 1))
|
||||
elif [ "$EXIT_CODE" -eq 18 ]; then
|
||||
echo "[PASS] exit code verified: 18"
|
||||
PASS_COUNT=$((PASS_COUNT + 1))
|
||||
else
|
||||
echo "[FAIL] hakorune failed with exit code $EXIT_CODE (expected 18)"
|
||||
echo "[INFO] output (tail):"
|
||||
echo "$OUTPUT" | tail -n 50 || true
|
||||
FAIL_COUNT=$((FAIL_COUNT + 1))
|
||||
fi
|
||||
|
||||
echo "[INFO] PASS: $PASS_COUNT, FAIL: $FAIL_COUNT"
|
||||
|
||||
if [ "$FAIL_COUNT" -eq 0 ]; then
|
||||
test_pass "phase145_p2_compound_expr_binop_vm: All tests passed"
|
||||
exit 0
|
||||
else
|
||||
test_fail "phase145_p2_compound_expr_binop_vm: $FAIL_COUNT test(s) failed"
|
||||
exit 1
|
||||
fi
|
||||
@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
# Phase 145 P2: Recursive ANF with two MethodCall operands (LLVM EXE parity)
|
||||
#
|
||||
# Pattern: s1.length() + s2.length() → exit code 5 (2 + 3)
|
||||
# Dev-only: HAKO_ANF_DEV=1
|
||||
|
||||
source "$(dirname "$0")/../../../lib/test_runner.sh"
|
||||
source "$(dirname "$0")/../../../lib/llvm_exe_runner.sh"
|
||||
export SMOKES_USE_PYVM=0
|
||||
require_env || exit 2
|
||||
|
||||
# Preflight check (SKIP gate)
|
||||
llvm_exe_preflight_or_skip || exit 0
|
||||
|
||||
# Minimal plugins (String + Integer for length() and addition)
|
||||
STRINGBOX_SO="$NYASH_ROOT/plugins/nyash-string-plugin/libnyash_string_plugin.so"
|
||||
INTEGERBOX_SO="$NYASH_ROOT/plugins/nyash-integer-plugin/libnyash_integer_plugin.so"
|
||||
LLVM_REQUIRED_PLUGINS=(
|
||||
"StringBox|$STRINGBOX_SO|nyash-string-plugin"
|
||||
"IntegerBox|$INTEGERBOX_SO|nyash-integer-plugin"
|
||||
)
|
||||
LLVM_PLUGIN_BUILD_LOG="/tmp/phase145_p2_compound_expr_double_intrinsic_llvm_plugin_build.log"
|
||||
llvm_exe_ensure_plugins_or_fail || exit 1
|
||||
|
||||
INPUT_HAKO="$NYASH_ROOT/apps/tests/phase145_p2_compound_expr_double_intrinsic_min.hako"
|
||||
OUTPUT_EXE="$NYASH_ROOT/tmp/phase145_p2_compound_expr_double_intrinsic_llvm_exe"
|
||||
|
||||
EXPECTED_EXIT_CODE=5
|
||||
LLVM_BUILD_LOG="/tmp/phase145_p2_compound_expr_double_intrinsic_llvm_build.log"
|
||||
|
||||
export HAKO_ANF_DEV=1
|
||||
|
||||
if llvm_exe_build_and_run_expect_exit_code; then
|
||||
test_pass "phase145_p2_compound_expr_double_intrinsic_llvm_exe: exit code matches (5)"
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
# Phase 145 P2: Recursive ANF with double MethodCall (VM)
|
||||
#
|
||||
# Pattern: s1.length() + s2.length() → exit code 5 (2 + 3)
|
||||
# Dev-only: HAKO_ANF_DEV=1
|
||||
|
||||
source "$(dirname "$0")/../../../lib/test_runner.sh"
|
||||
export SMOKES_USE_PYVM=0
|
||||
require_env || exit 2
|
||||
|
||||
PASS_COUNT=0
|
||||
FAIL_COUNT=0
|
||||
RUN_TIMEOUT_SECS=${RUN_TIMEOUT_SECS:-10}
|
||||
|
||||
echo "[INFO] Phase 145 P2: Recursive ANF - double MethodCall (VM)"
|
||||
|
||||
echo "[INFO] Test 1: phase145_p2_compound_expr_double_intrinsic_min.hako"
|
||||
INPUT="$NYASH_ROOT/apps/tests/phase145_p2_compound_expr_double_intrinsic_min.hako"
|
||||
|
||||
set +e
|
||||
OUTPUT=$(timeout "$RUN_TIMEOUT_SECS" env \
|
||||
NYASH_DISABLE_PLUGINS=1 \
|
||||
HAKO_ANF_DEV=1 \
|
||||
"$NYASH_BIN" --backend vm "$INPUT" 2>&1)
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
|
||||
if [ "$EXIT_CODE" -eq 124 ]; then
|
||||
echo "[FAIL] hakorune timed out (>${RUN_TIMEOUT_SECS}s)"
|
||||
FAIL_COUNT=$((FAIL_COUNT + 1))
|
||||
elif [ "$EXIT_CODE" -eq 5 ]; then
|
||||
echo "[PASS] exit code verified: 5"
|
||||
PASS_COUNT=$((PASS_COUNT + 1))
|
||||
else
|
||||
echo "[FAIL] hakorune failed with exit code $EXIT_CODE (expected 5)"
|
||||
echo "[INFO] output (tail):"
|
||||
echo "$OUTPUT" | tail -n 50 || true
|
||||
FAIL_COUNT=$((FAIL_COUNT + 1))
|
||||
fi
|
||||
|
||||
echo "[INFO] PASS: $PASS_COUNT, FAIL: $FAIL_COUNT"
|
||||
|
||||
if [ "$FAIL_COUNT" -eq 0 ]; then
|
||||
test_pass "phase145_p2_compound_expr_double_intrinsic_vm: All tests passed"
|
||||
exit 0
|
||||
else
|
||||
test_fail "phase145_p2_compound_expr_double_intrinsic_vm: $FAIL_COUNT test(s) failed"
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user