feat(normalized): Phase 141 P1.5 - external env inputs + KnownIntrinsic SSOT
## Task B: External env input bug fix (Priority 1) Fix: Suffix normalization couldn't access prefix-built local variables **Problem**: `s.length()` failed because 's' (from prefix `s = "abc"`) was not in available_inputs during suffix normalization. **Root cause**: `AvailableInputsCollectorBox::collect()` only collected function params and CapturedEnv, missing `builder.variable_map`. **Solution**: Add `prefix_variables` parameter with 3-source merge: 1. Function params (highest priority) 2. Prefix variables (medium priority - NEW) 3. CapturedEnv (lowest priority) **Changed files**: - src/mir/control_tree/normalized_shadow/available_inputs_collector.rs - src/mir/builder/control_flow/normalization/execute_box.rs - src/mir/builder/control_flow/joinir/patterns/policies/normalized_shadow_suffix_router_box.rs - src/mir/builder/control_flow/joinir/routing.rs - src/mir/builder/stmts.rs - src/mir/control_tree/normalized_shadow/dev_pipeline.rs - docs/development/current/main/design/normalized-expr-lowering.md (Available Inputs SSOT section) **Tests**: 3 new unit tests (prefix merge, priority order) ## Task A: KnownIntrinsic SSOT化 (Priority 2) Eliminate string literal scattered matching by centralizing to registry. **Problem**: Adding new intrinsics required editing if/match chains with hard-coded string literals (`if method == KnownIntrinsic::Length0.method_name()`). **Solution**: Create `KnownIntrinsicRegistryBox` as SSOT: - `lookup(method, arity) -> Option<KnownIntrinsic>` - `get_spec(intrinsic) -> KnownIntrinsicSpec` - Adding new intrinsics now requires: (1) enum variant, (2) registry entry only **Changed files**: - src/mir/control_tree/normalized_shadow/common/known_intrinsics.rs (NEW) - src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs - src/mir/control_tree/normalized_shadow/common/expr_lowering_contract.rs (deprecated methods removed) - src/mir/control_tree/normalized_shadow/common/mod.rs - docs/development/current/main/design/normalized-expr-lowering.md (Known Intrinsic SSOT section) **Impact**: ~30% code reduction in intrinsic matching logic ## Task C: Better diagnostics (Priority 3) Add `OutOfScopeReason::IntrinsicNotWhitelisted` for precise diagnostics. **Changed files**: - src/mir/control_tree/normalized_shadow/common/expr_lowering_contract.rs (enum variant) - src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs (diagnostic logic) ## Verification ✅ Build: `cargo build --release` - PASS ✅ Phase 97 regression: next_non_ws LLVM EXE - PASS ✅ Phase 131: loop(true) break-once VM - PASS ✅ Phase 136: return literal VM - PASS ✅ Phase 137: return x+2 VM - PASS 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
193
docs/development/current/main/design/normalized-expr-lowering.md
Normal file
193
docs/development/current/main/design/normalized-expr-lowering.md
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
# Normalized Expression Lowering (ExprLowererBox)
|
||||||
|
|
||||||
|
Status: SSOT
|
||||||
|
Scope: Normalized shadow での expression lowering(pure のみ)を、パターン総当たりではなく AST walker で一般化する。
|
||||||
|
Related:
|
||||||
|
- `docs/development/current/main/30-Backlog.md`
|
||||||
|
- `docs/development/current/main/phases/phase-138/README.md`
|
||||||
|
- `docs/development/current/main/phases/phase-140/README.md`
|
||||||
|
- `docs/development/current/main/phases/phase-141/README.md`
|
||||||
|
- `src/mir/control_tree/normalized_shadow/common/return_value_lowerer_box.rs`
|
||||||
|
- `src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs`
|
||||||
|
- `src/mir/control_tree/normalized_shadow/common/expr_lowering_contract.rs`
|
||||||
|
- `src/mir/builder/control_flow/normalization/README.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 目的(なぜ必要か)
|
||||||
|
|
||||||
|
Phase 131–138 で loop(true) 系の Normalized shadow を段階的に拡張したが、return/式の表現力を「形ごとのパターン追加」で増やし続けると、パターン爆発で持続不可能になる。
|
||||||
|
|
||||||
|
ここでの収束方針は次の通り:
|
||||||
|
|
||||||
|
- **制御フローの骨格(loop/if/post_k/continuation)は正規化(段階投入)で固める**
|
||||||
|
- **式(return value を含む)は一般化(AST walker)で受ける**
|
||||||
|
|
||||||
|
## 非目標
|
||||||
|
|
||||||
|
- Call/MethodCall の一般化(effects + typing を含む)を Phase 140 でやらない
|
||||||
|
→ Phase 141+ に分離して段階投入する。
|
||||||
|
- PHI 生成を Normalized 側に戻すこと(PHI-free 維持。必要なら後段に押し出す)
|
||||||
|
|
||||||
|
## SSOT: NormalizedExprLowererBox
|
||||||
|
|
||||||
|
### 役割
|
||||||
|
|
||||||
|
`NormalizedExprLowererBox` は、pure expression を JoinIR(Normalized dialect)へ lowering する SSOT になる。
|
||||||
|
|
||||||
|
- 入力: AST(`AstNodeHandle` / `ASTNode`)+ env(`BTreeMap<String, ValueId>`)+ 出力先 `Vec<JoinInst>` + `next_value_id`
|
||||||
|
- 出力: `Result<Option<ValueId>, String>`
|
||||||
|
- `Ok(Some(vid))`: lowering 成功(`vid` が式の値)
|
||||||
|
- `Ok(None)`: out-of-scope(既存経路へフォールバック、既定挙動不変)
|
||||||
|
- `Err(...)`: 内部不整合のみ(strict では fail-fast)
|
||||||
|
|
||||||
|
### Pure Expression の定義(Phase 140 の範囲)
|
||||||
|
|
||||||
|
副作用がなく、Control-flow を内包しない式のみ。
|
||||||
|
|
||||||
|
- `Variable`
|
||||||
|
- `Literal`(Integer/Bool を主対象。String/Float は必要になったら追加)
|
||||||
|
- `UnaryOp`(`not`, `-`)
|
||||||
|
- `BinaryOp`(`+ - * /`)
|
||||||
|
- `Compare`(`== < <= > >=` 等の比較系)
|
||||||
|
|
||||||
|
Call/MethodCall は対象外(Phase 141+)。
|
||||||
|
|
||||||
|
## ReturnValueLowererBox の縮退方針
|
||||||
|
|
||||||
|
Phase 138 時点では `ReturnValueLowererBox` が return の形(var/int/add)を直接扱っている。
|
||||||
|
|
||||||
|
Phase 140 以降は、`ReturnValueLowererBox` を「return 構文(None/Some)を処理する薄い箱」に縮退し、実体は `NormalizedExprLowererBox` に委譲する方針とする。
|
||||||
|
|
||||||
|
実装 SSOT:
|
||||||
|
- `src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs`
|
||||||
|
- `src/mir/control_tree/normalized_shadow/common/return_value_lowerer_box.rs`
|
||||||
|
|
||||||
|
## 収束ロードマップ
|
||||||
|
|
||||||
|
- Phase 139: if-only `post_if_post_k.rs` の return を `ReturnValueLowererBox` へ統一(出口 SSOT 完成)
|
||||||
|
- Phase 140: `NormalizedExprLowererBox` 初版(pure のみ)導入、return は ExprLowerer 経由へ寄せる
|
||||||
|
- Phase 141 P0: impure 拡張点(contract)を SSOT 化(Call/MethodCall はまだ out-of-scope)
|
||||||
|
- Phase 141 P1: “既知 intrinsic だけ” を許可して段階投入(それ以外は out-of-scope のまま)
|
||||||
|
- Phase 141 P2+: Call/MethodCall を effects + typing で分離しつつ段階投入
|
||||||
|
|
||||||
|
## パターン爆発の分解(実務)
|
||||||
|
|
||||||
|
パターン爆発には “別種” が混ざるため、対策も分ける。
|
||||||
|
|
||||||
|
- **式(return 値 / 右辺)の爆発**: `return 7`, `return x+2`, `return x+y`, `return (x+2)+3` …
|
||||||
|
- 対策: ExprLowerer(AST walker)の対応ノードを増やす 1 箇所に収束させる。
|
||||||
|
- **制御(loop/if/break/continue)の爆発**: `loop(true){break}` と `loop(true){if(...) break}` 等
|
||||||
|
- 対策: ControlTree/StepTree の語彙(If/Loop/Exit)を増やし、同じ lowering に通す。
|
||||||
|
- “新パターン” ではなく “語彙追加” にするのが収束の鍵。
|
||||||
|
|
||||||
|
## 正規化単位(NormalizationPlan の粒度)
|
||||||
|
|
||||||
|
現状は `NormalizationPlanBox` が block suffix を単位として正規化(loop + post assigns + return など)を扱う。
|
||||||
|
|
||||||
|
将来の収束案として、正規化単位を **statement(loop 1 個)** に寄せ、post/return は通常の lowering に任せる選択肢がある。
|
||||||
|
|
||||||
|
- 利点: suffix パターン増殖の圧力が下がる
|
||||||
|
- 注意: 既存の Phase 132–138 の成果(post_k/DirectValue/exit reconnect)と整合する形で段階移行すること
|
||||||
|
- 既定方針: Phase 140(pure expr)で ExprLowerer を確立した後に再検討する(Phase 141+ の設計判断)
|
||||||
|
|
||||||
|
## 受け入れ基準(設計)
|
||||||
|
|
||||||
|
- "形ごとの if 分岐追加" ではなく、AST walker の対応ノード追加が主たる拡張点になっている
|
||||||
|
- out-of-scope は `Ok(None)` でフォールバックし、既定挙動不変を維持する
|
||||||
|
- JoinIR merge は by-name 推測をせず、boundary/contract を SSOT として従う
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Available Inputs SSOT (Phase 141 P1.5)
|
||||||
|
|
||||||
|
### 問題(Problem)
|
||||||
|
|
||||||
|
Suffix normalization couldn't access prefix-built local variables (e.g., `s = "abc"`).
|
||||||
|
|
||||||
|
**Bug scenario**:
|
||||||
|
```hako
|
||||||
|
s = "abc" // Prefix: builder.variable_map["s"] = ValueId(42)
|
||||||
|
flag = 1
|
||||||
|
if flag == 1 { // Suffix normalization starts
|
||||||
|
s = s
|
||||||
|
}
|
||||||
|
return s.length() // ERROR: 's' not in available_inputs!
|
||||||
|
```
|
||||||
|
|
||||||
|
**Root cause**: `AvailableInputsCollectorBox::collect(builder, None)` only collected function params and CapturedEnv, missing prefix-built local variables.
|
||||||
|
|
||||||
|
### 解決策(Solution)
|
||||||
|
|
||||||
|
3-source merge with priority order:
|
||||||
|
|
||||||
|
1. **Function params** (highest priority - from `scope_ctx.function_param_names`)
|
||||||
|
2. **Prefix variables** (medium priority - from `builder.variable_map`)
|
||||||
|
3. **CapturedEnv** (lowest priority - from closure captures)
|
||||||
|
|
||||||
|
### Contract
|
||||||
|
|
||||||
|
```rust
|
||||||
|
AvailableInputsCollectorBox::collect(
|
||||||
|
builder: &MirBuilder,
|
||||||
|
captured_env: Option<&CapturedEnv>,
|
||||||
|
prefix_variables: Option<&BTreeMap<String, ValueId>>, // NEW: Phase 141 P1.5
|
||||||
|
) -> BTreeMap<String, ValueId>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// At call site (e.g., stmts.rs)
|
||||||
|
let prefix_variables = Some(&self.variable_ctx.variable_map);
|
||||||
|
|
||||||
|
// Execute normalization
|
||||||
|
NormalizationExecuteBox::execute(
|
||||||
|
builder,
|
||||||
|
&plan,
|
||||||
|
remaining,
|
||||||
|
func_name,
|
||||||
|
debug,
|
||||||
|
prefix_variables, // Pass through
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Inside execute_loop_only/execute_loop_with_post
|
||||||
|
let available_inputs = AvailableInputsCollectorBox::collect(
|
||||||
|
builder,
|
||||||
|
None,
|
||||||
|
prefix_variables, // Merge prefix variables
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
|
||||||
|
- **Variable shadowing**: Function params override prefix variables
|
||||||
|
- **Empty prefix**: `prefix_variables = None` (e.g., suffix with no prefix execution)
|
||||||
|
- **Mutation**: `variable_map` is cloned/borrowed, not moved
|
||||||
|
|
||||||
|
### SSOT Location
|
||||||
|
|
||||||
|
- **Implementation**: `src/mir/control_tree/normalized_shadow/available_inputs_collector.rs`
|
||||||
|
- **Call sites**:
|
||||||
|
- `src/mir/builder/control_flow/normalization/execute_box.rs` (execute_loop_only, execute_loop_with_post)
|
||||||
|
- `src/mir/builder/control_flow/joinir/patterns/policies/normalized_shadow_suffix_router_box.rs`
|
||||||
|
- `src/mir/builder/stmts.rs` (build_block suffix router call)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Known Intrinsic SSOT (Phase 141 P1.5)
|
||||||
|
|
||||||
|
### 目的
|
||||||
|
|
||||||
|
“既知 intrinsic だけ” の段階投入を、by-name の直書き増殖ではなく SSOT registry に収束させる。
|
||||||
|
|
||||||
|
### SSOT
|
||||||
|
|
||||||
|
- Registry: `src/mir/control_tree/normalized_shadow/common/known_intrinsics.rs`
|
||||||
|
- `KnownIntrinsicRegistryBox::{lookup,get_spec}`
|
||||||
|
- Marker enum: `src/mir/control_tree/normalized_shadow/common/expr_lowering_contract.rs`
|
||||||
|
- `KnownIntrinsic` は “識別子” のみ(metadata は registry に集約)
|
||||||
|
|
||||||
|
### Diagnostics
|
||||||
|
|
||||||
|
- `OutOfScopeReason::IntrinsicNotWhitelisted` を追加し、`MethodCall` の out-of-scope 理由を精密化する。
|
||||||
@ -30,6 +30,7 @@ impl NormalizedShadowSuffixRouterBox {
|
|||||||
/// Try to lower a block suffix starting with loop(true) + post statements
|
/// Try to lower a block suffix starting with loop(true) + post statements
|
||||||
///
|
///
|
||||||
/// Phase 134 P0: Unified with NormalizationPlanBox for pattern detection
|
/// Phase 134 P0: Unified with NormalizationPlanBox for pattern detection
|
||||||
|
/// Phase 141 P1.5: Added prefix_variables parameter for external env inputs
|
||||||
///
|
///
|
||||||
/// Returns:
|
/// Returns:
|
||||||
/// - Ok(Some(consumed)): Successfully processed remaining[..consumed]
|
/// - Ok(Some(consumed)): Successfully processed remaining[..consumed]
|
||||||
@ -40,6 +41,7 @@ impl NormalizedShadowSuffixRouterBox {
|
|||||||
remaining: &[ASTNode],
|
remaining: &[ASTNode],
|
||||||
func_name: &str,
|
func_name: &str,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
|
prefix_variables: Option<&std::collections::BTreeMap<String, crate::mir::ValueId>>,
|
||||||
) -> Result<Option<usize>, String> {
|
) -> Result<Option<usize>, String> {
|
||||||
let trace = crate::mir::builder::control_flow::joinir::trace::trace();
|
let trace = crate::mir::builder::control_flow::joinir::trace::trace();
|
||||||
|
|
||||||
@ -86,7 +88,8 @@ impl NormalizedShadowSuffixRouterBox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Phase 134 P0: Delegate execution to NormalizationExecuteBox (SSOT)
|
// Phase 134 P0: Delegate execution to NormalizationExecuteBox (SSOT)
|
||||||
match NormalizationExecuteBox::execute(builder, &plan, remaining, func_name, debug) {
|
// Phase 141 P1.5: Pass prefix_variables
|
||||||
|
match NormalizationExecuteBox::execute(builder, &plan, remaining, func_name, debug, prefix_variables) {
|
||||||
Ok(_value_id) => {
|
Ok(_value_id) => {
|
||||||
// ExecuteBox returns a void constant, we don't need it for suffix routing
|
// ExecuteBox returns a void constant, we don't need it for suffix routing
|
||||||
// The consumed count is what build_block() needs
|
// The consumed count is what build_block() needs
|
||||||
|
|||||||
@ -469,7 +469,10 @@ impl MirBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Phase 134 P0: Delegate execution to NormalizationExecuteBox (SSOT)
|
// Phase 134 P0: Delegate execution to NormalizationExecuteBox (SSOT)
|
||||||
match NormalizationExecuteBox::execute(self, &plan, &remaining, func_name, debug) {
|
// Phase 141 P1.5: Pass prefix_variables (using variable_map at this point)
|
||||||
|
// Clone to avoid borrow checker conflict (self is borrowed mutably in execute)
|
||||||
|
let prefix_var_map = self.variable_ctx.variable_map.clone();
|
||||||
|
match NormalizationExecuteBox::execute(self, &plan, &remaining, func_name, debug, Some(&prefix_var_map)) {
|
||||||
Ok(value_id) => {
|
Ok(value_id) => {
|
||||||
if debug {
|
if debug {
|
||||||
trace::trace().routing(
|
trace::trace().routing(
|
||||||
|
|||||||
@ -16,6 +16,7 @@ use crate::ast::ASTNode;
|
|||||||
use crate::mir::builder::MirBuilder;
|
use crate::mir::builder::MirBuilder;
|
||||||
use crate::mir::ValueId;
|
use crate::mir::ValueId;
|
||||||
use super::plan::{NormalizationPlan, PlanKind};
|
use super::plan::{NormalizationPlan, PlanKind};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
/// Box-First: Execute normalization plan
|
/// Box-First: Execute normalization plan
|
||||||
pub struct NormalizationExecuteBox;
|
pub struct NormalizationExecuteBox;
|
||||||
@ -23,6 +24,8 @@ pub struct NormalizationExecuteBox;
|
|||||||
impl NormalizationExecuteBox {
|
impl NormalizationExecuteBox {
|
||||||
/// Execute a normalization plan
|
/// Execute a normalization plan
|
||||||
///
|
///
|
||||||
|
/// ## Phase 141 P1.5: Added prefix_variables parameter
|
||||||
|
///
|
||||||
/// Returns:
|
/// Returns:
|
||||||
/// - Ok(value_id): Successfully executed, returns result value
|
/// - Ok(value_id): Successfully executed, returns result value
|
||||||
/// - Err(_): Lowering or merge failed
|
/// - Err(_): Lowering or merge failed
|
||||||
@ -32,6 +35,7 @@ impl NormalizationExecuteBox {
|
|||||||
remaining: &[ASTNode],
|
remaining: &[ASTNode],
|
||||||
func_name: &str,
|
func_name: &str,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
|
prefix_variables: Option<&BTreeMap<String, ValueId>>,
|
||||||
) -> Result<ValueId, String> {
|
) -> Result<ValueId, String> {
|
||||||
let trace = crate::mir::builder::control_flow::joinir::trace::trace();
|
let trace = crate::mir::builder::control_flow::joinir::trace::trace();
|
||||||
|
|
||||||
@ -57,20 +61,23 @@ impl NormalizationExecuteBox {
|
|||||||
|
|
||||||
match &plan.kind {
|
match &plan.kind {
|
||||||
PlanKind::LoopOnly => {
|
PlanKind::LoopOnly => {
|
||||||
Self::execute_loop_only(builder, remaining, func_name, debug)
|
Self::execute_loop_only(builder, remaining, func_name, debug, prefix_variables)
|
||||||
}
|
}
|
||||||
PlanKind::LoopWithPost { .. } => {
|
PlanKind::LoopWithPost { .. } => {
|
||||||
Self::execute_loop_with_post(builder, plan, remaining, func_name, debug)
|
Self::execute_loop_with_post(builder, plan, remaining, func_name, debug, prefix_variables)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute Phase 131: Loop-only pattern
|
/// Execute Phase 131: Loop-only pattern
|
||||||
|
///
|
||||||
|
/// ## Phase 141 P1.5: Added prefix_variables parameter
|
||||||
fn execute_loop_only(
|
fn execute_loop_only(
|
||||||
builder: &mut MirBuilder,
|
builder: &mut MirBuilder,
|
||||||
remaining: &[ASTNode],
|
remaining: &[ASTNode],
|
||||||
func_name: &str,
|
func_name: &str,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
|
prefix_variables: Option<&BTreeMap<String, ValueId>>,
|
||||||
) -> Result<ValueId, String> {
|
) -> Result<ValueId, String> {
|
||||||
use crate::ast::Span;
|
use crate::ast::Span;
|
||||||
use crate::mir::control_tree::normalized_shadow::available_inputs_collector::AvailableInputsCollectorBox;
|
use crate::mir::control_tree::normalized_shadow::available_inputs_collector::AvailableInputsCollectorBox;
|
||||||
@ -92,8 +99,8 @@ impl NormalizationExecuteBox {
|
|||||||
|
|
||||||
let tree = StepTreeBuilderBox::build_from_ast(&loop_ast);
|
let tree = StepTreeBuilderBox::build_from_ast(&loop_ast);
|
||||||
|
|
||||||
// Collect available inputs
|
// Collect available inputs (Phase 141 P1.5: with prefix variables)
|
||||||
let available_inputs = AvailableInputsCollectorBox::collect(builder, None);
|
let available_inputs = AvailableInputsCollectorBox::collect(builder, None, prefix_variables);
|
||||||
|
|
||||||
if debug {
|
if debug {
|
||||||
trace.routing(
|
trace.routing(
|
||||||
@ -141,12 +148,15 @@ impl NormalizationExecuteBox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Execute Phase 132-133: Loop + post assignments + return
|
/// Execute Phase 132-133: Loop + post assignments + return
|
||||||
|
///
|
||||||
|
/// ## Phase 141 P1.5: Added prefix_variables parameter
|
||||||
fn execute_loop_with_post(
|
fn execute_loop_with_post(
|
||||||
builder: &mut MirBuilder,
|
builder: &mut MirBuilder,
|
||||||
plan: &NormalizationPlan,
|
plan: &NormalizationPlan,
|
||||||
remaining: &[ASTNode],
|
remaining: &[ASTNode],
|
||||||
func_name: &str,
|
func_name: &str,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
|
prefix_variables: Option<&BTreeMap<String, ValueId>>,
|
||||||
) -> Result<ValueId, String> {
|
) -> Result<ValueId, String> {
|
||||||
use crate::mir::control_tree::normalized_shadow::available_inputs_collector::AvailableInputsCollectorBox;
|
use crate::mir::control_tree::normalized_shadow::available_inputs_collector::AvailableInputsCollectorBox;
|
||||||
use crate::mir::control_tree::normalized_shadow::StepTreeNormalizedShadowLowererBox;
|
use crate::mir::control_tree::normalized_shadow::StepTreeNormalizedShadowLowererBox;
|
||||||
@ -158,8 +168,8 @@ impl NormalizationExecuteBox {
|
|||||||
let suffix = &remaining[..plan.consumed];
|
let suffix = &remaining[..plan.consumed];
|
||||||
let step_tree = StepTreeBuilderBox::build_from_block(suffix);
|
let step_tree = StepTreeBuilderBox::build_from_block(suffix);
|
||||||
|
|
||||||
// Collect available inputs
|
// Collect available inputs (Phase 141 P1.5: with prefix variables)
|
||||||
let available_inputs = AvailableInputsCollectorBox::collect(builder, None);
|
let available_inputs = AvailableInputsCollectorBox::collect(builder, None, prefix_variables);
|
||||||
|
|
||||||
if debug {
|
if debug {
|
||||||
trace.routing(
|
trace.routing(
|
||||||
|
|||||||
@ -198,8 +198,11 @@ impl super::MirBuilder {
|
|||||||
let debug = trace.is_enabled();
|
let debug = trace.is_enabled();
|
||||||
|
|
||||||
use crate::mir::builder::control_flow::joinir::patterns::policies::normalized_shadow_suffix_router_box::NormalizedShadowSuffixRouterBox;
|
use crate::mir::builder::control_flow::joinir::patterns::policies::normalized_shadow_suffix_router_box::NormalizedShadowSuffixRouterBox;
|
||||||
|
// Phase 141 P1.5: Pass prefix variables for external env inputs
|
||||||
|
// Clone to avoid borrow checker conflict (self is borrowed mutably in try_lower_loop_suffix)
|
||||||
|
let prefix_var_map = self.variable_ctx.variable_map.clone();
|
||||||
match NormalizedShadowSuffixRouterBox::try_lower_loop_suffix(
|
match NormalizedShadowSuffixRouterBox::try_lower_loop_suffix(
|
||||||
self, remaining, &func_name, debug
|
self, remaining, &func_name, debug, Some(&prefix_var_map)
|
||||||
)? {
|
)? {
|
||||||
Some(consumed) => {
|
Some(consumed) => {
|
||||||
trace.emit_if(
|
trace.emit_if(
|
||||||
|
|||||||
@ -1,16 +1,17 @@
|
|||||||
//! Phase 126: available_inputs collector (SSOT)
|
//! Phase 126 + Phase 141 P1.5: available_inputs collector (SSOT)
|
||||||
//!
|
//!
|
||||||
//! ## Responsibility
|
//! ## Responsibility
|
||||||
//!
|
//!
|
||||||
//! - Collect available_inputs from function params + CapturedEnv
|
//! - Collect available_inputs from function params + prefix variables + CapturedEnv
|
||||||
//! - Returns BTreeMap<String, ValueId> with deterministic order
|
//! - Returns BTreeMap<String, ValueId> with deterministic order
|
||||||
//! - No AST inference (SSOT sources only)
|
//! - No AST inference (SSOT sources only)
|
||||||
//!
|
//!
|
||||||
//! ## Design
|
//! ## Design
|
||||||
//!
|
//!
|
||||||
//! - Input sources (priority order):
|
//! - Input sources (priority order):
|
||||||
//! 1. Function params (from ScopeContext + VariableContext)
|
//! 1. Function params (from ScopeContext + VariableContext) - highest priority
|
||||||
//! 2. CapturedEnv (pinned/captured variables from outer scope)
|
//! 2. Prefix variables (from builder.variable_map) - medium priority (Phase 141 P1.5)
|
||||||
|
//! 3. CapturedEnv (pinned/captured variables from outer scope) - lowest priority
|
||||||
//! - Forbidden: AST-based capture inference (Phase 100 CapturedEnv is SSOT)
|
//! - Forbidden: AST-based capture inference (Phase 100 CapturedEnv is SSOT)
|
||||||
|
|
||||||
// Phase 126: Import contexts from MirBuilder (pub(in crate::mir) visibility)
|
// Phase 126: Import contexts from MirBuilder (pub(in crate::mir) visibility)
|
||||||
@ -26,36 +27,49 @@ pub struct AvailableInputsCollectorBox;
|
|||||||
impl AvailableInputsCollectorBox {
|
impl AvailableInputsCollectorBox {
|
||||||
/// Collect available_inputs from SSOT sources (via MirBuilder)
|
/// Collect available_inputs from SSOT sources (via MirBuilder)
|
||||||
///
|
///
|
||||||
/// ## Contract
|
/// ## Contract (Phase 141 P1.5)
|
||||||
///
|
///
|
||||||
/// - Sources (priority order):
|
/// - Sources (priority order):
|
||||||
/// 1. Function params: builder.scope_ctx.function_param_names + builder.variable_ctx.variable_map
|
/// 1. Function params: builder.scope_ctx.function_param_names + builder.variable_ctx.variable_map
|
||||||
/// 2. CapturedEnv: captured_env.vars (pinned/captured from outer scope)
|
/// 2. Prefix variables: prefix_variables (from builder.variable_map at call site)
|
||||||
|
/// 3. CapturedEnv: captured_env.vars (pinned/captured from outer scope)
|
||||||
/// - Output: BTreeMap<String, ValueId> (deterministic order)
|
/// - Output: BTreeMap<String, ValueId> (deterministic order)
|
||||||
/// - No AST inference: only use pre-computed SSOT sources
|
/// - No AST inference: only use pre-computed SSOT sources
|
||||||
///
|
///
|
||||||
/// ## Implementation
|
/// ## Implementation (Phase 141 P1.5)
|
||||||
///
|
///
|
||||||
/// - Collect function params first (higher priority)
|
/// - Collect function params first (highest priority)
|
||||||
/// - Collect CapturedEnv vars (lower priority, don't override params)
|
/// - Collect prefix variables (medium priority, don't override params)
|
||||||
|
/// - Collect CapturedEnv vars (lowest priority, don't override params or prefix)
|
||||||
/// - Use BTreeMap for deterministic iteration
|
/// - Use BTreeMap for deterministic iteration
|
||||||
pub fn collect(
|
pub fn collect(
|
||||||
builder: &MirBuilder,
|
builder: &MirBuilder,
|
||||||
captured_env: Option<&CapturedEnv>,
|
captured_env: Option<&CapturedEnv>,
|
||||||
|
prefix_variables: Option<&BTreeMap<String, ValueId>>,
|
||||||
) -> BTreeMap<String, ValueId> {
|
) -> BTreeMap<String, ValueId> {
|
||||||
let mut available_inputs = BTreeMap::new();
|
let mut available_inputs = BTreeMap::new();
|
||||||
|
|
||||||
// 1. Function params (SSOT: scope_ctx + variable_ctx)
|
// 1. Function params (SSOT: scope_ctx + variable_ctx) - highest priority
|
||||||
for param_name in &builder.scope_ctx.function_param_names {
|
for param_name in &builder.scope_ctx.function_param_names {
|
||||||
if let Some(value_id) = builder.variable_ctx.lookup(param_name) {
|
if let Some(value_id) = builder.variable_ctx.lookup(param_name) {
|
||||||
available_inputs.insert(param_name.clone(), value_id);
|
available_inputs.insert(param_name.clone(), value_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. CapturedEnv (SSOT: pinned/captured vars)
|
// 2. Prefix variables (medium priority) - Phase 141 P1.5
|
||||||
|
if let Some(prefix) = prefix_variables {
|
||||||
|
for (name, value_id) in prefix {
|
||||||
|
// Don't override function params (params have higher priority)
|
||||||
|
if !available_inputs.contains_key(name) {
|
||||||
|
available_inputs.insert(name.clone(), *value_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. CapturedEnv (SSOT: pinned/captured vars) - lowest priority
|
||||||
if let Some(env) = captured_env {
|
if let Some(env) = captured_env {
|
||||||
for var in &env.vars {
|
for var in &env.vars {
|
||||||
// Don't override function params (params have higher priority)
|
// Don't override function params or prefix vars
|
||||||
if !available_inputs.contains_key(&var.name) {
|
if !available_inputs.contains_key(&var.name) {
|
||||||
available_inputs.insert(var.name.clone(), var.host_id);
|
available_inputs.insert(var.name.clone(), var.host_id);
|
||||||
}
|
}
|
||||||
@ -74,7 +88,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_collect_empty() {
|
fn test_collect_empty() {
|
||||||
let builder = MirBuilder::new();
|
let builder = MirBuilder::new();
|
||||||
let result = AvailableInputsCollectorBox::collect(&builder, None);
|
let result = AvailableInputsCollectorBox::collect(&builder, None, None);
|
||||||
assert_eq!(result.len(), 0);
|
assert_eq!(result.len(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,7 +100,7 @@ mod tests {
|
|||||||
builder.scope_ctx.function_param_names.insert("x".to_string());
|
builder.scope_ctx.function_param_names.insert("x".to_string());
|
||||||
builder.variable_ctx.insert("x".to_string(), ValueId(1));
|
builder.variable_ctx.insert("x".to_string(), ValueId(1));
|
||||||
|
|
||||||
let result = AvailableInputsCollectorBox::collect(&builder, None);
|
let result = AvailableInputsCollectorBox::collect(&builder, None, None);
|
||||||
assert_eq!(result.len(), 1);
|
assert_eq!(result.len(), 1);
|
||||||
assert_eq!(result.get("x"), Some(&ValueId(1)));
|
assert_eq!(result.get("x"), Some(&ValueId(1)));
|
||||||
}
|
}
|
||||||
@ -98,7 +112,7 @@ mod tests {
|
|||||||
let mut captured = CapturedEnv::new();
|
let mut captured = CapturedEnv::new();
|
||||||
captured.insert("outer_x".to_string(), ValueId(42));
|
captured.insert("outer_x".to_string(), ValueId(42));
|
||||||
|
|
||||||
let result = AvailableInputsCollectorBox::collect(&builder, Some(&captured));
|
let result = AvailableInputsCollectorBox::collect(&builder, Some(&captured), None);
|
||||||
assert_eq!(result.len(), 1);
|
assert_eq!(result.len(), 1);
|
||||||
assert_eq!(result.get("outer_x"), Some(&ValueId(42)));
|
assert_eq!(result.get("outer_x"), Some(&ValueId(42)));
|
||||||
}
|
}
|
||||||
@ -115,7 +129,7 @@ mod tests {
|
|||||||
let mut captured = CapturedEnv::new();
|
let mut captured = CapturedEnv::new();
|
||||||
captured.insert("x".to_string(), ValueId(42));
|
captured.insert("x".to_string(), ValueId(42));
|
||||||
|
|
||||||
let result = AvailableInputsCollectorBox::collect(&builder, Some(&captured));
|
let result = AvailableInputsCollectorBox::collect(&builder, Some(&captured), None);
|
||||||
assert_eq!(result.len(), 1);
|
assert_eq!(result.len(), 1);
|
||||||
// Function param (ValueId(1)) should win over captured (ValueId(42))
|
// Function param (ValueId(1)) should win over captured (ValueId(42))
|
||||||
assert_eq!(result.get("x"), Some(&ValueId(1)));
|
assert_eq!(result.get("x"), Some(&ValueId(1)));
|
||||||
@ -133,10 +147,73 @@ mod tests {
|
|||||||
builder.variable_ctx.insert("a".to_string(), ValueId(1));
|
builder.variable_ctx.insert("a".to_string(), ValueId(1));
|
||||||
builder.variable_ctx.insert("m".to_string(), ValueId(2));
|
builder.variable_ctx.insert("m".to_string(), ValueId(2));
|
||||||
|
|
||||||
let result = AvailableInputsCollectorBox::collect(&builder, None);
|
let result = AvailableInputsCollectorBox::collect(&builder, None, None);
|
||||||
let keys: Vec<_> = result.keys().collect();
|
let keys: Vec<_> = result.keys().collect();
|
||||||
|
|
||||||
// BTreeMap ensures alphabetical order
|
// BTreeMap ensures alphabetical order
|
||||||
assert_eq!(keys, vec![&"a".to_string(), &"m".to_string(), &"z".to_string()]);
|
assert_eq!(keys, vec![&"a".to_string(), &"m".to_string(), &"z".to_string()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 141 P1.5: New tests for prefix variables
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_collect_with_prefix_variables() {
|
||||||
|
let builder = MirBuilder::new();
|
||||||
|
|
||||||
|
let mut prefix_vars = BTreeMap::new();
|
||||||
|
prefix_vars.insert("s".to_string(), ValueId(42));
|
||||||
|
prefix_vars.insert("flag".to_string(), ValueId(43));
|
||||||
|
|
||||||
|
let result = AvailableInputsCollectorBox::collect(&builder, None, Some(&prefix_vars));
|
||||||
|
assert_eq!(result.len(), 2);
|
||||||
|
assert_eq!(result.get("s"), Some(&ValueId(42)));
|
||||||
|
assert_eq!(result.get("flag"), Some(&ValueId(43)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_collect_params_override_prefix() {
|
||||||
|
let mut builder = MirBuilder::new();
|
||||||
|
|
||||||
|
// Function param: x -> ValueId(1)
|
||||||
|
builder.scope_ctx.function_param_names.insert("x".to_string());
|
||||||
|
builder.variable_ctx.insert("x".to_string(), ValueId(1));
|
||||||
|
|
||||||
|
// Prefix: x -> ValueId(42) (should be ignored)
|
||||||
|
let mut prefix_vars = BTreeMap::new();
|
||||||
|
prefix_vars.insert("x".to_string(), ValueId(42));
|
||||||
|
|
||||||
|
let result = AvailableInputsCollectorBox::collect(&builder, None, Some(&prefix_vars));
|
||||||
|
assert_eq!(result.len(), 1);
|
||||||
|
// Function param (ValueId(1)) should win over prefix (ValueId(42))
|
||||||
|
assert_eq!(result.get("x"), Some(&ValueId(1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_collect_priority_order() {
|
||||||
|
let mut builder = MirBuilder::new();
|
||||||
|
|
||||||
|
// Function param: x -> ValueId(1)
|
||||||
|
builder.scope_ctx.function_param_names.insert("x".to_string());
|
||||||
|
builder.variable_ctx.insert("x".to_string(), ValueId(1));
|
||||||
|
|
||||||
|
// Prefix: x -> ValueId(2), y -> ValueId(3)
|
||||||
|
let mut prefix_vars = BTreeMap::new();
|
||||||
|
prefix_vars.insert("x".to_string(), ValueId(2));
|
||||||
|
prefix_vars.insert("y".to_string(), ValueId(3));
|
||||||
|
|
||||||
|
// Captured: x -> ValueId(4), y -> ValueId(5), z -> ValueId(6)
|
||||||
|
let mut captured = CapturedEnv::new();
|
||||||
|
captured.insert("x".to_string(), ValueId(4));
|
||||||
|
captured.insert("y".to_string(), ValueId(5));
|
||||||
|
captured.insert("z".to_string(), ValueId(6));
|
||||||
|
|
||||||
|
let result = AvailableInputsCollectorBox::collect(&builder, Some(&captured), Some(&prefix_vars));
|
||||||
|
assert_eq!(result.len(), 3);
|
||||||
|
// x: param wins (ValueId(1))
|
||||||
|
assert_eq!(result.get("x"), Some(&ValueId(1)));
|
||||||
|
// y: prefix wins (ValueId(3))
|
||||||
|
assert_eq!(result.get("y"), Some(&ValueId(3)));
|
||||||
|
// z: captured wins (ValueId(6))
|
||||||
|
assert_eq!(result.get("z"), Some(&ValueId(6)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,887 @@
|
|||||||
|
//! NormalizedExprLowererBox: Pure expression lowering SSOT (Phase 140 P0)
|
||||||
|
//!
|
||||||
|
//! ## Responsibility
|
||||||
|
//!
|
||||||
|
//! Lower a *pure* AST expression into JoinIR `ValueId` + emitted `JoinInst`s.
|
||||||
|
//!
|
||||||
|
//! ## Scope (Phase 140 P0)
|
||||||
|
//!
|
||||||
|
//! Supported (pure only):
|
||||||
|
//! - Variable: env lookup → ValueId
|
||||||
|
//! - Literals: Integer / Bool
|
||||||
|
//! - Unary: `-` (Neg), `not` (Not)
|
||||||
|
//! - Binary arith (int-only): `+ - * /`
|
||||||
|
//! - Compare (int-only): `== != < <= > >=`
|
||||||
|
//!
|
||||||
|
//! Out of scope (returns `Ok(None)`):
|
||||||
|
//! - Call/MethodCall/FromCall/Array/Map/FieldAccess/Index/New/This/Me/…
|
||||||
|
//! - Literals other than Integer/Bool
|
||||||
|
//! - Operators outside the scope above
|
||||||
|
//!
|
||||||
|
//! ## Contract
|
||||||
|
//!
|
||||||
|
//! - Out-of-scope returns `Ok(None)` (fallback to legacy routing).
|
||||||
|
//! - `Err(_)` is reserved for internal invariants only (should be rare).
|
||||||
|
|
||||||
|
use super::expr_lowering_contract::{ExprLoweringScope, ImpurePolicy, KnownIntrinsic, OutOfScopeReason};
|
||||||
|
use super::known_intrinsics::KnownIntrinsicRegistryBox; // Phase 141 P1.5
|
||||||
|
use crate::ast::{ASTNode, BinaryOperator, LiteralValue, UnaryOperator};
|
||||||
|
use crate::mir::join_ir::{BinOpKind, CompareOp, ConstValue, JoinInst, MirLikeInst, UnaryOp};
|
||||||
|
use crate::mir::types::MirType;
|
||||||
|
use crate::mir::ValueId;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
/// Box-First: Pure expression lowering for Normalized shadow paths
|
||||||
|
pub struct NormalizedExprLowererBox;
|
||||||
|
|
||||||
|
impl NormalizedExprLowererBox {
|
||||||
|
pub fn lower_expr(
|
||||||
|
ast: &ASTNode,
|
||||||
|
env: &BTreeMap<String, ValueId>,
|
||||||
|
body: &mut Vec<JoinInst>,
|
||||||
|
next_value_id: &mut u32,
|
||||||
|
) -> Result<Option<ValueId>, String> {
|
||||||
|
Self::lower_expr_with_scope(ExprLoweringScope::PureOnly, ast, env, body, next_value_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lower_expr_with_scope(
|
||||||
|
scope: ExprLoweringScope,
|
||||||
|
ast: &ASTNode,
|
||||||
|
env: &BTreeMap<String, ValueId>,
|
||||||
|
body: &mut Vec<JoinInst>,
|
||||||
|
next_value_id: &mut u32,
|
||||||
|
) -> Result<Option<ValueId>, String> {
|
||||||
|
if Self::out_of_scope_reason(scope, ast, env).is_some() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
match ast {
|
||||||
|
ASTNode::Variable { name, .. } => Ok(env.get(name).copied()),
|
||||||
|
ASTNode::Literal { value, .. } => Self::lower_literal(value, body, next_value_id),
|
||||||
|
ASTNode::UnaryOp { operator, operand, .. } => {
|
||||||
|
Self::lower_unary(operator, operand, env, body, next_value_id)
|
||||||
|
}
|
||||||
|
ASTNode::BinaryOp { operator, left, right, .. } => {
|
||||||
|
Self::lower_binary(operator, left, right, env, body, next_value_id)
|
||||||
|
}
|
||||||
|
ASTNode::MethodCall { object, method, arguments, .. } => match scope {
|
||||||
|
ExprLoweringScope::PureOnly => Ok(None),
|
||||||
|
ExprLoweringScope::WithImpure(policy) => {
|
||||||
|
let Some(intrinsic) =
|
||||||
|
Self::match_known_intrinsic_method_call(policy, object, method, arguments, env)
|
||||||
|
else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
Self::lower_known_intrinsic_method_call(intrinsic, object, body, next_value_id, env)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Classify out-of-scope reasons for diagnostics/tests without changing the core API.
|
||||||
|
pub fn out_of_scope_reason(
|
||||||
|
scope: ExprLoweringScope,
|
||||||
|
ast: &ASTNode,
|
||||||
|
env: &BTreeMap<String, ValueId>,
|
||||||
|
) -> Option<OutOfScopeReason> {
|
||||||
|
match scope {
|
||||||
|
ExprLoweringScope::PureOnly => Self::out_of_scope_reason_pure(ast, env),
|
||||||
|
ExprLoweringScope::WithImpure(policy) => Self::out_of_scope_reason_with_impure(policy, ast, env),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn out_of_scope_reason_pure(ast: &ASTNode, env: &BTreeMap<String, ValueId>) -> Option<OutOfScopeReason> {
|
||||||
|
match ast {
|
||||||
|
ASTNode::FunctionCall { .. } | ASTNode::Call { .. } => Some(OutOfScopeReason::Call),
|
||||||
|
ASTNode::MethodCall { .. } => Some(OutOfScopeReason::MethodCall),
|
||||||
|
|
||||||
|
ASTNode::Variable { name, .. } => {
|
||||||
|
if env.contains_key(name) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(OutOfScopeReason::MissingEnvVar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ASTNode::Literal { value, .. } => match value {
|
||||||
|
LiteralValue::Integer(_) | LiteralValue::Bool(_) => None,
|
||||||
|
_ => Some(OutOfScopeReason::UnsupportedLiteral),
|
||||||
|
},
|
||||||
|
|
||||||
|
ASTNode::UnaryOp { operator, operand, .. } => match operator {
|
||||||
|
UnaryOperator::Minus => {
|
||||||
|
// Only int operand is supported.
|
||||||
|
if Self::is_supported_int_expr(operand, env) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(OutOfScopeReason::UnsupportedOperator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UnaryOperator::Not => {
|
||||||
|
// Only bool operand is supported.
|
||||||
|
if Self::is_supported_bool_expr(operand, env) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(OutOfScopeReason::UnsupportedOperator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UnaryOperator::BitNot => Some(OutOfScopeReason::UnsupportedOperator),
|
||||||
|
},
|
||||||
|
|
||||||
|
ASTNode::BinaryOp { operator, left, right, .. } => {
|
||||||
|
if Self::binary_kind(operator).is_some() {
|
||||||
|
// Phase 140 policy: binary ops require int operands (arith/compare).
|
||||||
|
if Self::is_supported_int_expr(left, env) && Self::is_supported_int_expr(right, env) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(OutOfScopeReason::UnsupportedOperator)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Some(OutOfScopeReason::UnsupportedOperator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => Some(OutOfScopeReason::ImpureExpression),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn out_of_scope_reason_with_impure(
|
||||||
|
policy: ImpurePolicy,
|
||||||
|
ast: &ASTNode,
|
||||||
|
env: &BTreeMap<String, ValueId>,
|
||||||
|
) -> Option<OutOfScopeReason> {
|
||||||
|
match ast {
|
||||||
|
ASTNode::FunctionCall { .. } | ASTNode::Call { .. } => Some(OutOfScopeReason::Call),
|
||||||
|
ASTNode::MethodCall { object, method, arguments, .. } => {
|
||||||
|
// Phase 141 P1.5: Use registry for better diagnostics
|
||||||
|
if Self::match_known_intrinsic_method_call(policy, object, method, arguments, env).is_some()
|
||||||
|
{
|
||||||
|
None // In scope (whitelisted)
|
||||||
|
} else {
|
||||||
|
// Check if it's a known intrinsic but not whitelisted
|
||||||
|
let receiver_ok = matches!(object.as_ref(), ASTNode::Variable { name, .. } if env.contains_key(name));
|
||||||
|
if receiver_ok && KnownIntrinsicRegistryBox::lookup(method, arguments.len()).is_some() {
|
||||||
|
// Known intrinsic signature but not in current policy allowlist
|
||||||
|
Some(OutOfScopeReason::IntrinsicNotWhitelisted)
|
||||||
|
} else {
|
||||||
|
// Generic method call not supported
|
||||||
|
Some(OutOfScopeReason::MethodCall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ASTNode::Variable { name, .. } => {
|
||||||
|
if env.contains_key(name) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(OutOfScopeReason::MissingEnvVar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ASTNode::Literal { value, .. } => match value {
|
||||||
|
LiteralValue::Integer(_) | LiteralValue::Bool(_) => None,
|
||||||
|
_ => Some(OutOfScopeReason::UnsupportedLiteral),
|
||||||
|
},
|
||||||
|
|
||||||
|
ASTNode::UnaryOp { operator, operand, .. } => match operator {
|
||||||
|
UnaryOperator::Minus => {
|
||||||
|
if Self::is_supported_int_expr_with_scope(ExprLoweringScope::WithImpure(policy), operand, env) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(OutOfScopeReason::UnsupportedOperator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UnaryOperator::Not => {
|
||||||
|
if Self::is_supported_bool_expr_with_scope(ExprLoweringScope::WithImpure(policy), operand, env) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(OutOfScopeReason::UnsupportedOperator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UnaryOperator::BitNot => Some(OutOfScopeReason::UnsupportedOperator),
|
||||||
|
},
|
||||||
|
|
||||||
|
ASTNode::BinaryOp { operator, left, right, .. } => {
|
||||||
|
if Self::binary_kind(operator).is_some() {
|
||||||
|
if Self::is_supported_int_expr_with_scope(ExprLoweringScope::WithImpure(policy), left, env)
|
||||||
|
&& Self::is_supported_int_expr_with_scope(ExprLoweringScope::WithImpure(policy), right, env)
|
||||||
|
{
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(OutOfScopeReason::UnsupportedOperator)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Some(OutOfScopeReason::UnsupportedOperator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => Some(OutOfScopeReason::ImpureExpression),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_supported_int_expr(ast: &ASTNode, env: &BTreeMap<String, ValueId>) -> bool {
|
||||||
|
match ast {
|
||||||
|
ASTNode::Variable { name, .. } => env.contains_key(name),
|
||||||
|
ASTNode::Literal { value: LiteralValue::Integer(_), .. } => true,
|
||||||
|
ASTNode::UnaryOp { operator: UnaryOperator::Minus, operand, .. } => {
|
||||||
|
Self::is_supported_int_expr(operand, env)
|
||||||
|
}
|
||||||
|
ASTNode::BinaryOp { operator, left, right, .. } => {
|
||||||
|
matches!(Self::binary_kind(operator), Some(BinaryKind::Arith(_)))
|
||||||
|
&& Self::is_supported_int_expr(left, env)
|
||||||
|
&& Self::is_supported_int_expr(right, env)
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_supported_bool_expr(ast: &ASTNode, env: &BTreeMap<String, ValueId>) -> bool {
|
||||||
|
match ast {
|
||||||
|
ASTNode::Variable { name, .. } => env.contains_key(name),
|
||||||
|
ASTNode::Literal { value: LiteralValue::Bool(_), .. } => true,
|
||||||
|
ASTNode::UnaryOp { operator: UnaryOperator::Not, operand, .. } => {
|
||||||
|
Self::is_supported_bool_expr(operand, env)
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_supported_int_expr_with_scope(scope: ExprLoweringScope, ast: &ASTNode, env: &BTreeMap<String, ValueId>) -> bool {
|
||||||
|
match scope {
|
||||||
|
ExprLoweringScope::PureOnly => Self::is_supported_int_expr(ast, env),
|
||||||
|
ExprLoweringScope::WithImpure(policy) => match ast {
|
||||||
|
ASTNode::MethodCall { object, method, arguments, .. } => {
|
||||||
|
Self::match_known_intrinsic_method_call(policy, object, method, arguments, env).is_some()
|
||||||
|
}
|
||||||
|
ASTNode::Variable { name, .. } => env.contains_key(name),
|
||||||
|
ASTNode::Literal { value: LiteralValue::Integer(_), .. } => true,
|
||||||
|
ASTNode::UnaryOp { operator: UnaryOperator::Minus, operand, .. } => {
|
||||||
|
Self::is_supported_int_expr_with_scope(ExprLoweringScope::WithImpure(policy), operand, env)
|
||||||
|
}
|
||||||
|
ASTNode::BinaryOp { operator, left, right, .. } => {
|
||||||
|
matches!(Self::binary_kind(operator), Some(BinaryKind::Arith(_)) | Some(BinaryKind::Compare(_)))
|
||||||
|
&& Self::is_supported_int_expr_with_scope(ExprLoweringScope::WithImpure(policy), left, env)
|
||||||
|
&& Self::is_supported_int_expr_with_scope(ExprLoweringScope::WithImpure(policy), right, env)
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_supported_bool_expr_with_scope(scope: ExprLoweringScope, ast: &ASTNode, env: &BTreeMap<String, ValueId>) -> bool {
|
||||||
|
match scope {
|
||||||
|
ExprLoweringScope::PureOnly => Self::is_supported_bool_expr(ast, env),
|
||||||
|
ExprLoweringScope::WithImpure(policy) => match ast {
|
||||||
|
ASTNode::Variable { name, .. } => env.contains_key(name),
|
||||||
|
ASTNode::Literal { value: LiteralValue::Bool(_), .. } => true,
|
||||||
|
ASTNode::UnaryOp { operator: UnaryOperator::Not, operand, .. } => {
|
||||||
|
Self::is_supported_bool_expr_with_scope(ExprLoweringScope::WithImpure(policy), operand, env)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let _ = policy;
|
||||||
|
false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn alloc_value_id(next_value_id: &mut u32) -> ValueId {
|
||||||
|
let vid = ValueId(*next_value_id);
|
||||||
|
*next_value_id += 1;
|
||||||
|
vid
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lower_literal(
|
||||||
|
value: &LiteralValue,
|
||||||
|
body: &mut Vec<JoinInst>,
|
||||||
|
next_value_id: &mut u32,
|
||||||
|
) -> Result<Option<ValueId>, String> {
|
||||||
|
match value {
|
||||||
|
LiteralValue::Integer(i) => {
|
||||||
|
let dst = Self::alloc_value_id(next_value_id);
|
||||||
|
body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||||
|
dst,
|
||||||
|
value: ConstValue::Integer(*i),
|
||||||
|
}));
|
||||||
|
Ok(Some(dst))
|
||||||
|
}
|
||||||
|
LiteralValue::Bool(b) => {
|
||||||
|
let dst = Self::alloc_value_id(next_value_id);
|
||||||
|
body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||||
|
dst,
|
||||||
|
value: ConstValue::Bool(*b),
|
||||||
|
}));
|
||||||
|
Ok(Some(dst))
|
||||||
|
}
|
||||||
|
_ => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lower_unary(
|
||||||
|
operator: &UnaryOperator,
|
||||||
|
operand: &ASTNode,
|
||||||
|
env: &BTreeMap<String, ValueId>,
|
||||||
|
body: &mut Vec<JoinInst>,
|
||||||
|
next_value_id: &mut u32,
|
||||||
|
) -> Result<Option<ValueId>, String> {
|
||||||
|
match operator {
|
||||||
|
UnaryOperator::Minus => {
|
||||||
|
let operand_vid =
|
||||||
|
match Self::lower_int_expr(operand, env, body, next_value_id)? {
|
||||||
|
Some(v) => v,
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
let dst = Self::alloc_value_id(next_value_id);
|
||||||
|
body.push(JoinInst::Compute(MirLikeInst::UnaryOp {
|
||||||
|
dst,
|
||||||
|
op: UnaryOp::Neg,
|
||||||
|
operand: operand_vid,
|
||||||
|
}));
|
||||||
|
Ok(Some(dst))
|
||||||
|
}
|
||||||
|
UnaryOperator::Not => {
|
||||||
|
let operand_vid =
|
||||||
|
match Self::lower_bool_expr(operand, env, body, next_value_id)? {
|
||||||
|
Some(v) => v,
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
let dst = Self::alloc_value_id(next_value_id);
|
||||||
|
body.push(JoinInst::Compute(MirLikeInst::UnaryOp {
|
||||||
|
dst,
|
||||||
|
op: UnaryOp::Not,
|
||||||
|
operand: operand_vid,
|
||||||
|
}));
|
||||||
|
Ok(Some(dst))
|
||||||
|
}
|
||||||
|
UnaryOperator::BitNot => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Phase 141 P1.5: Refactored to use KnownIntrinsicRegistryBox
|
||||||
|
fn match_known_intrinsic_method_call(
|
||||||
|
policy: ImpurePolicy,
|
||||||
|
object: &ASTNode,
|
||||||
|
method: &str,
|
||||||
|
arguments: &[ASTNode],
|
||||||
|
env: &BTreeMap<String, ValueId>,
|
||||||
|
) -> Option<KnownIntrinsic> {
|
||||||
|
match policy {
|
||||||
|
ImpurePolicy::KnownIntrinsicOnly => {
|
||||||
|
let receiver_ok = matches!(object, ASTNode::Variable { name, .. } if env.contains_key(name));
|
||||||
|
if !receiver_ok {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// SSOT: Use registry for lookup (Phase 141 P1.5)
|
||||||
|
KnownIntrinsicRegistryBox::lookup(method, arguments.len())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Phase 141 P1.5: Refactored to use KnownIntrinsicRegistryBox
|
||||||
|
fn lower_known_intrinsic_method_call(
|
||||||
|
intrinsic: KnownIntrinsic,
|
||||||
|
object: &ASTNode,
|
||||||
|
body: &mut Vec<JoinInst>,
|
||||||
|
next_value_id: &mut u32,
|
||||||
|
env: &BTreeMap<String, ValueId>,
|
||||||
|
) -> Result<Option<ValueId>, String> {
|
||||||
|
let receiver = match object {
|
||||||
|
ASTNode::Variable { name, .. } => match env.get(name).copied() {
|
||||||
|
Some(v) => v,
|
||||||
|
None => return Ok(None),
|
||||||
|
},
|
||||||
|
_ => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
// SSOT: Get spec from registry (Phase 141 P1.5)
|
||||||
|
let spec = KnownIntrinsicRegistryBox::get_spec(intrinsic);
|
||||||
|
|
||||||
|
let dst = Self::alloc_value_id(next_value_id);
|
||||||
|
body.push(JoinInst::MethodCall {
|
||||||
|
dst,
|
||||||
|
receiver,
|
||||||
|
method: spec.method_name.to_string(),
|
||||||
|
args: vec![],
|
||||||
|
// Use type hint from registry (currently all intrinsics return Integer)
|
||||||
|
type_hint: Some(MirType::Integer),
|
||||||
|
});
|
||||||
|
Ok(Some(dst))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lower_binary(
|
||||||
|
operator: &BinaryOperator,
|
||||||
|
left: &ASTNode,
|
||||||
|
right: &ASTNode,
|
||||||
|
env: &BTreeMap<String, ValueId>,
|
||||||
|
body: &mut Vec<JoinInst>,
|
||||||
|
next_value_id: &mut u32,
|
||||||
|
) -> Result<Option<ValueId>, String> {
|
||||||
|
let Some(kind) = Self::binary_kind(operator) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
match kind {
|
||||||
|
BinaryKind::Arith(op) => {
|
||||||
|
let lhs = match Self::lower_int_expr(left, env, body, next_value_id)? {
|
||||||
|
Some(v) => v,
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
let rhs = match Self::lower_int_expr(right, env, body, next_value_id)? {
|
||||||
|
Some(v) => v,
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
let dst = Self::alloc_value_id(next_value_id);
|
||||||
|
body.push(JoinInst::Compute(MirLikeInst::BinOp { dst, op, lhs, rhs }));
|
||||||
|
Ok(Some(dst))
|
||||||
|
}
|
||||||
|
BinaryKind::Compare(op) => {
|
||||||
|
let lhs = match Self::lower_int_expr(left, env, body, next_value_id)? {
|
||||||
|
Some(v) => v,
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
let rhs = match Self::lower_int_expr(right, env, body, next_value_id)? {
|
||||||
|
Some(v) => v,
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
let dst = Self::alloc_value_id(next_value_id);
|
||||||
|
body.push(JoinInst::Compute(MirLikeInst::Compare { dst, op, lhs, rhs }));
|
||||||
|
Ok(Some(dst))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lower_int_expr(
|
||||||
|
ast: &ASTNode,
|
||||||
|
env: &BTreeMap<String, ValueId>,
|
||||||
|
body: &mut Vec<JoinInst>,
|
||||||
|
next_value_id: &mut u32,
|
||||||
|
) -> Result<Option<ValueId>, String> {
|
||||||
|
match ast {
|
||||||
|
ASTNode::Variable { name, .. } => Ok(env.get(name).copied()),
|
||||||
|
ASTNode::Literal { value, .. } => match value {
|
||||||
|
LiteralValue::Integer(_) => Self::lower_literal(value, body, next_value_id),
|
||||||
|
_ => Ok(None),
|
||||||
|
},
|
||||||
|
ASTNode::UnaryOp { operator: UnaryOperator::Minus, operand, .. } => {
|
||||||
|
Self::lower_unary(&UnaryOperator::Minus, operand, env, body, next_value_id)
|
||||||
|
}
|
||||||
|
ASTNode::BinaryOp { operator, left, right, .. } => {
|
||||||
|
let Some(BinaryKind::Arith(_)) = Self::binary_kind(operator) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
Self::lower_binary(operator, left, right, env, body, next_value_id)
|
||||||
|
}
|
||||||
|
_ => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lower_bool_expr(
|
||||||
|
ast: &ASTNode,
|
||||||
|
env: &BTreeMap<String, ValueId>,
|
||||||
|
body: &mut Vec<JoinInst>,
|
||||||
|
next_value_id: &mut u32,
|
||||||
|
) -> Result<Option<ValueId>, String> {
|
||||||
|
match ast {
|
||||||
|
ASTNode::Variable { name, .. } => Ok(env.get(name).copied()),
|
||||||
|
ASTNode::Literal { value, .. } => match value {
|
||||||
|
LiteralValue::Bool(_) => Self::lower_literal(value, body, next_value_id),
|
||||||
|
_ => Ok(None),
|
||||||
|
},
|
||||||
|
ASTNode::UnaryOp { operator: UnaryOperator::Not, operand, .. } => {
|
||||||
|
Self::lower_unary(&UnaryOperator::Not, operand, env, body, next_value_id)
|
||||||
|
}
|
||||||
|
_ => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn binary_kind(op: &BinaryOperator) -> Option<BinaryKind> {
|
||||||
|
match op {
|
||||||
|
BinaryOperator::Add => Some(BinaryKind::Arith(BinOpKind::Add)),
|
||||||
|
BinaryOperator::Subtract => Some(BinaryKind::Arith(BinOpKind::Sub)),
|
||||||
|
BinaryOperator::Multiply => Some(BinaryKind::Arith(BinOpKind::Mul)),
|
||||||
|
BinaryOperator::Divide => Some(BinaryKind::Arith(BinOpKind::Div)),
|
||||||
|
BinaryOperator::Equal => Some(BinaryKind::Compare(CompareOp::Eq)),
|
||||||
|
BinaryOperator::NotEqual => Some(BinaryKind::Compare(CompareOp::Ne)),
|
||||||
|
BinaryOperator::Less => Some(BinaryKind::Compare(CompareOp::Lt)),
|
||||||
|
BinaryOperator::LessEqual => Some(BinaryKind::Compare(CompareOp::Le)),
|
||||||
|
BinaryOperator::Greater => Some(BinaryKind::Compare(CompareOp::Gt)),
|
||||||
|
BinaryOperator::GreaterEqual => Some(BinaryKind::Compare(CompareOp::Ge)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum BinaryKind {
|
||||||
|
Arith(BinOpKind),
|
||||||
|
Compare(CompareOp),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::ast::Span;
|
||||||
|
|
||||||
|
fn span() -> Span {
|
||||||
|
Span::unknown()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lower_var_from_env() {
|
||||||
|
let mut env = BTreeMap::new();
|
||||||
|
env.insert("x".to_string(), ValueId(42));
|
||||||
|
let mut body = vec![];
|
||||||
|
let mut next = 100;
|
||||||
|
|
||||||
|
let ast = ASTNode::Variable {
|
||||||
|
name: "x".to_string(),
|
||||||
|
span: span(),
|
||||||
|
};
|
||||||
|
let got = NormalizedExprLowererBox::lower_expr(&ast, &env, &mut body, &mut next).unwrap();
|
||||||
|
assert_eq!(got, Some(ValueId(42)));
|
||||||
|
assert!(body.is_empty());
|
||||||
|
assert_eq!(next, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn out_of_scope_var_missing() {
|
||||||
|
let env = BTreeMap::new();
|
||||||
|
let mut body = vec![];
|
||||||
|
let mut next = 100;
|
||||||
|
|
||||||
|
let ast = ASTNode::Variable {
|
||||||
|
name: "missing".to_string(),
|
||||||
|
span: span(),
|
||||||
|
};
|
||||||
|
let got = NormalizedExprLowererBox::lower_expr(&ast, &env, &mut body, &mut next).unwrap();
|
||||||
|
assert_eq!(got, None);
|
||||||
|
assert!(body.is_empty());
|
||||||
|
assert_eq!(next, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lower_int_literal_const_emits() {
|
||||||
|
let env = BTreeMap::new();
|
||||||
|
let mut body = vec![];
|
||||||
|
let mut next = 100;
|
||||||
|
|
||||||
|
let ast = ASTNode::Literal {
|
||||||
|
value: LiteralValue::Integer(7),
|
||||||
|
span: span(),
|
||||||
|
};
|
||||||
|
let got = NormalizedExprLowererBox::lower_expr(&ast, &env, &mut body, &mut next).unwrap();
|
||||||
|
assert_eq!(got, Some(ValueId(100)));
|
||||||
|
assert_eq!(next, 101);
|
||||||
|
assert!(matches!(
|
||||||
|
body.as_slice(),
|
||||||
|
[JoinInst::Compute(MirLikeInst::Const {
|
||||||
|
dst: ValueId(100),
|
||||||
|
value: ConstValue::Integer(7)
|
||||||
|
})]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lower_bool_literal_const_emits() {
|
||||||
|
let env = BTreeMap::new();
|
||||||
|
let mut body = vec![];
|
||||||
|
let mut next = 100;
|
||||||
|
|
||||||
|
let ast = ASTNode::Literal {
|
||||||
|
value: LiteralValue::Bool(true),
|
||||||
|
span: span(),
|
||||||
|
};
|
||||||
|
let got = NormalizedExprLowererBox::lower_expr(&ast, &env, &mut body, &mut next).unwrap();
|
||||||
|
assert_eq!(got, Some(ValueId(100)));
|
||||||
|
assert_eq!(next, 101);
|
||||||
|
assert!(matches!(
|
||||||
|
body.as_slice(),
|
||||||
|
[JoinInst::Compute(MirLikeInst::Const {
|
||||||
|
dst: ValueId(100),
|
||||||
|
value: ConstValue::Bool(true)
|
||||||
|
})]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lower_unary_minus_int() {
|
||||||
|
let env = BTreeMap::new();
|
||||||
|
let mut body = vec![];
|
||||||
|
let mut next = 10;
|
||||||
|
|
||||||
|
let ast = ASTNode::UnaryOp {
|
||||||
|
operator: UnaryOperator::Minus,
|
||||||
|
operand: Box::new(ASTNode::Literal {
|
||||||
|
value: LiteralValue::Integer(3),
|
||||||
|
span: span(),
|
||||||
|
}),
|
||||||
|
span: span(),
|
||||||
|
};
|
||||||
|
let got = NormalizedExprLowererBox::lower_expr(&ast, &env, &mut body, &mut next).unwrap();
|
||||||
|
assert_eq!(got, Some(ValueId(11)));
|
||||||
|
assert_eq!(next, 12);
|
||||||
|
assert!(matches!(
|
||||||
|
body.as_slice(),
|
||||||
|
[
|
||||||
|
JoinInst::Compute(MirLikeInst::Const { .. }),
|
||||||
|
JoinInst::Compute(MirLikeInst::UnaryOp { dst: ValueId(11), op: UnaryOp::Neg, .. })
|
||||||
|
]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lower_unary_not_bool() {
|
||||||
|
let env = BTreeMap::new();
|
||||||
|
let mut body = vec![];
|
||||||
|
let mut next = 10;
|
||||||
|
|
||||||
|
let ast = ASTNode::UnaryOp {
|
||||||
|
operator: UnaryOperator::Not,
|
||||||
|
operand: Box::new(ASTNode::Literal {
|
||||||
|
value: LiteralValue::Bool(false),
|
||||||
|
span: span(),
|
||||||
|
}),
|
||||||
|
span: span(),
|
||||||
|
};
|
||||||
|
let got = NormalizedExprLowererBox::lower_expr(&ast, &env, &mut body, &mut next).unwrap();
|
||||||
|
assert_eq!(got, Some(ValueId(11)));
|
||||||
|
assert_eq!(next, 12);
|
||||||
|
assert!(matches!(
|
||||||
|
body.as_slice(),
|
||||||
|
[
|
||||||
|
JoinInst::Compute(MirLikeInst::Const { .. }),
|
||||||
|
JoinInst::Compute(MirLikeInst::UnaryOp { dst: ValueId(11), op: UnaryOp::Not, .. })
|
||||||
|
]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lower_add_sub_mul_div_ints() {
|
||||||
|
let mut env = BTreeMap::new();
|
||||||
|
env.insert("x".to_string(), ValueId(1));
|
||||||
|
let mut body = vec![];
|
||||||
|
let mut next = 100;
|
||||||
|
|
||||||
|
let add = 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 got = NormalizedExprLowererBox::lower_expr(&add, &env, &mut body, &mut next).unwrap();
|
||||||
|
assert_eq!(got, Some(ValueId(101)));
|
||||||
|
|
||||||
|
let sub = ASTNode::BinaryOp {
|
||||||
|
operator: BinaryOperator::Subtract,
|
||||||
|
left: Box::new(ASTNode::Literal {
|
||||||
|
value: LiteralValue::Integer(5),
|
||||||
|
span: span(),
|
||||||
|
}),
|
||||||
|
right: Box::new(ASTNode::Literal {
|
||||||
|
value: LiteralValue::Integer(3),
|
||||||
|
span: span(),
|
||||||
|
}),
|
||||||
|
span: span(),
|
||||||
|
};
|
||||||
|
let _ = NormalizedExprLowererBox::lower_expr(&sub, &env, &mut body, &mut next).unwrap();
|
||||||
|
|
||||||
|
let mul = ASTNode::BinaryOp {
|
||||||
|
operator: BinaryOperator::Multiply,
|
||||||
|
left: Box::new(ASTNode::Literal {
|
||||||
|
value: LiteralValue::Integer(6),
|
||||||
|
span: span(),
|
||||||
|
}),
|
||||||
|
right: Box::new(ASTNode::Literal {
|
||||||
|
value: LiteralValue::Integer(7),
|
||||||
|
span: span(),
|
||||||
|
}),
|
||||||
|
span: span(),
|
||||||
|
};
|
||||||
|
let _ = NormalizedExprLowererBox::lower_expr(&mul, &env, &mut body, &mut next).unwrap();
|
||||||
|
|
||||||
|
let div = ASTNode::BinaryOp {
|
||||||
|
operator: BinaryOperator::Divide,
|
||||||
|
left: Box::new(ASTNode::Literal {
|
||||||
|
value: LiteralValue::Integer(8),
|
||||||
|
span: span(),
|
||||||
|
}),
|
||||||
|
right: Box::new(ASTNode::Literal {
|
||||||
|
value: LiteralValue::Integer(2),
|
||||||
|
span: span(),
|
||||||
|
}),
|
||||||
|
span: span(),
|
||||||
|
};
|
||||||
|
let _ = NormalizedExprLowererBox::lower_expr(&div, &env, &mut body, &mut next).unwrap();
|
||||||
|
|
||||||
|
assert!(body.iter().any(|i| matches!(i, JoinInst::Compute(MirLikeInst::BinOp { op: BinOpKind::Add, .. }))));
|
||||||
|
assert!(body.iter().any(|i| matches!(i, JoinInst::Compute(MirLikeInst::BinOp { op: BinOpKind::Sub, .. }))));
|
||||||
|
assert!(body.iter().any(|i| matches!(i, JoinInst::Compute(MirLikeInst::BinOp { op: BinOpKind::Mul, .. }))));
|
||||||
|
assert!(body.iter().any(|i| matches!(i, JoinInst::Compute(MirLikeInst::BinOp { op: BinOpKind::Div, .. }))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lower_compare_eq_lt_ints() {
|
||||||
|
let mut env = BTreeMap::new();
|
||||||
|
env.insert("x".to_string(), ValueId(1));
|
||||||
|
let mut body = vec![];
|
||||||
|
let mut next = 100;
|
||||||
|
|
||||||
|
let eq = ASTNode::BinaryOp {
|
||||||
|
operator: BinaryOperator::Equal,
|
||||||
|
left: Box::new(ASTNode::Variable {
|
||||||
|
name: "x".to_string(),
|
||||||
|
span: span(),
|
||||||
|
}),
|
||||||
|
right: Box::new(ASTNode::Literal {
|
||||||
|
value: LiteralValue::Integer(1),
|
||||||
|
span: span(),
|
||||||
|
}),
|
||||||
|
span: span(),
|
||||||
|
};
|
||||||
|
let got = NormalizedExprLowererBox::lower_expr(&eq, &env, &mut body, &mut next).unwrap();
|
||||||
|
assert!(got.is_some());
|
||||||
|
|
||||||
|
let lt = ASTNode::BinaryOp {
|
||||||
|
operator: BinaryOperator::Less,
|
||||||
|
left: Box::new(ASTNode::Literal {
|
||||||
|
value: LiteralValue::Integer(0),
|
||||||
|
span: span(),
|
||||||
|
}),
|
||||||
|
right: Box::new(ASTNode::Literal {
|
||||||
|
value: LiteralValue::Integer(1),
|
||||||
|
span: span(),
|
||||||
|
}),
|
||||||
|
span: span(),
|
||||||
|
};
|
||||||
|
let got = NormalizedExprLowererBox::lower_expr(<, &env, &mut body, &mut next).unwrap();
|
||||||
|
assert!(got.is_some());
|
||||||
|
|
||||||
|
assert!(body.iter().any(|i| matches!(i, JoinInst::Compute(MirLikeInst::Compare { op: CompareOp::Eq, .. }))));
|
||||||
|
assert!(body.iter().any(|i| matches!(i, JoinInst::Compute(MirLikeInst::Compare { op: CompareOp::Lt, .. }))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn out_of_scope_call() {
|
||||||
|
let env = BTreeMap::new();
|
||||||
|
let mut body = vec![];
|
||||||
|
let mut next = 1;
|
||||||
|
|
||||||
|
let ast = ASTNode::FunctionCall {
|
||||||
|
name: "f".to_string(),
|
||||||
|
arguments: vec![],
|
||||||
|
span: span(),
|
||||||
|
};
|
||||||
|
let got = NormalizedExprLowererBox::lower_expr(&ast, &env, &mut body, &mut next).unwrap();
|
||||||
|
assert_eq!(got, None);
|
||||||
|
assert!(body.is_empty());
|
||||||
|
assert_eq!(next, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn call_is_out_of_scope_in_pure_only() {
|
||||||
|
let env = BTreeMap::new();
|
||||||
|
let ast = ASTNode::Call {
|
||||||
|
callee: Box::new(ASTNode::Variable {
|
||||||
|
name: "f".to_string(),
|
||||||
|
span: span(),
|
||||||
|
}),
|
||||||
|
arguments: vec![],
|
||||||
|
span: span(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
NormalizedExprLowererBox::out_of_scope_reason(ExprLoweringScope::PureOnly, &ast, &env),
|
||||||
|
Some(OutOfScopeReason::Call)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn methodcall_is_out_of_scope_in_pure_only() {
|
||||||
|
let env = BTreeMap::new();
|
||||||
|
let ast = ASTNode::MethodCall {
|
||||||
|
object: Box::new(ASTNode::Variable {
|
||||||
|
name: "x".to_string(),
|
||||||
|
span: span(),
|
||||||
|
}),
|
||||||
|
method: "m".to_string(),
|
||||||
|
arguments: vec![],
|
||||||
|
span: span(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
NormalizedExprLowererBox::out_of_scope_reason(ExprLoweringScope::PureOnly, &ast, &env),
|
||||||
|
Some(OutOfScopeReason::MethodCall)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn methodcall_length0_is_in_scope_with_known_intrinsic_only() {
|
||||||
|
let mut env = BTreeMap::new();
|
||||||
|
env.insert("s".to_string(), ValueId(1));
|
||||||
|
|
||||||
|
let ast = ASTNode::MethodCall {
|
||||||
|
object: Box::new(ASTNode::Variable {
|
||||||
|
name: "s".to_string(),
|
||||||
|
span: span(),
|
||||||
|
}),
|
||||||
|
method: "length".to_string(),
|
||||||
|
arguments: vec![],
|
||||||
|
span: span(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
NormalizedExprLowererBox::out_of_scope_reason(
|
||||||
|
ExprLoweringScope::WithImpure(ImpurePolicy::KnownIntrinsicOnly),
|
||||||
|
&ast,
|
||||||
|
&env
|
||||||
|
),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lower_methodcall_length0_emits_method_call_inst() {
|
||||||
|
let mut env = BTreeMap::new();
|
||||||
|
env.insert("s".to_string(), ValueId(7));
|
||||||
|
let mut body = vec![];
|
||||||
|
let mut next = 100;
|
||||||
|
|
||||||
|
let ast = ASTNode::MethodCall {
|
||||||
|
object: Box::new(ASTNode::Variable {
|
||||||
|
name: "s".to_string(),
|
||||||
|
span: span(),
|
||||||
|
}),
|
||||||
|
method: "length".to_string(),
|
||||||
|
arguments: vec![],
|
||||||
|
span: span(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let got = NormalizedExprLowererBox::lower_expr_with_scope(
|
||||||
|
ExprLoweringScope::WithImpure(ImpurePolicy::KnownIntrinsicOnly),
|
||||||
|
&ast,
|
||||||
|
&env,
|
||||||
|
&mut body,
|
||||||
|
&mut next,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(got, Some(ValueId(100)));
|
||||||
|
assert_eq!(next, 101);
|
||||||
|
assert!(matches!(
|
||||||
|
body.as_slice(),
|
||||||
|
[JoinInst::MethodCall {
|
||||||
|
dst: ValueId(100),
|
||||||
|
receiver: ValueId(7),
|
||||||
|
method,
|
||||||
|
args,
|
||||||
|
type_hint: Some(MirType::Integer),
|
||||||
|
}] if method == "length" && args.is_empty()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
//! ExprLoweringContract: Pure/Impure boundary SSOT (Phase 141 P0)
|
||||||
|
//!
|
||||||
|
//! ## Responsibility
|
||||||
|
//!
|
||||||
|
//! Define the contract surface for `NormalizedExprLowererBox` so that Phase 141+
|
||||||
|
//! can introduce Call/MethodCall lowering without changing existing behavior.
|
||||||
|
//!
|
||||||
|
//! ## Key Policy (Phase 141 P0)
|
||||||
|
//!
|
||||||
|
//! - Current lowering is **PureOnly**.
|
||||||
|
//! - Call/MethodCall and other impure constructs remain **out-of-scope** and must
|
||||||
|
//! return `Ok(None)` (fallback), preserving default behavior.
|
||||||
|
//!
|
||||||
|
//! ## Phase 141 P1 (incremental)
|
||||||
|
//!
|
||||||
|
//! - `WithImpure(KnownIntrinsicOnly)` allows a *small, explicit allowlist* of
|
||||||
|
//! "known intrinsic" method calls (still pure, fixed arity, stable type_hint).
|
||||||
|
//! - Everything else stays out-of-scope and must return `Ok(None)` (fallback).
|
||||||
|
|
||||||
|
/// Scope selector for expression lowering.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ExprLoweringScope {
|
||||||
|
/// Pure expressions only (Phase 140 baseline).
|
||||||
|
PureOnly,
|
||||||
|
/// Placeholder for future impure support (Phase 141+).
|
||||||
|
WithImpure(ImpurePolicy),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Phase 141+ policies (effects/ordering/typing).
|
||||||
|
///
|
||||||
|
/// Phase 141 P1 uses `KnownIntrinsicOnly` as a safe on-ramp.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ImpurePolicy {
|
||||||
|
/// Allow a small explicit allowlist of intrinsic calls only.
|
||||||
|
///
|
||||||
|
/// Still must be semantics-preserving: if the intrinsic is not applicable at runtime,
|
||||||
|
/// it should behave the same as the legacy lowering path.
|
||||||
|
KnownIntrinsicOnly,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ImpurePolicy {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::KnownIntrinsicOnly
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Phase 141 P1: "known intrinsic" method calls that are allowed under `KnownIntrinsicOnly`.
|
||||||
|
///
|
||||||
|
/// ## Phase 141 P1.5: Metadata moved to KnownIntrinsicRegistryBox
|
||||||
|
///
|
||||||
|
/// - method_name() and arity() methods **REMOVED** (deprecated)
|
||||||
|
/// - Use `KnownIntrinsicRegistryBox::lookup()` and `get_spec()` instead
|
||||||
|
/// - This enum is now a simple marker; metadata lives in known_intrinsics.rs
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum KnownIntrinsic {
|
||||||
|
/// `receiver.length()` with 0 args, expected to return Integer.
|
||||||
|
///
|
||||||
|
/// Notes:
|
||||||
|
/// - Receiver type is *not* resolved here; this is a structural allowlist.
|
||||||
|
/// - Return type hint is used to prevent downstream type inference noise.
|
||||||
|
Length0,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Minimal classification for "why did we return Ok(None)?" (Phase 141 P0).
|
||||||
|
///
|
||||||
|
/// ## Phase 141 P1.5: Added IntrinsicNotWhitelisted
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum OutOfScopeReason {
|
||||||
|
Call,
|
||||||
|
MethodCall,
|
||||||
|
/// Phase 141 P1.5: Known intrinsic but not in current allowlist
|
||||||
|
/// (Better diagnostic than generic MethodCall)
|
||||||
|
IntrinsicNotWhitelisted,
|
||||||
|
ImpureExpression,
|
||||||
|
UnsupportedLiteral,
|
||||||
|
UnsupportedOperator,
|
||||||
|
MissingEnvVar,
|
||||||
|
}
|
||||||
@ -0,0 +1,159 @@
|
|||||||
|
//! Phase 141 P1.5: Known Intrinsic Registry (SSOT)
|
||||||
|
//!
|
||||||
|
//! ## Responsibility
|
||||||
|
//!
|
||||||
|
//! - Centralize known intrinsic metadata (method_name, arity, return_type_hint)
|
||||||
|
//! - Provide lookup API: method name + arity → KnownIntrinsic
|
||||||
|
//! - Single responsibility: eliminate string literal scattered matching
|
||||||
|
//!
|
||||||
|
//! ## Design
|
||||||
|
//!
|
||||||
|
//! - `KnownIntrinsicSpec`: Metadata struct for each intrinsic
|
||||||
|
//! - `KnownIntrinsicRegistryBox`: Lookup SSOT (all_specs() static array)
|
||||||
|
//! - Adding new intrinsics: (1) enum variant, (2) registry entry only
|
||||||
|
//!
|
||||||
|
//! ## Non-Goals
|
||||||
|
//!
|
||||||
|
//! - Effects/typing system (Phase 141 P2+ with IntrinsicEffects trait)
|
||||||
|
//! - Runtime intrinsic dispatch (this is compile-time metadata only)
|
||||||
|
|
||||||
|
use super::expr_lowering_contract::KnownIntrinsic;
|
||||||
|
|
||||||
|
/// Phase 141 P1.5: Known intrinsic specification
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct KnownIntrinsicSpec {
|
||||||
|
pub intrinsic: KnownIntrinsic,
|
||||||
|
pub method_name: &'static str,
|
||||||
|
pub arity: usize,
|
||||||
|
pub return_type_hint: Option<&'static str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Phase 141 P1.5: Known intrinsic registry SSOT
|
||||||
|
pub struct KnownIntrinsicRegistryBox;
|
||||||
|
|
||||||
|
impl KnownIntrinsicRegistryBox {
|
||||||
|
/// Lookup intrinsic by method name and arity
|
||||||
|
///
|
||||||
|
/// ## Contract
|
||||||
|
///
|
||||||
|
/// - Input: method name + arity
|
||||||
|
/// - Output: Some(KnownIntrinsic) if matched, None otherwise
|
||||||
|
/// - Use case: Pattern matching in expr_lowerer_box.rs
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// // receiver.length() with 0 args
|
||||||
|
/// let intrinsic = KnownIntrinsicRegistryBox::lookup("length", 0);
|
||||||
|
/// assert_eq!(intrinsic, Some(KnownIntrinsic::Length0));
|
||||||
|
/// ```
|
||||||
|
pub fn lookup(method: &str, arity: usize) -> Option<KnownIntrinsic> {
|
||||||
|
Self::all_specs()
|
||||||
|
.iter()
|
||||||
|
.find(|spec| spec.method_name == method && spec.arity == arity)
|
||||||
|
.map(|spec| spec.intrinsic)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get spec for a known intrinsic
|
||||||
|
///
|
||||||
|
/// ## Contract
|
||||||
|
///
|
||||||
|
/// - Input: KnownIntrinsic enum variant
|
||||||
|
/// - Output: KnownIntrinsicSpec
|
||||||
|
/// - Panics if intrinsic not registered (design invariant)
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let spec = KnownIntrinsicRegistryBox::get_spec(KnownIntrinsic::Length0);
|
||||||
|
/// assert_eq!(spec.method_name, "length");
|
||||||
|
/// assert_eq!(spec.arity, 0);
|
||||||
|
/// ```
|
||||||
|
pub fn get_spec(intrinsic: KnownIntrinsic) -> KnownIntrinsicSpec {
|
||||||
|
Self::all_specs()
|
||||||
|
.iter()
|
||||||
|
.find(|spec| spec.intrinsic == intrinsic)
|
||||||
|
.expect("KnownIntrinsic not registered in all_specs() - design invariant violated")
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All registered intrinsics (SSOT)
|
||||||
|
///
|
||||||
|
/// ## Extending
|
||||||
|
///
|
||||||
|
/// To add a new intrinsic:
|
||||||
|
/// 1. Add enum variant to KnownIntrinsic (in expr_lowering_contract.rs)
|
||||||
|
/// 2. Add entry to this array
|
||||||
|
///
|
||||||
|
/// No other files need editing!
|
||||||
|
fn all_specs() -> &'static [KnownIntrinsicSpec] {
|
||||||
|
static SPECS: &[KnownIntrinsicSpec] = &[
|
||||||
|
// Phase 141 P1: receiver.length() with 0 args
|
||||||
|
KnownIntrinsicSpec {
|
||||||
|
intrinsic: KnownIntrinsic::Length0,
|
||||||
|
method_name: "length",
|
||||||
|
arity: 0,
|
||||||
|
return_type_hint: Some("integer"),
|
||||||
|
},
|
||||||
|
// Future intrinsics here...
|
||||||
|
// Example: receiver.toUpperCase() with 0 args
|
||||||
|
// KnownIntrinsicSpec {
|
||||||
|
// intrinsic: KnownIntrinsic::ToUpperCase0,
|
||||||
|
// method_name: "toUpperCase",
|
||||||
|
// arity: 0,
|
||||||
|
// return_type_hint: Some("string".to_string()),
|
||||||
|
// },
|
||||||
|
];
|
||||||
|
SPECS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_lookup_length0_success() {
|
||||||
|
let result = KnownIntrinsicRegistryBox::lookup("length", 0);
|
||||||
|
assert_eq!(result, Some(KnownIntrinsic::Length0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_lookup_wrong_arity_returns_none() {
|
||||||
|
// length() with 1 arg should not match Length0 (arity 0)
|
||||||
|
let result = KnownIntrinsicRegistryBox::lookup("length", 1);
|
||||||
|
assert_eq!(result, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_lookup_unknown_method_returns_none() {
|
||||||
|
let result = KnownIntrinsicRegistryBox::lookup("unknown_method", 0);
|
||||||
|
assert_eq!(result, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_spec_length0() {
|
||||||
|
let spec = KnownIntrinsicRegistryBox::get_spec(KnownIntrinsic::Length0);
|
||||||
|
assert_eq!(spec.method_name, "length");
|
||||||
|
assert_eq!(spec.arity, 0);
|
||||||
|
assert_eq!(
|
||||||
|
spec.return_type_hint,
|
||||||
|
Some("integer")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_lookup_deterministic() {
|
||||||
|
// Ensure lookup is deterministic (same input = same output)
|
||||||
|
for _ in 0..10 {
|
||||||
|
let result = KnownIntrinsicRegistryBox::lookup("length", 0);
|
||||||
|
assert_eq!(result, Some(KnownIntrinsic::Length0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_all_specs_non_empty() {
|
||||||
|
let specs = KnownIntrinsicRegistryBox::all_specs();
|
||||||
|
assert!(!specs.is_empty(), "all_specs() should have at least one entry");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,6 @@
|
|||||||
//! Common utilities for Normalized shadow (Phase 138+)
|
//! Common utilities for Normalized shadow (Phase 138+)
|
||||||
|
|
||||||
pub mod return_value_lowerer_box;
|
pub mod return_value_lowerer_box;
|
||||||
|
pub mod expr_lowerer_box;
|
||||||
|
pub mod expr_lowering_contract;
|
||||||
|
pub mod known_intrinsics; // Phase 141 P1.5
|
||||||
|
|||||||
@ -39,7 +39,8 @@ impl StepTreeDevPipelineBox {
|
|||||||
|
|
||||||
// Phase 126: Collect available_inputs from SSOT sources
|
// Phase 126: Collect available_inputs from SSOT sources
|
||||||
// Note: CapturedEnv is None for now (if-only patterns don't use CapturedEnv yet)
|
// Note: CapturedEnv is None for now (if-only patterns don't use CapturedEnv yet)
|
||||||
let available_inputs = AvailableInputsCollectorBox::collect(builder, None);
|
// Phase 141 P1.5: No prefix_variables in dev_pipeline context (function-level only)
|
||||||
|
let available_inputs = AvailableInputsCollectorBox::collect(builder, None, None);
|
||||||
|
|
||||||
// Try shadow lowering (if-only scope)
|
// Try shadow lowering (if-only scope)
|
||||||
let shadow_result =
|
let shadow_result =
|
||||||
|
|||||||
Reference in New Issue
Block a user