Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 21 additions & 11 deletions src/v3/compiler/src/emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
}
Expand Down
33 changes: 22 additions & 11 deletions src/v3/compiler/src/emit/python_target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, EmitPythonError> {
// 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(
Expand Down
42 changes: 35 additions & 7 deletions src/v3/compiler/tests/boundary/m1_3_emit_go_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -312,6 +309,37 @@ 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 = "\
Expand Down
83 changes: 75 additions & 8 deletions src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
"#
}

Expand Down Expand Up @@ -534,13 +546,11 @@ fn py_debug<T: std::fmt::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!(
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -779,3 +787,62 @@ 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}");
}
Loading