diff --git a/docs/development/current/main/design/normalized-expr-lowering.md b/docs/development/current/main/design/normalized-expr-lowering.md new file mode 100644 index 00000000..30afcaa4 --- /dev/null +++ b/docs/development/current/main/design/normalized-expr-lowering.md @@ -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`)+ 出力先 `Vec` + `next_value_id` +- 出力: `Result, 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>, // NEW: Phase 141 P1.5 +) -> BTreeMap +``` + +### 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 理由を精密化する。 diff --git a/src/mir/builder/control_flow/joinir/patterns/policies/normalized_shadow_suffix_router_box.rs b/src/mir/builder/control_flow/joinir/patterns/policies/normalized_shadow_suffix_router_box.rs index 29d99a73..c86ee1c9 100644 --- a/src/mir/builder/control_flow/joinir/patterns/policies/normalized_shadow_suffix_router_box.rs +++ b/src/mir/builder/control_flow/joinir/patterns/policies/normalized_shadow_suffix_router_box.rs @@ -30,6 +30,7 @@ impl NormalizedShadowSuffixRouterBox { /// Try to lower a block suffix starting with loop(true) + post statements /// /// Phase 134 P0: Unified with NormalizationPlanBox for pattern detection + /// Phase 141 P1.5: Added prefix_variables parameter for external env inputs /// /// Returns: /// - Ok(Some(consumed)): Successfully processed remaining[..consumed] @@ -40,6 +41,7 @@ impl NormalizedShadowSuffixRouterBox { remaining: &[ASTNode], func_name: &str, debug: bool, + prefix_variables: Option<&std::collections::BTreeMap>, ) -> Result, String> { let trace = crate::mir::builder::control_flow::joinir::trace::trace(); @@ -86,7 +88,8 @@ impl NormalizedShadowSuffixRouterBox { } // 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) => { // ExecuteBox returns a void constant, we don't need it for suffix routing // The consumed count is what build_block() needs diff --git a/src/mir/builder/control_flow/joinir/routing.rs b/src/mir/builder/control_flow/joinir/routing.rs index eaac41df..e3fb8bd5 100644 --- a/src/mir/builder/control_flow/joinir/routing.rs +++ b/src/mir/builder/control_flow/joinir/routing.rs @@ -469,7 +469,10 @@ impl MirBuilder { } // 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) => { if debug { trace::trace().routing( diff --git a/src/mir/builder/control_flow/normalization/execute_box.rs b/src/mir/builder/control_flow/normalization/execute_box.rs index 99100546..9afbf324 100644 --- a/src/mir/builder/control_flow/normalization/execute_box.rs +++ b/src/mir/builder/control_flow/normalization/execute_box.rs @@ -16,6 +16,7 @@ use crate::ast::ASTNode; use crate::mir::builder::MirBuilder; use crate::mir::ValueId; use super::plan::{NormalizationPlan, PlanKind}; +use std::collections::BTreeMap; /// Box-First: Execute normalization plan pub struct NormalizationExecuteBox; @@ -23,6 +24,8 @@ pub struct NormalizationExecuteBox; impl NormalizationExecuteBox { /// Execute a normalization plan /// + /// ## Phase 141 P1.5: Added prefix_variables parameter + /// /// Returns: /// - Ok(value_id): Successfully executed, returns result value /// - Err(_): Lowering or merge failed @@ -32,6 +35,7 @@ impl NormalizationExecuteBox { remaining: &[ASTNode], func_name: &str, debug: bool, + prefix_variables: Option<&BTreeMap>, ) -> Result { let trace = crate::mir::builder::control_flow::joinir::trace::trace(); @@ -57,20 +61,23 @@ impl NormalizationExecuteBox { match &plan.kind { PlanKind::LoopOnly => { - Self::execute_loop_only(builder, remaining, func_name, debug) + Self::execute_loop_only(builder, remaining, func_name, debug, prefix_variables) } 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 + /// + /// ## Phase 141 P1.5: Added prefix_variables parameter fn execute_loop_only( builder: &mut MirBuilder, remaining: &[ASTNode], func_name: &str, debug: bool, + prefix_variables: Option<&BTreeMap>, ) -> Result { use crate::ast::Span; 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); - // Collect available inputs - let available_inputs = AvailableInputsCollectorBox::collect(builder, None); + // Collect available inputs (Phase 141 P1.5: with prefix variables) + let available_inputs = AvailableInputsCollectorBox::collect(builder, None, prefix_variables); if debug { trace.routing( @@ -141,12 +148,15 @@ impl NormalizationExecuteBox { } /// Execute Phase 132-133: Loop + post assignments + return + /// + /// ## Phase 141 P1.5: Added prefix_variables parameter fn execute_loop_with_post( builder: &mut MirBuilder, plan: &NormalizationPlan, remaining: &[ASTNode], func_name: &str, debug: bool, + prefix_variables: Option<&BTreeMap>, ) -> Result { use crate::mir::control_tree::normalized_shadow::available_inputs_collector::AvailableInputsCollectorBox; use crate::mir::control_tree::normalized_shadow::StepTreeNormalizedShadowLowererBox; @@ -158,8 +168,8 @@ impl NormalizationExecuteBox { let suffix = &remaining[..plan.consumed]; let step_tree = StepTreeBuilderBox::build_from_block(suffix); - // Collect available inputs - let available_inputs = AvailableInputsCollectorBox::collect(builder, None); + // Collect available inputs (Phase 141 P1.5: with prefix variables) + let available_inputs = AvailableInputsCollectorBox::collect(builder, None, prefix_variables); if debug { trace.routing( diff --git a/src/mir/builder/stmts.rs b/src/mir/builder/stmts.rs index 082927d6..410d5c79 100644 --- a/src/mir/builder/stmts.rs +++ b/src/mir/builder/stmts.rs @@ -198,8 +198,11 @@ impl super::MirBuilder { let debug = trace.is_enabled(); 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( - self, remaining, &func_name, debug + self, remaining, &func_name, debug, Some(&prefix_var_map) )? { Some(consumed) => { trace.emit_if( diff --git a/src/mir/control_tree/normalized_shadow/available_inputs_collector.rs b/src/mir/control_tree/normalized_shadow/available_inputs_collector.rs index c1328a77..3a9afd11 100644 --- a/src/mir/control_tree/normalized_shadow/available_inputs_collector.rs +++ b/src/mir/control_tree/normalized_shadow/available_inputs_collector.rs @@ -1,16 +1,17 @@ -//! Phase 126: available_inputs collector (SSOT) +//! Phase 126 + Phase 141 P1.5: available_inputs collector (SSOT) //! //! ## Responsibility //! -//! - Collect available_inputs from function params + CapturedEnv +//! - Collect available_inputs from function params + prefix variables + CapturedEnv //! - Returns BTreeMap with deterministic order //! - No AST inference (SSOT sources only) //! //! ## Design //! //! - Input sources (priority order): -//! 1. Function params (from ScopeContext + VariableContext) -//! 2. CapturedEnv (pinned/captured variables from outer scope) +//! 1. Function params (from ScopeContext + VariableContext) - highest priority +//! 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) // Phase 126: Import contexts from MirBuilder (pub(in crate::mir) visibility) @@ -26,36 +27,49 @@ pub struct AvailableInputsCollectorBox; impl AvailableInputsCollectorBox { /// Collect available_inputs from SSOT sources (via MirBuilder) /// - /// ## Contract + /// ## Contract (Phase 141 P1.5) /// /// - Sources (priority order): /// 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 (deterministic order) /// - No AST inference: only use pre-computed SSOT sources /// - /// ## Implementation + /// ## Implementation (Phase 141 P1.5) /// - /// - Collect function params first (higher priority) - /// - Collect CapturedEnv vars (lower priority, don't override params) + /// - Collect function params first (highest priority) + /// - 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 pub fn collect( builder: &MirBuilder, captured_env: Option<&CapturedEnv>, + prefix_variables: Option<&BTreeMap>, ) -> BTreeMap { 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 { if let Some(value_id) = builder.variable_ctx.lookup(param_name) { 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 { 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) { available_inputs.insert(var.name.clone(), var.host_id); } @@ -74,7 +88,7 @@ mod tests { #[test] fn test_collect_empty() { let builder = MirBuilder::new(); - let result = AvailableInputsCollectorBox::collect(&builder, None); + let result = AvailableInputsCollectorBox::collect(&builder, None, None); assert_eq!(result.len(), 0); } @@ -86,7 +100,7 @@ mod tests { builder.scope_ctx.function_param_names.insert("x".to_string()); 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.get("x"), Some(&ValueId(1))); } @@ -98,7 +112,7 @@ mod tests { let mut captured = CapturedEnv::new(); 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.get("outer_x"), Some(&ValueId(42))); } @@ -115,7 +129,7 @@ mod tests { let mut captured = CapturedEnv::new(); 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); // Function param (ValueId(1)) should win over captured (ValueId(42)) 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("m".to_string(), ValueId(2)); - let result = AvailableInputsCollectorBox::collect(&builder, None); + let result = AvailableInputsCollectorBox::collect(&builder, None, None); let keys: Vec<_> = result.keys().collect(); // BTreeMap ensures alphabetical order 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))); + } } diff --git a/src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs b/src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs new file mode 100644 index 00000000..9f58aa4e --- /dev/null +++ b/src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs @@ -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, + body: &mut Vec, + next_value_id: &mut u32, + ) -> Result, 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, + body: &mut Vec, + next_value_id: &mut u32, + ) -> Result, 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, + ) -> Option { + 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) -> Option { + 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, + ) -> Option { + 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) -> 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) -> 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) -> 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) -> 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, + next_value_id: &mut u32, + ) -> Result, 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, + body: &mut Vec, + next_value_id: &mut u32, + ) -> Result, 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, + ) -> Option { + 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, + next_value_id: &mut u32, + env: &BTreeMap, + ) -> Result, 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, + body: &mut Vec, + next_value_id: &mut u32, + ) -> Result, 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, + body: &mut Vec, + next_value_id: &mut u32, + ) -> Result, 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, + body: &mut Vec, + next_value_id: &mut u32, + ) -> Result, 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 { + 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() + )); + } +} diff --git a/src/mir/control_tree/normalized_shadow/common/expr_lowering_contract.rs b/src/mir/control_tree/normalized_shadow/common/expr_lowering_contract.rs new file mode 100644 index 00000000..e4242572 --- /dev/null +++ b/src/mir/control_tree/normalized_shadow/common/expr_lowering_contract.rs @@ -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, +} diff --git a/src/mir/control_tree/normalized_shadow/common/known_intrinsics.rs b/src/mir/control_tree/normalized_shadow/common/known_intrinsics.rs new file mode 100644 index 00000000..1ea54d58 --- /dev/null +++ b/src/mir/control_tree/normalized_shadow/common/known_intrinsics.rs @@ -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 { + 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"); + } +} diff --git a/src/mir/control_tree/normalized_shadow/common/mod.rs b/src/mir/control_tree/normalized_shadow/common/mod.rs index 1b736fd6..ce4d5259 100644 --- a/src/mir/control_tree/normalized_shadow/common/mod.rs +++ b/src/mir/control_tree/normalized_shadow/common/mod.rs @@ -1,3 +1,6 @@ //! Common utilities for Normalized shadow (Phase 138+) pub mod return_value_lowerer_box; +pub mod expr_lowerer_box; +pub mod expr_lowering_contract; +pub mod known_intrinsics; // Phase 141 P1.5 diff --git a/src/mir/control_tree/normalized_shadow/dev_pipeline.rs b/src/mir/control_tree/normalized_shadow/dev_pipeline.rs index 0c5de6dc..46e2970f 100644 --- a/src/mir/control_tree/normalized_shadow/dev_pipeline.rs +++ b/src/mir/control_tree/normalized_shadow/dev_pipeline.rs @@ -39,7 +39,8 @@ impl StepTreeDevPipelineBox { // Phase 126: Collect available_inputs from SSOT sources // 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) let shadow_result =