From 638ac2c587484142ac275a0d6fd8f9aeefdfc884 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 1 May 2026 07:11:33 +0000 Subject: [PATCH 1/6] feat(evaluator): add E1 value behavior execution --- src/v3/compiler/src/lib.rs | 213 ++++++++++++++++++++++++++++++++++++- 1 file changed, 210 insertions(+), 3 deletions(-) diff --git a/src/v3/compiler/src/lib.rs b/src/v3/compiler/src/lib.rs index a386d973af4..71b1b30debe 100644 --- a/src/v3/compiler/src/lib.rs +++ b/src/v3/compiler/src/lib.rs @@ -51,7 +51,72 @@ pub mod evaluator { use std::collections::HashMap; - use crate::dag::PortId; + use crate::dag::{Behavior, Dag, DeclarationId, LiteralBits, LoopBound, NodeId, PortId}; + + /// Rust mirror of the substrate runtime `Value` carrier in + /// `src/v3/std/runtime.dag`. + /// + /// **Dissolution receipt: TERMINAL.** This is not a second value authority: + /// it has the same five inhabitants as the `.dag` carrier and exists so the + /// Rust eager evaluator can return typed runtime data until generated + /// substrate-backed calls replace this host helper. + #[derive(Debug, Clone, PartialEq, Eq)] + pub enum Value { + LiteralValue(LiteralBits), + RecordValue(Vec), + VariantValue { + tag: DeclarationId, + payload: Box, + }, + NodeRef(NodeId), + CardinalityValue(LoopBound), + } + + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct NamedField { + pub label: String, + pub value: Value, + } + + /// **Dissolution receipt: TERMINAL.** These are the E1 body-evaluator miss + /// modes until later PR-E slices implement the remaining behavior arms. + #[derive(Debug, Clone, PartialEq, Eq)] + pub enum EvalError { + MissingNode { + node: NodeId, + }, + UnsupportedBehavior { + node: NodeId, + behavior: &'static str, + }, + } + + pub fn evaluate_body( + dag: &Dag, + entry: NodeId, + _state: &EvalStateStack, + ) -> Result { + match dag + .node_opt(&entry) + .ok_or(EvalError::MissingNode { node: entry })? + { + Behavior::Value(value) => Ok(Value::LiteralValue(value.data.clone())), + behavior => Err(EvalError::UnsupportedBehavior { + node: behavior.id(), + behavior: behavior_label(behavior), + }), + } + } + + fn behavior_label(behavior: &Behavior) -> &'static str { + match behavior { + Behavior::Value(_) => "Value", + Behavior::Transform(_) => "Transform", + Behavior::Branch(_) => "Branch", + Behavior::Loop(_) => "Loop", + Behavior::Bind(_) => "Bind", + } + } /// **Dissolution receipt: TERMINAL.** The three variants are distinct /// fail-closed outcomes for the E2 frame boundary. @@ -149,8 +214,11 @@ pub mod evaluator { #[cfg(test)] mod tests { - use super::{EvalFrame, EvalFrameError, EvalStateStack}; - use crate::dag::{Dag, LiteralBits, PortId}; + use super::{evaluate_body, EvalError, EvalFrame, EvalFrameError, EvalStateStack, Value}; + use crate::dag::{ + ArithmeticOp, Behavior, BranchPattern, Dag, LiteralBits, LoopBound, NodeId, Path, + PortId, TransformTarget, + }; use crate::diagnostics::SourceSpan; fn span() -> SourceSpan { @@ -164,6 +232,145 @@ pub mod evaluator { .collect() } + fn node_for_port(dag: &Dag, port: PortId) -> NodeId { + dag.resolve_producer_opt(&port).expect("producer").id() + } + + fn empty_state() -> EvalStateStack { + EvalStateStack::with_root_frame(EvalFrame::empty()) + } + + #[test] + fn value_behavior_evaluates_to_literal_runtime_value() { + let mut dag = Dag::new(); + let output = dag.push_value(LiteralBits::String("ready".to_string()), span()); + let entry = node_for_port(&dag, output); + let state = empty_state(); + + let value = evaluate_body(&dag, entry, &state).expect("value evaluates"); + + assert_eq!( + value, + Value::LiteralValue(LiteralBits::String("ready".to_string())) + ); + } + + #[test] + fn missing_entry_node_fails_closed() { + let mut source = Dag::new(); + let output = source.push_value(LiteralBits::Int(1), span()); + let stale_entry = node_for_port(&source, output); + let empty = Dag::new(); + let state = empty_state(); + + let err = evaluate_body(&empty, stale_entry, &state).expect_err("missing node"); + + assert_eq!(err, EvalError::MissingNode { node: stale_entry }); + } + + #[test] + fn transform_behavior_fails_closed() { + let mut dag = Dag::new(); + let lhs = dag.push_value(LiteralBits::Int(1), span()); + let rhs = dag.push_value(LiteralBits::Int(2), span()); + let output = dag.push_transform( + TransformTarget::Operator(crate::dag::OperatorKind::Arithmetic(ArithmeticOp::Add)), + vec![lhs, rhs], + span(), + ); + let entry = node_for_port(&dag, output); + let state = empty_state(); + + let err = evaluate_body(&dag, entry, &state).expect_err("unsupported transform"); + + assert_eq!( + err, + EvalError::UnsupportedBehavior { + node: entry, + behavior: "Transform" + } + ); + } + + #[test] + fn branch_behavior_fails_closed() { + let mut dag = Dag::new(); + let input = dag.push_value(LiteralBits::Bool(true), span()); + let arm_output = dag.push_value(LiteralBits::Int(1), span()); + let arm_body = dag.push_bind("arm", arm_output, Vec::new(), span()); + let output = dag.push_branch( + input, + vec![Path { + body: arm_body, + output: arm_output, + pattern: BranchPattern::UnresolvedVariant { + name: "True".to_string(), + span: span(), + }, + binding: None, + }], + span(), + ); + let entry = node_for_port(&dag, output); + let state = empty_state(); + + let err = evaluate_body(&dag, entry, &state).expect_err("unsupported branch"); + + assert_eq!( + err, + EvalError::UnsupportedBehavior { + node: entry, + behavior: "Branch" + } + ); + } + + #[test] + fn loop_behavior_fails_closed() { + let mut dag = Dag::new(); + let source = dag.push_value(LiteralBits::Int(3), span()); + let init = dag.push_value(LiteralBits::Int(0), span()); + let body = dag.push_bind("body", init, Vec::new(), span()); + let output = dag.push_loop( + source, + init, + body, + LoopBound::Cardinality { count: source }, + span(), + ); + let entry = node_for_port(&dag, output); + let state = empty_state(); + + let err = evaluate_body(&dag, entry, &state).expect_err("unsupported loop"); + + assert_eq!( + err, + EvalError::UnsupportedBehavior { + node: entry, + behavior: "Loop" + } + ); + } + + #[test] + fn bind_behavior_fails_closed() { + let mut dag = Dag::new(); + let value = dag.push_value(LiteralBits::Bool(false), span()); + let entry = dag.push_bind("flag", value, Vec::new(), span()); + let state = empty_state(); + + let err = evaluate_body(&dag, entry, &state).expect_err("unsupported bind"); + + assert_eq!( + err, + EvalError::UnsupportedBehavior { + node: entry, + behavior: "Bind" + } + ); + assert!(matches!(dag.node(entry), Behavior::Bind(_))); + } + #[test] fn lookup_walks_innermost_frame_first() { let ids = ports(2); From 43589fe9336f6b2e337dc54f347ea6969272e64c Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 1 May 2026 07:14:56 +0000 Subject: [PATCH 2/6] feat(evaluator): expose E1 eval shell --- src/v3/compiler/src/lib.rs | 125 ++++++++++++++++++++++++++++++++++--- 1 file changed, 115 insertions(+), 10 deletions(-) diff --git a/src/v3/compiler/src/lib.rs b/src/v3/compiler/src/lib.rs index 71b1b30debe..b59c6e6e6a2 100644 --- a/src/v3/compiler/src/lib.rs +++ b/src/v3/compiler/src/lib.rs @@ -85,22 +85,40 @@ pub mod evaluator { MissingNode { node: NodeId, }, + UnboundPort { + port: PortId, + }, UnsupportedBehavior { node: NodeId, behavior: &'static str, }, } - pub fn evaluate_body( + pub fn eval_value(value: &crate::dag::ValueNode) -> Value { + Value::LiteralValue(value.data.clone()) + } + + pub fn eval_port( dag: &Dag, - entry: NodeId, + port: PortId, + state: &EvalStateStack, + ) -> Result { + if let Ok(value) = state.lookup(port) { + return Ok(value.clone()); + } + let producer = dag + .resolve_producer_opt(&port) + .ok_or(EvalError::UnboundPort { port })?; + eval_node(dag, producer.id(), state) + } + + pub fn eval_node( + dag: &Dag, + node: NodeId, _state: &EvalStateStack, ) -> Result { - match dag - .node_opt(&entry) - .ok_or(EvalError::MissingNode { node: entry })? - { - Behavior::Value(value) => Ok(Value::LiteralValue(value.data.clone())), + match dag.node_opt(&node).ok_or(EvalError::MissingNode { node })? { + Behavior::Value(value) => Ok(eval_value(value)), behavior => Err(EvalError::UnsupportedBehavior { node: behavior.id(), behavior: behavior_label(behavior), @@ -108,6 +126,14 @@ pub mod evaluator { } } + pub fn evaluate_body( + dag: &Dag, + entry: NodeId, + state: &EvalStateStack, + ) -> Result { + eval_node(dag, entry, state) + } + fn behavior_label(behavior: &Behavior) -> &'static str { match behavior { Behavior::Value(_) => "Value", @@ -214,7 +240,10 @@ pub mod evaluator { #[cfg(test)] mod tests { - use super::{evaluate_body, EvalError, EvalFrame, EvalFrameError, EvalStateStack, Value}; + use super::{ + eval_node, eval_port, eval_value, evaluate_body, EvalError, EvalFrame, EvalFrameError, + EvalStateStack, Value, + }; use crate::dag::{ ArithmeticOp, Behavior, BranchPattern, Dag, LiteralBits, LoopBound, NodeId, Path, PortId, TransformTarget, @@ -241,13 +270,30 @@ pub mod evaluator { } #[test] - fn value_behavior_evaluates_to_literal_runtime_value() { + fn eval_value_constructs_literal_runtime_value() { + let mut dag = Dag::new(); + let output = dag.push_value(LiteralBits::String("ready".to_string()), span()); + let value_node = dag + .resolve_producer_opt(&output) + .and_then(Behavior::as_value) + .expect("value producer"); + + let value = eval_value(value_node); + + assert_eq!( + value, + Value::LiteralValue(LiteralBits::String("ready".to_string())) + ); + } + + #[test] + fn eval_node_dispatches_value_behavior_to_literal_runtime_value() { let mut dag = Dag::new(); let output = dag.push_value(LiteralBits::String("ready".to_string()), span()); let entry = node_for_port(&dag, output); let state = empty_state(); - let value = evaluate_body(&dag, entry, &state).expect("value evaluates"); + let value = eval_node(&dag, entry, &state).expect("value evaluates"); assert_eq!( value, @@ -255,6 +301,65 @@ pub mod evaluator { ); } + #[test] + fn eval_port_prefers_innermost_frame_binding() { + let mut dag = Dag::new(); + let port = dag.push_value(LiteralBits::Int(1), span()); + let outer = EvalFrame::from_bindings([( + port, + Value::LiteralValue(LiteralBits::String("outer".to_string())), + )]) + .expect("outer frame"); + let inner = EvalFrame::from_bindings([( + port, + Value::LiteralValue(LiteralBits::String("inner".to_string())), + )]) + .expect("inner frame"); + let state = EvalStateStack::from_outer_to_inner(vec![outer, inner]); + + let value = eval_port(&dag, port, &state).expect("bound port"); + + assert_eq!( + value, + Value::LiteralValue(LiteralBits::String("inner".to_string())) + ); + } + + #[test] + fn eval_port_falls_back_to_producer_value() { + let mut dag = Dag::new(); + let port = dag.push_value(LiteralBits::Bool(true), span()); + let state = empty_state(); + + let value = eval_port(&dag, port, &state).expect("producer value"); + + assert_eq!(value, Value::LiteralValue(LiteralBits::Bool(true))); + } + + #[test] + fn eval_port_reports_unbound_port_when_no_frame_or_producer_resolves() { + let mut source = Dag::new(); + let stale_port = source.push_value(LiteralBits::Int(1), span()); + let empty = Dag::new(); + let state = empty_state(); + + let err = eval_port(&empty, stale_port, &state).expect_err("unbound port"); + + assert_eq!(err, EvalError::UnboundPort { port: stale_port }); + } + + #[test] + fn evaluate_body_delegates_to_eval_node_shell() { + let mut dag = Dag::new(); + let output = dag.push_value(LiteralBits::Int(8), span()); + let entry = node_for_port(&dag, output); + let state = empty_state(); + + let value = evaluate_body(&dag, entry, &state).expect("value evaluates"); + + assert_eq!(value, Value::LiteralValue(LiteralBits::Int(8))); + } + #[test] fn missing_entry_node_fails_closed() { let mut source = Dag::new(); From 30c0a93f7311a6676dd811aa303b8ccea54eccf3 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 1 May 2026 07:19:30 +0000 Subject: [PATCH 3/6] feat(evaluator): preserve E1 strategy boundary --- src/v3/compiler/src/lib.rs | 126 +++++++++++++++++++++++++++++-------- 1 file changed, 100 insertions(+), 26 deletions(-) diff --git a/src/v3/compiler/src/lib.rs b/src/v3/compiler/src/lib.rs index b59c6e6e6a2..2eafe38ee74 100644 --- a/src/v3/compiler/src/lib.rs +++ b/src/v3/compiler/src/lib.rs @@ -78,6 +78,24 @@ pub mod evaluator { pub value: Value, } + /// Rust mirror of the PR-A.3 eager-baseline strategy carrier. + /// + /// **Dissolution receipt: 🟡 SCAFFOLD.** The public evaluator boundary must + /// carry strategy now so downstream slices cannot erase it. Only + /// `ApplicativeOrder / LeftFirst` is executable in E1; additional + /// inhabitants land with their evaluator rules. + #[derive(Debug, Clone, PartialEq, Eq)] + pub enum EvalStrategy { + ApplicativeOrder { input_order: InputEvaluationOrder }, + Unsupported { label: &'static str }, + } + + #[derive(Debug, Clone, PartialEq, Eq)] + pub enum InputEvaluationOrder { + LeftFirst, + Unsupported { label: &'static str }, + } + /// **Dissolution receipt: TERMINAL.** These are the E1 body-evaluator miss /// modes until later PR-E slices implement the remaining behavior arms. #[derive(Debug, Clone, PartialEq, Eq)] @@ -88,6 +106,9 @@ pub mod evaluator { UnboundPort { port: PortId, }, + UnsupportedStrategy { + strategy: EvalStrategy, + }, UnsupportedBehavior { node: NodeId, behavior: &'static str, @@ -101,22 +122,26 @@ pub mod evaluator { pub fn eval_port( dag: &Dag, port: PortId, - state: &EvalStateStack, + state: &mut EvalStateStack, + strategy: &EvalStrategy, ) -> Result { + ensure_supported_strategy(strategy)?; if let Ok(value) = state.lookup(port) { return Ok(value.clone()); } let producer = dag .resolve_producer_opt(&port) .ok_or(EvalError::UnboundPort { port })?; - eval_node(dag, producer.id(), state) + eval_node(dag, producer.id(), state, strategy) } pub fn eval_node( dag: &Dag, node: NodeId, - _state: &EvalStateStack, + _state: &mut EvalStateStack, + strategy: &EvalStrategy, ) -> Result { + ensure_supported_strategy(strategy)?; match dag.node_opt(&node).ok_or(EvalError::MissingNode { node })? { Behavior::Value(value) => Ok(eval_value(value)), behavior => Err(EvalError::UnsupportedBehavior { @@ -129,9 +154,21 @@ pub mod evaluator { pub fn evaluate_body( dag: &Dag, entry: NodeId, - state: &EvalStateStack, + state: &mut EvalStateStack, + strategy: EvalStrategy, ) -> Result { - eval_node(dag, entry, state) + eval_node(dag, entry, state, &strategy) + } + + fn ensure_supported_strategy(strategy: &EvalStrategy) -> Result<(), EvalError> { + match strategy { + EvalStrategy::ApplicativeOrder { + input_order: InputEvaluationOrder::LeftFirst, + } => Ok(()), + _ => Err(EvalError::UnsupportedStrategy { + strategy: strategy.clone(), + }), + } } fn behavior_label(behavior: &Behavior) -> &'static str { @@ -242,7 +279,7 @@ pub mod evaluator { mod tests { use super::{ eval_node, eval_port, eval_value, evaluate_body, EvalError, EvalFrame, EvalFrameError, - EvalStateStack, Value, + EvalStateStack, EvalStrategy, InputEvaluationOrder, Value, }; use crate::dag::{ ArithmeticOp, Behavior, BranchPattern, Dag, LiteralBits, LoopBound, NodeId, Path, @@ -269,6 +306,12 @@ pub mod evaluator { EvalStateStack::with_root_frame(EvalFrame::empty()) } + fn eager_strategy() -> EvalStrategy { + EvalStrategy::ApplicativeOrder { + input_order: InputEvaluationOrder::LeftFirst, + } + } + #[test] fn eval_value_constructs_literal_runtime_value() { let mut dag = Dag::new(); @@ -291,9 +334,10 @@ pub mod evaluator { let mut dag = Dag::new(); let output = dag.push_value(LiteralBits::String("ready".to_string()), span()); let entry = node_for_port(&dag, output); - let state = empty_state(); + let mut state = empty_state(); + let strategy = eager_strategy(); - let value = eval_node(&dag, entry, &state).expect("value evaluates"); + let value = eval_node(&dag, entry, &mut state, &strategy).expect("value evaluates"); assert_eq!( value, @@ -315,9 +359,10 @@ pub mod evaluator { Value::LiteralValue(LiteralBits::String("inner".to_string())), )]) .expect("inner frame"); - let state = EvalStateStack::from_outer_to_inner(vec![outer, inner]); + let mut state = EvalStateStack::from_outer_to_inner(vec![outer, inner]); + let strategy = eager_strategy(); - let value = eval_port(&dag, port, &state).expect("bound port"); + let value = eval_port(&dag, port, &mut state, &strategy).expect("bound port"); assert_eq!( value, @@ -329,9 +374,10 @@ pub mod evaluator { fn eval_port_falls_back_to_producer_value() { let mut dag = Dag::new(); let port = dag.push_value(LiteralBits::Bool(true), span()); - let state = empty_state(); + let mut state = empty_state(); + let strategy = eager_strategy(); - let value = eval_port(&dag, port, &state).expect("producer value"); + let value = eval_port(&dag, port, &mut state, &strategy).expect("producer value"); assert_eq!(value, Value::LiteralValue(LiteralBits::Bool(true))); } @@ -341,9 +387,11 @@ pub mod evaluator { let mut source = Dag::new(); let stale_port = source.push_value(LiteralBits::Int(1), span()); let empty = Dag::new(); - let state = empty_state(); + let mut state = empty_state(); + let strategy = eager_strategy(); - let err = eval_port(&empty, stale_port, &state).expect_err("unbound port"); + let err = + eval_port(&empty, stale_port, &mut state, &strategy).expect_err("unbound port"); assert_eq!(err, EvalError::UnboundPort { port: stale_port }); } @@ -353,22 +401,42 @@ pub mod evaluator { let mut dag = Dag::new(); let output = dag.push_value(LiteralBits::Int(8), span()); let entry = node_for_port(&dag, output); - let state = empty_state(); + let mut state = empty_state(); - let value = evaluate_body(&dag, entry, &state).expect("value evaluates"); + let value = + evaluate_body(&dag, entry, &mut state, eager_strategy()).expect("value evaluates"); assert_eq!(value, Value::LiteralValue(LiteralBits::Int(8))); } + #[test] + fn unsupported_strategy_fails_closed_before_dispatch() { + let mut dag = Dag::new(); + let output = dag.push_value(LiteralBits::Int(8), span()); + let entry = node_for_port(&dag, output); + let mut state = empty_state(); + let strategy = EvalStrategy::ApplicativeOrder { + input_order: InputEvaluationOrder::Unsupported { + label: "RightFirst", + }, + }; + + let err = evaluate_body(&dag, entry, &mut state, strategy.clone()) + .expect_err("unsupported strategy"); + + assert_eq!(err, EvalError::UnsupportedStrategy { strategy }); + } + #[test] fn missing_entry_node_fails_closed() { let mut source = Dag::new(); let output = source.push_value(LiteralBits::Int(1), span()); let stale_entry = node_for_port(&source, output); let empty = Dag::new(); - let state = empty_state(); + let mut state = empty_state(); - let err = evaluate_body(&empty, stale_entry, &state).expect_err("missing node"); + let err = evaluate_body(&empty, stale_entry, &mut state, eager_strategy()) + .expect_err("missing node"); assert_eq!(err, EvalError::MissingNode { node: stale_entry }); } @@ -384,9 +452,11 @@ pub mod evaluator { span(), ); let entry = node_for_port(&dag, output); - let state = empty_state(); + let mut state = empty_state(); + let strategy = eager_strategy(); - let err = evaluate_body(&dag, entry, &state).expect_err("unsupported transform"); + let err = + eval_node(&dag, entry, &mut state, &strategy).expect_err("unsupported transform"); assert_eq!( err, @@ -417,9 +487,11 @@ pub mod evaluator { span(), ); let entry = node_for_port(&dag, output); - let state = empty_state(); + let mut state = empty_state(); + let strategy = eager_strategy(); - let err = evaluate_body(&dag, entry, &state).expect_err("unsupported branch"); + let err = + eval_node(&dag, entry, &mut state, &strategy).expect_err("unsupported branch"); assert_eq!( err, @@ -444,9 +516,10 @@ pub mod evaluator { span(), ); let entry = node_for_port(&dag, output); - let state = empty_state(); + let mut state = empty_state(); + let strategy = eager_strategy(); - let err = evaluate_body(&dag, entry, &state).expect_err("unsupported loop"); + let err = eval_node(&dag, entry, &mut state, &strategy).expect_err("unsupported loop"); assert_eq!( err, @@ -462,9 +535,10 @@ pub mod evaluator { let mut dag = Dag::new(); let value = dag.push_value(LiteralBits::Bool(false), span()); let entry = dag.push_bind("flag", value, Vec::new(), span()); - let state = empty_state(); + let mut state = empty_state(); + let strategy = eager_strategy(); - let err = evaluate_body(&dag, entry, &state).expect_err("unsupported bind"); + let err = eval_node(&dag, entry, &mut state, &strategy).expect_err("unsupported bind"); assert_eq!( err, From f83bb1057d4a84a10d605d4026f2b43c1b009e16 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 1 May 2026 07:30:54 +0000 Subject: [PATCH 4/6] fix(evaluator): preserve producer authority in eval_port --- src/v3/compiler/src/lib.rs | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/v3/compiler/src/lib.rs b/src/v3/compiler/src/lib.rs index 2eafe38ee74..97a228b3019 100644 --- a/src/v3/compiler/src/lib.rs +++ b/src/v3/compiler/src/lib.rs @@ -126,13 +126,13 @@ pub mod evaluator { strategy: &EvalStrategy, ) -> Result { ensure_supported_strategy(strategy)?; - if let Ok(value) = state.lookup(port) { - return Ok(value.clone()); + if let Some(producer) = dag.resolve_producer_opt(&port) { + return eval_node(dag, producer.id(), state, strategy); } - let producer = dag - .resolve_producer_opt(&port) - .ok_or(EvalError::UnboundPort { port })?; - eval_node(dag, producer.id(), state, strategy) + state + .lookup(port) + .cloned() + .map_err(|_| EvalError::UnboundPort { port }) } pub fn eval_node( @@ -346,9 +346,26 @@ pub mod evaluator { } #[test] - fn eval_port_prefers_innermost_frame_binding() { + fn eval_port_prefers_dag_producer_over_frame_binding() { let mut dag = Dag::new(); let port = dag.push_value(LiteralBits::Int(1), span()); + let frame = EvalFrame::from_bindings([( + port, + Value::LiteralValue(LiteralBits::String("shadow".to_string())), + )]) + .expect("frame"); + let mut state = EvalStateStack::with_root_frame(frame); + let strategy = eager_strategy(); + + let value = eval_port(&dag, port, &mut state, &strategy).expect("producer wins"); + + assert_eq!(value, Value::LiteralValue(LiteralBits::Int(1))); + } + + #[test] + fn eval_port_uses_innermost_frame_binding_for_producerless_port() { + let mut dag = Dag::std_fixture_bootstrap_snapshot(); + let port = dag.alloc_port_with_shape(dag.bool_shape().expect("Bool shape")); let outer = EvalFrame::from_bindings([( port, Value::LiteralValue(LiteralBits::String("outer".to_string())), From f06cf0fb5352587462a32a9d8e2018c87ec55004 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 1 May 2026 07:35:48 +0000 Subject: [PATCH 5/6] fix(evaluator): keep strategy mirror substrate-aligned --- src/v3/compiler/src/lib.rs | 50 ++++++++------------------------------ 1 file changed, 10 insertions(+), 40 deletions(-) diff --git a/src/v3/compiler/src/lib.rs b/src/v3/compiler/src/lib.rs index 97a228b3019..1543443d22e 100644 --- a/src/v3/compiler/src/lib.rs +++ b/src/v3/compiler/src/lib.rs @@ -80,20 +80,19 @@ pub mod evaluator { /// Rust mirror of the PR-A.3 eager-baseline strategy carrier. /// - /// **Dissolution receipt: 🟡 SCAFFOLD.** The public evaluator boundary must - /// carry strategy now so downstream slices cannot erase it. Only - /// `ApplicativeOrder / LeftFirst` is executable in E1; additional - /// inhabitants land with their evaluator rules. + /// **Dissolution receipt: TERMINAL at PR-A.3 eager-baseline scope.** The + /// public evaluator boundary carries strategy now so downstream slices + /// cannot erase it, but the mirror stays identical to `runtime.dag`: + /// exactly `ApplicativeOrder / LeftFirst`. Additional inhabitants must land + /// with substrate carriers and executable evaluator rules. #[derive(Debug, Clone, PartialEq, Eq)] pub enum EvalStrategy { ApplicativeOrder { input_order: InputEvaluationOrder }, - Unsupported { label: &'static str }, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum InputEvaluationOrder { LeftFirst, - Unsupported { label: &'static str }, } /// **Dissolution receipt: TERMINAL.** These are the E1 body-evaluator miss @@ -106,9 +105,6 @@ pub mod evaluator { UnboundPort { port: PortId, }, - UnsupportedStrategy { - strategy: EvalStrategy, - }, UnsupportedBehavior { node: NodeId, behavior: &'static str, @@ -125,7 +121,6 @@ pub mod evaluator { state: &mut EvalStateStack, strategy: &EvalStrategy, ) -> Result { - ensure_supported_strategy(strategy)?; if let Some(producer) = dag.resolve_producer_opt(&port) { return eval_node(dag, producer.id(), state, strategy); } @@ -141,7 +136,11 @@ pub mod evaluator { _state: &mut EvalStateStack, strategy: &EvalStrategy, ) -> Result { - ensure_supported_strategy(strategy)?; + match strategy { + EvalStrategy::ApplicativeOrder { + input_order: InputEvaluationOrder::LeftFirst, + } => {} + } match dag.node_opt(&node).ok_or(EvalError::MissingNode { node })? { Behavior::Value(value) => Ok(eval_value(value)), behavior => Err(EvalError::UnsupportedBehavior { @@ -160,17 +159,6 @@ pub mod evaluator { eval_node(dag, entry, state, &strategy) } - fn ensure_supported_strategy(strategy: &EvalStrategy) -> Result<(), EvalError> { - match strategy { - EvalStrategy::ApplicativeOrder { - input_order: InputEvaluationOrder::LeftFirst, - } => Ok(()), - _ => Err(EvalError::UnsupportedStrategy { - strategy: strategy.clone(), - }), - } - } - fn behavior_label(behavior: &Behavior) -> &'static str { match behavior { Behavior::Value(_) => "Value", @@ -426,24 +414,6 @@ pub mod evaluator { assert_eq!(value, Value::LiteralValue(LiteralBits::Int(8))); } - #[test] - fn unsupported_strategy_fails_closed_before_dispatch() { - let mut dag = Dag::new(); - let output = dag.push_value(LiteralBits::Int(8), span()); - let entry = node_for_port(&dag, output); - let mut state = empty_state(); - let strategy = EvalStrategy::ApplicativeOrder { - input_order: InputEvaluationOrder::Unsupported { - label: "RightFirst", - }, - }; - - let err = evaluate_body(&dag, entry, &mut state, strategy.clone()) - .expect_err("unsupported strategy"); - - assert_eq!(err, EvalError::UnsupportedStrategy { strategy }); - } - #[test] fn missing_entry_node_fails_closed() { let mut source = Dag::new(); From f488d55af51b57918563234185b706572bbede90 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 1 May 2026 08:04:16 +0000 Subject: [PATCH 6/6] fix(evaluator): gate frame lookup by dag port --- src/v3/compiler/src/lib.rs | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/v3/compiler/src/lib.rs b/src/v3/compiler/src/lib.rs index 1543443d22e..a3deca9a553 100644 --- a/src/v3/compiler/src/lib.rs +++ b/src/v3/compiler/src/lib.rs @@ -121,8 +121,11 @@ pub mod evaluator { state: &mut EvalStateStack, strategy: &EvalStrategy, ) -> Result { - if let Some(producer) = dag.resolve_producer_opt(&port) { - return eval_node(dag, producer.id(), state, strategy); + let Some(port_ref) = dag.port_opt(&port) else { + return Err(EvalError::UnboundPort { port }); + }; + if let Some(producer) = port_ref.produced_by { + return eval_node(dag, producer, state, strategy); } state .lookup(port) @@ -401,6 +404,25 @@ pub mod evaluator { assert_eq!(err, EvalError::UnboundPort { port: stale_port }); } + #[test] + fn eval_port_rejects_frame_binding_for_port_absent_from_dag() { + let mut source = Dag::new(); + let stale_port = source.push_value(LiteralBits::Int(1), span()); + let empty = Dag::new(); + let frame = EvalFrame::from_bindings([( + stale_port, + Value::LiteralValue(LiteralBits::String("stale".to_string())), + )]) + .expect("frame"); + let mut state = EvalStateStack::with_root_frame(frame); + let strategy = eager_strategy(); + + let err = + eval_port(&empty, stale_port, &mut state, &strategy).expect_err("unbound port"); + + assert_eq!(err, EvalError::UnboundPort { port: stale_port }); + } + #[test] fn evaluate_body_delegates_to_eval_node_shell() { let mut dag = Dag::new();