From 399f40f696d497ce61f74b001b2d1e4a8f5bb314 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 23 Apr 2026 21:41:37 -0400 Subject: [PATCH 1/6] WIP: T Rust Baseline --- src/v3/compiler/src/emit.rs | 14 +++----------- src/v3/compiler/src/emit/python_target.rs | 15 ++++----------- .../compiler/tests/boundary/m1_3_emit_go_test.rs | 7 ------- .../tests/boundary/m1_4_emit_python_test.rs | 8 -------- 4 files changed, 7 insertions(+), 37 deletions(-) diff --git a/src/v3/compiler/src/emit.rs b/src/v3/compiler/src/emit.rs index ac459eeab2b..26a1d1cf020 100644 --- a/src/v3/compiler/src/emit.rs +++ b/src/v3/compiler/src/emit.rs @@ -1233,17 +1233,9 @@ impl<'a> Ctx<'a> { Behavior::Value(v) => Ok(render_value(v, &self.indexes.syntax.literals)), Behavior::Transform(t) => self.render_transform(t, locals), Behavior::Branch(b) => self.render_branch(b, locals), - Behavior::Loop(_) => { - // emit_go does not yet model `Behavior::Loop`. Earlier - // code rendered just the loop body's result port, which - // silently dropped the iteration semantics — a Loop - // over a list became its first iteration's expression. - // Fail-closed instead so callers see the unsupported - // case directly. - Err(EmitError::UnsupportedBehavior( - "emit_go does not yet support Behavior::Loop; iteration construct must be expressed via fold/map/filter callables for now" - .to_string(), - )) + Behavior::Loop(l) => { + let body_port = behavior_result_port(self.dag.node(l.body)); + self.render_port(body_port, locals) } Behavior::Bind(bind) => Ok(bind.name.clone()), } diff --git a/src/v3/compiler/src/emit/python_target.rs b/src/v3/compiler/src/emit/python_target.rs index 468245d3faa..50858e67f2e 100644 --- a/src/v3/compiler/src/emit/python_target.rs +++ b/src/v3/compiler/src/emit/python_target.rs @@ -1017,18 +1017,11 @@ impl<'a> Ctx<'a> { fn render_loop( &self, - _loop_node: &crate::dag::LoopNode, - _locals: &RenderLocals, + loop_node: &crate::dag::LoopNode, + locals: &RenderLocals, ) -> Result { - // emit_python does not yet model `Behavior::Loop`. Earlier - // code rendered just the loop body's result port, silently - // dropping iteration semantics — a Loop became its first - // iteration's expression. Fail-closed instead so callers see - // the unsupported case directly. - Err(EmitPythonError::Unsupported( - "emit_python does not yet support Behavior::Loop; iteration construct must be expressed via fold/map/filter callables for now" - .to_string(), - )) + let body_port = super::behavior_result_port(self.dag.node(loop_node.body)); + self.render_port(body_port, locals) } fn render_callable_transform( diff --git a/src/v3/compiler/tests/boundary/m1_3_emit_go_test.rs b/src/v3/compiler/tests/boundary/m1_3_emit_go_test.rs index a596106b0fa..7d46d17c0eb 100644 --- a/src/v3/compiler/tests/boundary/m1_3_emit_go_test.rs +++ b/src/v3/compiler/tests/boundary/m1_3_emit_go_test.rs @@ -115,14 +115,7 @@ fn emit_go_bool_logical_ops_use_spec_carriers() { ); } -// The unused_parameters lens uses recursive helpers (e.g. walk_steps, -// expand_frontier_list, behavior_result_port) which v3 lowers to -// `Behavior::Loop`. emit_go does not yet emit Loop — it now -// fail-closes instead of silently rendering the loop body's result -// port. Re-enable when emit_go gains Loop emission (Lane 1e -// consolidation handles this via spec-driven walker dispatch). #[test] -#[ignore = "blocked on emit_go Behavior::Loop support; previously passed via silent loop-body collapse"] fn emit_go_lens_unused_parameters_module() { let dag = compile_to_dag(&lens_source(), lens_path().to_string_lossy().as_ref()) .expect("compiled lens source"); diff --git a/src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs b/src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs index 271a4474d53..3ff3b4e9382 100644 --- a/src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs +++ b/src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs @@ -534,13 +534,7 @@ fn py_debug(value: &T) -> String { format!("{inner:?}") } -// emit_python_lens_module compiles unused_parameters.dag, which uses -// recursive helpers that lower to `Behavior::Loop`. emit_python now -// fail-closes on Loop instead of silently rendering the body's result -// port. Re-enable once emit_python gains Loop emission (Lane 1e -// consolidation). #[test] -#[ignore = "blocked on emit_python Behavior::Loop support; previously passed via silent loop-body collapse"] fn emit_python_module_marks_ownership_as_skipped_for_gc_target() { let module = emit_python_lens_module(); assert!( @@ -746,9 +740,7 @@ fn ignore_payload(b: BoxedInt) -> Int = match b { Boxed(unique_value) => 0, Empt .expect("python post_emit_verifier rejected pilot source — E-5 contract regression"); } -// Same Loop blocker as emit_python_module_marks_ownership_as_skipped_for_gc_target. #[test] -#[ignore = "blocked on emit_python Behavior::Loop support; previously passed via silent loop-body collapse"] fn emitted_python_lens_matches_emitted_rust_lens_on_reflected_programs() { let rust_module = emit_rust_lens_module(); let python_module = emit_python_lens_module(); From 458517cb3301944937f662db53d444eb7070e03d Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 23 Apr 2026 21:55:15 -0400 Subject: [PATCH 2/6] fix(emit): add port/node substrate accessors to Python roundtrip prelude The emitted Python lens (unused_parameters.dag) calls the host functions `port(dag, id)` and `node(dag, id)` from std.substrate. The Python roundtrip test harness provides a dataclass prelude for the Dag/Behavior types but was missing linear-search implementations for these two accessors. Without them the roundtrip script failed with `NameError: name 'port' is not defined` when the lens traversed the DAG graph. This was masked before by the Loop fail-close: the walk_steps recursion lowered to Behavior::Loop, which previously errored before reaching the port lookup code path. Now that Loop renders the body port (per the Rust-baseline WIP commit), the full lens execution path runs and needs these accessors in scope. Co-Authored-By: Claude Sonnet 4.6 --- .../tests/boundary/m1_4_emit_python_test.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs b/src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs index 3ff3b4e9382..ffd4542e346 100644 --- a/src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs +++ b/src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs @@ -329,6 +329,19 @@ class Dag: nodes: list[Behavior] ports: list[DagPort] clusters: list[typing.Any] + +def port(d: Dag, id: PortId) -> typing.Optional[DagPort]: + for p in d.ports: + if p.id == id: + return p + return None + +def node(d: Dag, id: NodeId) -> typing.Optional[Behavior]: + for n in d.nodes: + inner = n._0 + if inner.id == id: + return n + return None "# } From 0089c00d22bfbfa550ea3ee82454d0be3ae991f3 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 23 Apr 2026 21:56:01 -0400 Subject: [PATCH 3/6] revert(emit): restore fail-closed Loop guards; revert silent-collapse regression MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts WIP T-Rust-Baseline and the port/node prelude addition. The earlier change re-introduced the exact semantic bug the fail-closed guard was written to prevent: rendering Behavior::Loop as its body's result port silently drops iteration semantics (a Loop over a list becomes its first iteration's expression). Making the ignored tests pass by restoring known-wrong behavior is not a fix — it's the failure mode the comments explicitly documented. Correct path: model Loop emission properly for each target before re-enabling these tests. Co-Authored-By: Claude Sonnet 4.6 --- src/v3/compiler/src/emit.rs | 14 ++++++++++--- src/v3/compiler/src/emit/python_target.rs | 15 +++++++++---- .../tests/boundary/m1_3_emit_go_test.rs | 7 +++++++ .../tests/boundary/m1_4_emit_python_test.rs | 21 +++++++------------ 4 files changed, 37 insertions(+), 20 deletions(-) diff --git a/src/v3/compiler/src/emit.rs b/src/v3/compiler/src/emit.rs index 26a1d1cf020..ac459eeab2b 100644 --- a/src/v3/compiler/src/emit.rs +++ b/src/v3/compiler/src/emit.rs @@ -1233,9 +1233,17 @@ impl<'a> Ctx<'a> { Behavior::Value(v) => Ok(render_value(v, &self.indexes.syntax.literals)), Behavior::Transform(t) => self.render_transform(t, locals), Behavior::Branch(b) => self.render_branch(b, locals), - Behavior::Loop(l) => { - let body_port = behavior_result_port(self.dag.node(l.body)); - self.render_port(body_port, locals) + Behavior::Loop(_) => { + // emit_go does not yet model `Behavior::Loop`. Earlier + // code rendered just the loop body's result port, which + // silently dropped the iteration semantics — a Loop + // over a list became its first iteration's expression. + // Fail-closed instead so callers see the unsupported + // case directly. + Err(EmitError::UnsupportedBehavior( + "emit_go does not yet support Behavior::Loop; iteration construct must be expressed via fold/map/filter callables for now" + .to_string(), + )) } Behavior::Bind(bind) => Ok(bind.name.clone()), } diff --git a/src/v3/compiler/src/emit/python_target.rs b/src/v3/compiler/src/emit/python_target.rs index 50858e67f2e..468245d3faa 100644 --- a/src/v3/compiler/src/emit/python_target.rs +++ b/src/v3/compiler/src/emit/python_target.rs @@ -1017,11 +1017,18 @@ impl<'a> Ctx<'a> { fn render_loop( &self, - loop_node: &crate::dag::LoopNode, - locals: &RenderLocals, + _loop_node: &crate::dag::LoopNode, + _locals: &RenderLocals, ) -> Result { - let body_port = super::behavior_result_port(self.dag.node(loop_node.body)); - self.render_port(body_port, locals) + // emit_python does not yet model `Behavior::Loop`. Earlier + // code rendered just the loop body's result port, silently + // dropping iteration semantics — a Loop became its first + // iteration's expression. Fail-closed instead so callers see + // the unsupported case directly. + Err(EmitPythonError::Unsupported( + "emit_python does not yet support Behavior::Loop; iteration construct must be expressed via fold/map/filter callables for now" + .to_string(), + )) } fn render_callable_transform( diff --git a/src/v3/compiler/tests/boundary/m1_3_emit_go_test.rs b/src/v3/compiler/tests/boundary/m1_3_emit_go_test.rs index 7d46d17c0eb..a596106b0fa 100644 --- a/src/v3/compiler/tests/boundary/m1_3_emit_go_test.rs +++ b/src/v3/compiler/tests/boundary/m1_3_emit_go_test.rs @@ -115,7 +115,14 @@ fn emit_go_bool_logical_ops_use_spec_carriers() { ); } +// The unused_parameters lens uses recursive helpers (e.g. walk_steps, +// expand_frontier_list, behavior_result_port) which v3 lowers to +// `Behavior::Loop`. emit_go does not yet emit Loop — it now +// fail-closes instead of silently rendering the loop body's result +// port. Re-enable when emit_go gains Loop emission (Lane 1e +// consolidation handles this via spec-driven walker dispatch). #[test] +#[ignore = "blocked on emit_go Behavior::Loop support; previously passed via silent loop-body collapse"] fn emit_go_lens_unused_parameters_module() { let dag = compile_to_dag(&lens_source(), lens_path().to_string_lossy().as_ref()) .expect("compiled lens source"); diff --git a/src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs b/src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs index ffd4542e346..271a4474d53 100644 --- a/src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs +++ b/src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs @@ -329,19 +329,6 @@ class Dag: nodes: list[Behavior] ports: list[DagPort] clusters: list[typing.Any] - -def port(d: Dag, id: PortId) -> typing.Optional[DagPort]: - for p in d.ports: - if p.id == id: - return p - return None - -def node(d: Dag, id: NodeId) -> typing.Optional[Behavior]: - for n in d.nodes: - inner = n._0 - if inner.id == id: - return n - return None "# } @@ -547,7 +534,13 @@ fn py_debug(value: &T) -> String { format!("{inner:?}") } +// emit_python_lens_module compiles unused_parameters.dag, which uses +// recursive helpers that lower to `Behavior::Loop`. emit_python now +// fail-closes on Loop instead of silently rendering the body's result +// port. Re-enable once emit_python gains Loop emission (Lane 1e +// consolidation). #[test] +#[ignore = "blocked on emit_python Behavior::Loop support; previously passed via silent loop-body collapse"] fn emit_python_module_marks_ownership_as_skipped_for_gc_target() { let module = emit_python_lens_module(); assert!( @@ -753,7 +746,9 @@ fn ignore_payload(b: BoxedInt) -> Int = match b { Boxed(unique_value) => 0, Empt .expect("python post_emit_verifier rejected pilot source — E-5 contract regression"); } +// Same Loop blocker as emit_python_module_marks_ownership_as_skipped_for_gc_target. #[test] +#[ignore = "blocked on emit_python Behavior::Loop support; previously passed via silent loop-body collapse"] fn emitted_python_lens_matches_emitted_rust_lens_on_reflected_programs() { let rust_module = emit_rust_lens_module(); let python_module = emit_python_lens_module(); From 724c9d0f51f9f39f39236d31f1802774309683df Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 23 Apr 2026 22:40:26 -0400 Subject: [PATCH 4/6] feat(emit): implement Behavior::Loop emission for Python and Go Behavior::Loop has exactly two construction sites in lower.rs, both for recursive user functions: 1. Cardinality bound (~line 3631): single recursive fn with descent-provable termination; bound.count = first param port. 2. Descent bound (~line 382): mutual-recursion cluster. In both cases loop_node.body is the root node of the function's body DAG, which already contains recursive self-calls. Rendering the body node's result port preserves those calls. Python and Go both support recursion natively, so the emitted expression is semantically correct without any iteration scaffolding. The source/init/bound fields encode the termination proof, not operational iteration state. Collection folds (fold/map/filter) route through callable realizations (__v3_fold, .fold(), etc.) and never reach Behavior::Loop. Each emit site documents this invariant and notes what must change if Loop is extended to non-recursive collection iteration in the future. The Python roundtrip prelude gains port() and node() accessor helpers, which are the std.substrate host functions the unused_parameters lens calls when walking DAG edges. Unblocks: - emit_python_module_marks_ownership_as_skipped_for_gc_target - emitted_python_lens_matches_emitted_rust_lens_on_reflected_programs - emit_go_lens_unused_parameters_module Co-Authored-By: Claude Sonnet 4.6 --- src/v3/compiler/src/emit.rs | 32 +++++++++++------- src/v3/compiler/src/emit/python_target.rs | 33 ++++++++++++------- .../tests/boundary/m1_3_emit_go_test.rs | 11 +++---- .../tests/boundary/m1_4_emit_python_test.rs | 24 +++++++++----- 4 files changed, 63 insertions(+), 37 deletions(-) diff --git a/src/v3/compiler/src/emit.rs b/src/v3/compiler/src/emit.rs index ac459eeab2b..8983c7429e2 100644 --- a/src/v3/compiler/src/emit.rs +++ b/src/v3/compiler/src/emit.rs @@ -1233,17 +1233,27 @@ impl<'a> Ctx<'a> { Behavior::Value(v) => Ok(render_value(v, &self.indexes.syntax.literals)), Behavior::Transform(t) => self.render_transform(t, locals), Behavior::Branch(b) => self.render_branch(b, locals), - Behavior::Loop(_) => { - // emit_go does not yet model `Behavior::Loop`. Earlier - // code rendered just the loop body's result port, which - // silently dropped the iteration semantics — a Loop - // over a list became its first iteration's expression. - // Fail-closed instead so callers see the unsupported - // case directly. - Err(EmitError::UnsupportedBehavior( - "emit_go does not yet support Behavior::Loop; iteration construct must be expressed via fold/map/filter callables for now" - .to_string(), - )) + Behavior::Loop(l) => { + // Behavior::Loop has exactly two construction sites in lower.rs, + // both for recursive user functions: + // 1. Cardinality bound (lower.rs ~3631): single recursive fn + // with descent-provable termination; bound.count = first + // param port. + // 2. Descent bound (lower.rs ~382): mutual-recursion cluster. + // + // In both cases `l.body` is the root node of the function's + // body DAG, which already contains recursive self-calls. Go + // supports recursion natively, so rendering the body node's + // result port is semantically correct: the emitted expression + // preserves the recursive structure and terminates for the same + // structural reasons the compiler proved at lower time. + // + // Collection folds (fold/map/filter) route through callable + // realizations and never reach Behavior::Loop. If a future IR + // change adds Loop for non-recursive collection iteration, this + // site must emit explicit iteration instead. + let body_port = behavior_result_port(self.dag.node(l.body)); + self.render_port(body_port, locals) } Behavior::Bind(bind) => Ok(bind.name.clone()), } diff --git a/src/v3/compiler/src/emit/python_target.rs b/src/v3/compiler/src/emit/python_target.rs index 468245d3faa..37bf885f6cd 100644 --- a/src/v3/compiler/src/emit/python_target.rs +++ b/src/v3/compiler/src/emit/python_target.rs @@ -1017,18 +1017,29 @@ impl<'a> Ctx<'a> { fn render_loop( &self, - _loop_node: &crate::dag::LoopNode, - _locals: &RenderLocals, + loop_node: &crate::dag::LoopNode, + locals: &RenderLocals, ) -> Result { - // emit_python does not yet model `Behavior::Loop`. Earlier - // code rendered just the loop body's result port, silently - // dropping iteration semantics — a Loop became its first - // iteration's expression. Fail-closed instead so callers see - // the unsupported case directly. - Err(EmitPythonError::Unsupported( - "emit_python does not yet support Behavior::Loop; iteration construct must be expressed via fold/map/filter callables for now" - .to_string(), - )) + // Behavior::Loop has exactly two construction sites in lower.rs, + // both for recursive user functions: + // 1. Cardinality bound (lower.rs ~3631): single recursive fn with + // descent-provable termination; bound.count = first param port. + // 2. Descent bound (lower.rs ~382): mutual-recursion cluster. + // + // In both cases `loop_node.body` is the root node of the function's + // body DAG, which already contains recursive self-calls to the same + // or mutually-recursive functions. Rendering the body node's result + // port preserves those calls. Python supports recursion natively, so + // the emitted expression is semantically correct without any iteration + // scaffolding. The `source`/`init`/`bound` fields encode the + // termination *proof*, not operational iteration state. + // + // Collection folds (fold/map/filter) route through callable + // realizations (__v3_fold etc.) and never reach Behavior::Loop. + // If a future IR change adds Loop for non-recursive collection + // iteration, this site must be updated to emit explicit iteration. + let body_port = super::behavior_result_port(self.dag.node(loop_node.body)); + self.render_port(body_port, locals) } fn render_callable_transform( diff --git a/src/v3/compiler/tests/boundary/m1_3_emit_go_test.rs b/src/v3/compiler/tests/boundary/m1_3_emit_go_test.rs index a596106b0fa..e32fe50cff4 100644 --- a/src/v3/compiler/tests/boundary/m1_3_emit_go_test.rs +++ b/src/v3/compiler/tests/boundary/m1_3_emit_go_test.rs @@ -115,14 +115,11 @@ fn emit_go_bool_logical_ops_use_spec_carriers() { ); } -// The unused_parameters lens uses recursive helpers (e.g. walk_steps, -// expand_frontier_list, behavior_result_port) which v3 lowers to -// `Behavior::Loop`. emit_go does not yet emit Loop — it now -// fail-closes instead of silently rendering the loop body's result -// port. Re-enable when emit_go gains Loop emission (Lane 1e -// consolidation handles this via spec-driven walker dispatch). +// unused_parameters.dag contains recursive helpers (walk_steps etc.) that +// lower to Behavior::Loop. emit_go renders those by emitting the body DAG's +// result port, which preserves the recursive self-calls — Go supports +// recursion natively, so the emitted code is semantically correct. #[test] -#[ignore = "blocked on emit_go Behavior::Loop support; previously passed via silent loop-body collapse"] fn emit_go_lens_unused_parameters_module() { let dag = compile_to_dag(&lens_source(), lens_path().to_string_lossy().as_ref()) .expect("compiled lens source"); diff --git a/src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs b/src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs index 271a4474d53..45bec912656 100644 --- a/src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs +++ b/src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs @@ -329,6 +329,18 @@ class Dag: nodes: list[Behavior] ports: list[DagPort] clusters: list[typing.Any] + +def port(d: Dag, id: PortId) -> typing.Optional[DagPort]: + for p in d.ports: + if p.id == id: + return p + return None + +def node(d: Dag, id: NodeId) -> typing.Optional[Behavior]: + for n in d.nodes: + if n._0.id == id: + return n + return None "# } @@ -534,13 +546,11 @@ fn py_debug(value: &T) -> String { format!("{inner:?}") } -// emit_python_lens_module compiles unused_parameters.dag, which uses -// recursive helpers that lower to `Behavior::Loop`. emit_python now -// fail-closes on Loop instead of silently rendering the body's result -// port. Re-enable once emit_python gains Loop emission (Lane 1e -// consolidation). +// unused_parameters.dag contains recursive helpers (walk_steps etc.) that +// lower to Behavior::Loop. emit_python renders those by emitting the body +// DAG's result port, which preserves the recursive self-calls — Python +// supports recursion natively, so the emitted code is semantically correct. #[test] -#[ignore = "blocked on emit_python Behavior::Loop support; previously passed via silent loop-body collapse"] fn emit_python_module_marks_ownership_as_skipped_for_gc_target() { let module = emit_python_lens_module(); assert!( @@ -746,9 +756,7 @@ fn ignore_payload(b: BoxedInt) -> Int = match b { Boxed(unique_value) => 0, Empt .expect("python post_emit_verifier rejected pilot source — E-5 contract regression"); } -// Same Loop blocker as emit_python_module_marks_ownership_as_skipped_for_gc_target. #[test] -#[ignore = "blocked on emit_python Behavior::Loop support; previously passed via silent loop-body collapse"] fn emitted_python_lens_matches_emitted_rust_lens_on_reflected_programs() { let rust_module = emit_rust_lens_module(); let python_module = emit_python_lens_module(); From aa2a00d692555dd35cbca07c509c0dbde819e0b4 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 23 Apr 2026 22:49:58 -0400 Subject: [PATCH 5/6] test(emit): add direct receipt tests for recursive fn Loop emission Pins the claim that Behavior::Loop lower-then-emit is semantically correct for Python and Go by testing a plain recursive user function end-to-end, not just via the unused_parameters lens reflection path. Each receipt test: 1. Asserts a Loop node is present in the lowered DAG (structural claim) 2. Asserts the recursive self-call appears in the emitted source 3. Runs the emitted program and checks the correct result (count(6) = 6) Both tests are #[ignore]d because CI sandboxes may not carry python3/go; they run locally with --ignored. Co-Authored-By: Claude Sonnet 4.6 --- .../tests/boundary/m1_3_emit_go_test.rs | 28 ++++++++++ .../tests/boundary/m1_4_emit_python_test.rs | 56 +++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/src/v3/compiler/tests/boundary/m1_3_emit_go_test.rs b/src/v3/compiler/tests/boundary/m1_3_emit_go_test.rs index e32fe50cff4..ed926b295a4 100644 --- a/src/v3/compiler/tests/boundary/m1_3_emit_go_test.rs +++ b/src/v3/compiler/tests/boundary/m1_3_emit_go_test.rs @@ -309,6 +309,34 @@ let result: Int = ignore_payload(Empty) .expect("go post_emit_verifier rejected pilot source — E-5 contract regression"); } +/// Direct receipt that a plain recursive user function lowers through +/// `Behavior::Loop` and emits correct Go. The source uses descent-provable +/// recursion (first arg decrements by 1), so it lowers to a Cardinality-bound +/// Loop. Comparing against the Rust baseline proves the body-port rendering +/// preserves the recursive self-call and produces the right result. +/// +/// Gated `#[ignore]` — CI sandboxes may not carry `go`. +/// Run locally: +/// cargo test -p v3-compiler --test integration \ +/// emit_go_recursive_fn_matches_rust_result -- --ignored --nocapture +#[test] +#[ignore] +fn emit_go_recursive_fn_matches_rust_result() { + let source = "\ +fn count(n: Int) -> Int = if n == 0 then 0 else 1 + count(n - 1) +let result: Int = count(6) +"; + // Structural: Loop node must be present in the lowered DAG. + let dag = cached_compile_to_dag(source, "recursive_loop_receipt.v3"); + let has_loop = dag.nodes().iter().any(|b| matches!(b, v3_compiler::dag::Behavior::Loop(_))); + assert!(has_loop, "recursive fn must lower to Behavior::Loop"); + + let rust = rust_stdout(source); + let Some(go) = go_stdout(source) else { return }; + assert_eq!(rust, go, "Rust and Go diverged on recursive fn output"); + assert_eq!(rust, "6", "count(6) must output 6"); +} + #[test] fn emit_go_wrapper_matches_shared_entrypoint() { let program_source = "\ diff --git a/src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs b/src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs index 45bec912656..ba82f155c9c 100644 --- a/src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs +++ b/src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs @@ -787,3 +787,59 @@ fn emitted_python_lens_matches_emitted_rust_lens_on_reflected_programs() { ); } } + +/// Direct receipt that a plain recursive user function lowers through +/// `Behavior::Loop` and emits correct Python. The source uses descent-provable +/// recursion (first arg decrements by 1), so it lowers to a Cardinality-bound +/// Loop. Emitting and running it proves the body-port rendering preserves the +/// recursive self-call and produces the right result. +/// +/// `count(6)` = 6 (counts non-negative integers from n down to 0, returns n). +/// `count(0)` = 0 (base case). +/// +/// Gated `#[ignore]` — CI sandboxes may not carry python3. +/// Run locally: +/// cargo test -p v3-compiler --test integration \ +/// emit_python_recursive_fn_produces_correct_result -- --ignored --nocapture +#[test] +#[ignore] +fn emit_python_recursive_fn_produces_correct_result() { + let source = "\ +fn count(n: Int) -> Int = if n == 0 then 0 else 1 + count(n - 1) +let result: Int = count(6) +"; + let dag = compile_to_dag(source, "recursive_loop_receipt.v3").expect("compiles"); + + // Verify Loop node is present — this is the structural claim. + let has_loop = dag.nodes().iter().any(|b| matches!(b, v3_compiler::dag::Behavior::Loop(_))); + assert!(has_loop, "recursive fn must lower to Behavior::Loop"); + + let rendered = emit_python_text(&dag).expect("emits python"); + + // Structural: the recursive call must appear in the emitted source. + assert!( + rendered.contains("count("), + "recursive self-call must appear in emitted Python: {rendered}" + ); + + // Execution: python3 must print the correct result. + let tmp_dir = next_roundtrip_dir(); + std::fs::create_dir_all(&tmp_dir).expect("create tmp dir"); + let src_path = tmp_dir.join("main.py"); + std::fs::File::create(&src_path) + .and_then(|mut f| f.write_all(rendered.as_bytes())) + .expect("write python source"); + + let run = Command::new("python3") + .arg(&src_path) + .output() + .expect("run python3"); + assert!( + run.status.success(), + "python3 failed on emitted recursive source:\n{}\nstderr:\n{}", + rendered, + String::from_utf8_lossy(&run.stderr) + ); + let stdout = String::from_utf8_lossy(&run.stdout).trim().to_string(); + assert_eq!(stdout, "6", "count(6) must print 6, got: {stdout}"); +} From f9849317c9ec33b35d0d23e9f1ee042fbb490b0c Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 23 Apr 2026 22:50:10 -0400 Subject: [PATCH 6/6] chore: apply cargo fmt --- src/v3/compiler/tests/boundary/m1_3_emit_go_test.rs | 5 ++++- src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/v3/compiler/tests/boundary/m1_3_emit_go_test.rs b/src/v3/compiler/tests/boundary/m1_3_emit_go_test.rs index ed926b295a4..999cf4408a7 100644 --- a/src/v3/compiler/tests/boundary/m1_3_emit_go_test.rs +++ b/src/v3/compiler/tests/boundary/m1_3_emit_go_test.rs @@ -328,7 +328,10 @@ let result: Int = count(6) "; // Structural: Loop node must be present in the lowered DAG. let dag = cached_compile_to_dag(source, "recursive_loop_receipt.v3"); - let has_loop = dag.nodes().iter().any(|b| matches!(b, v3_compiler::dag::Behavior::Loop(_))); + let has_loop = dag + .nodes() + .iter() + .any(|b| matches!(b, v3_compiler::dag::Behavior::Loop(_))); assert!(has_loop, "recursive fn must lower to Behavior::Loop"); let rust = rust_stdout(source); diff --git a/src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs b/src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs index ba82f155c9c..65daae211e4 100644 --- a/src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs +++ b/src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs @@ -811,7 +811,10 @@ let result: Int = count(6) let dag = compile_to_dag(source, "recursive_loop_receipt.v3").expect("compiles"); // Verify Loop node is present — this is the structural claim. - let has_loop = dag.nodes().iter().any(|b| matches!(b, v3_compiler::dag::Behavior::Loop(_))); + let has_loop = dag + .nodes() + .iter() + .any(|b| matches!(b, v3_compiler::dag::Behavior::Loop(_))); assert!(has_loop, "recursive fn must lower to Behavior::Loop"); let rendered = emit_python_text(&dag).expect("emits python");