Refactor JoinIR lowerers and boundary

This commit is contained in:
2025-12-28 03:52:52 +09:00
parent 3e2086cc78
commit ca91be349d
26 changed files with 2228 additions and 2375 deletions

View File

@ -1,922 +0,0 @@
//! 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> {
// Phase 146 P1: ANF routing (dev-only, scope-aware)
if crate::config::env::anf_dev_enabled() {
// P1: Allow ANF for PureOnly if HAKO_ANF_ALLOW_PURE=1
let should_try_anf = match scope {
ExprLoweringScope::WithImpure(_) => true,
ExprLoweringScope::PureOnly => crate::config::env::anf_allow_pure_enabled(),
};
if should_try_anf {
use super::super::anf::{AnfPlanBox, AnfExecuteBox};
match AnfPlanBox::plan_expr(ast, env) {
Ok(Some(plan)) => {
match AnfExecuteBox::try_execute(&plan, ast, &mut env.clone(), body, next_value_id)? {
Some(vid) => return Ok(Some(vid)), // P1+: ANF succeeded
None => {
// P0: stub returns None, fallback to legacy
if crate::config::env::joinir_dev_enabled() {
eprintln!("[phase145/debug] ANF plan found but execute returned None (P0 stub)");
}
}
}
}
Ok(None) => {
// Out-of-scope for ANF, continue with legacy lowering
}
Err(_reason) => {
// Explicitly out-of-scope (ContainsCall/ContainsMethodCall), continue
}
}
}
}
if Self::out_of_scope_reason(scope, ast, env).is_some() {
return Ok(None);
}
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),
UnaryOperator::Weak => Some(OutOfScopeReason::UnsupportedOperator), // Phase 285W-Syntax-0
},
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),
UnaryOperator::Weak => Some(OutOfScopeReason::UnsupportedOperator), // Phase 285W-Syntax-0
},
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),
UnaryOperator::Weak => Ok(None), // Phase 285W-Syntax-0: Not supported in normalized lowering
}
}
/// 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(&lt, &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()
));
}
}

View File

@ -0,0 +1,23 @@
use crate::ast::BinaryOperator;
use crate::mir::join_ir::{BinOpKind, CompareOp};
pub(super) enum BinaryKind {
Arith(BinOpKind),
Compare(CompareOp),
}
pub(super) 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,
}
}

View File

@ -0,0 +1,59 @@
use super::NormalizedExprLowererBox;
use super::super::expr_lowering_contract::{ImpurePolicy, KnownIntrinsic};
use super::super::known_intrinsics::KnownIntrinsicRegistryBox;
use crate::ast::ASTNode;
use crate::mir::join_ir::JoinInst;
use crate::mir::types::MirType;
use crate::mir::ValueId;
use std::collections::BTreeMap;
impl NormalizedExprLowererBox {
/// Phase 141 P1.5: Refactored to use KnownIntrinsicRegistryBox
pub(super) 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;
}
KnownIntrinsicRegistryBox::lookup(method, arguments.len())
}
}
}
/// Phase 141 P1.5: Refactored to use KnownIntrinsicRegistryBox
pub(super) 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),
};
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![],
type_hint: Some(MirType::Integer),
});
Ok(Some(dst))
}
}

View File

@ -0,0 +1,173 @@
use super::binary::{binary_kind, BinaryKind};
use super::NormalizedExprLowererBox;
use crate::ast::{ASTNode, BinaryOperator, LiteralValue, UnaryOperator};
use crate::mir::join_ir::{ConstValue, JoinInst, MirLikeInst, UnaryOp};
use crate::mir::ValueId;
use std::collections::BTreeMap;
impl NormalizedExprLowererBox {
pub(super) fn alloc_value_id(next_value_id: &mut u32) -> ValueId {
let vid = ValueId(*next_value_id);
*next_value_id += 1;
vid
}
pub(super) 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),
}
}
pub(super) 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),
UnaryOperator::Weak => Ok(None), // Phase 285W-Syntax-0: Not supported in normalized lowering
}
}
pub(super) 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) = 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))
}
}
}
pub(super) 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(_)) = binary_kind(operator) else {
return Ok(None);
};
Self::lower_binary(operator, left, right, env, body, next_value_id)
}
_ => Ok(None),
}
}
pub(super) 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),
}
}
}

View File

@ -0,0 +1,137 @@
//! 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)` (caller routes to non-normalized lowering).
//! - `Err(_)` is reserved for internal invariants only (should be rare).
mod binary;
mod intrinsics;
mod lowering;
mod scope;
#[cfg(test)]
mod tests;
use super::expr_lowering_contract::ExprLoweringScope;
use crate::ast::ASTNode;
use crate::mir::join_ir::JoinInst;
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> {
// Phase 146 P1: ANF routing (dev-only, scope-aware)
if crate::config::env::anf_dev_enabled() {
// P1: Allow ANF for PureOnly if HAKO_ANF_ALLOW_PURE=1
let should_try_anf = match scope {
ExprLoweringScope::WithImpure(_) => true,
ExprLoweringScope::PureOnly => crate::config::env::anf_allow_pure_enabled(),
};
if should_try_anf {
use super::super::anf::{AnfExecuteBox, AnfPlanBox};
match AnfPlanBox::plan_expr(ast, env) {
Ok(Some(plan)) => {
match AnfExecuteBox::try_execute(
&plan,
ast,
&mut env.clone(),
body,
next_value_id,
)? {
Some(vid) => return Ok(Some(vid)),
None => {
if crate::config::env::joinir_dev_enabled() {
eprintln!(
"[phase145/debug] ANF plan found but execute returned None (P0 stub)"
);
}
}
}
}
Ok(None) => {
// Out-of-scope for ANF, continue with normalized lowering
}
Err(_reason) => {
// Explicitly out-of-scope (ContainsCall/ContainsMethodCall), continue
}
}
}
}
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),
}
}
}

View File

@ -0,0 +1,301 @@
use super::binary::{binary_kind, BinaryKind};
use super::NormalizedExprLowererBox;
use super::super::expr_lowering_contract::{
ExprLoweringScope, ImpurePolicy, OutOfScopeReason,
};
use super::super::known_intrinsics::KnownIntrinsicRegistryBox;
use crate::ast::{ASTNode, LiteralValue, UnaryOperator};
use crate::mir::ValueId;
use std::collections::BTreeMap;
impl NormalizedExprLowererBox {
/// 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 => {
if Self::is_supported_int_expr(operand, env) {
None
} else {
Some(OutOfScopeReason::UnsupportedOperator)
}
}
UnaryOperator::Not => {
if Self::is_supported_bool_expr(operand, env) {
None
} else {
Some(OutOfScopeReason::UnsupportedOperator)
}
}
UnaryOperator::BitNot => Some(OutOfScopeReason::UnsupportedOperator),
UnaryOperator::Weak => Some(OutOfScopeReason::UnsupportedOperator),
},
ASTNode::BinaryOp {
operator, left, right, ..
} => {
if binary_kind(operator).is_some() {
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,
..
} => {
if Self::match_known_intrinsic_method_call(policy, object, method, arguments, env)
.is_some()
{
None
} else {
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()
{
Some(OutOfScopeReason::IntrinsicNotWhitelisted)
} else {
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),
UnaryOperator::Weak => Some(OutOfScopeReason::UnsupportedOperator),
},
ASTNode::BinaryOp {
operator, left, right, ..
} => {
if 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!(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!(
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
}
},
}
}
}

View File

@ -0,0 +1,412 @@
use super::super::expr_lowering_contract::{ExprLoweringScope, ImpurePolicy, OutOfScopeReason};
use super::*;
use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span, 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;
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(&lt, &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()
));
}