Repository navigation
feat(emit): implement Behavior::Loop emission for Python and Go - #692
Conversation
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 <noreply@anthropic.com>
… regression 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
|
Review note: This is materially different from One thing I’d still like before treating this as a durable closure: add a direct Python/Go receipt for a plain recursive user function program, not just the A small targeted roundtrip using something like |
|
Review metadata
FindingsNone blocking. A couple of small observations:
VerdictAPPROVE. The change reverses the over-eager fail-closed guard from 0089c00 for a justified reason: lower.rs only emits |
|
Review metadata
Verdict: APPROVE. I don’t see a concrete violation of |
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 <noreply@anthropic.com>
… lists PR #692 landed Behavior::Loop emission for both Python and Go. The sole remaining entry in GO_EMIT_EXCLUDE and PYTHON_EMIT_EXCLUDE was recursive_function_call_six (blocked on Loop support). Both lists are now empty; all 9 PROGRAM_FIXTURES are covered by the omni set and by the 5× determinism matrix for every target. Update stale comments in determinism_test.rs to reflect the current state. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* WIP: T emit go/rust bounds
* chore: apply cargo fmt
* feat(python): add mul/div/ne/lt/le/gt/ge operator realizations; lift 3 Python exclusions
python.dag was missing seven Int operator realizations that Rust and Go
already had: mul (*), div (//), ne (!=), lt (<), le (<=), gt (>), ge (>=).
This unblocked list_map_then_fold_twelve (uses *), list_filter_then_fold_seven
(uses >), and nested_list_builtins_inside_lambda_six (uses * inside a lambda)
from Python emission — all three were excluded from PYTHON_EMIT_EXCLUDE with
a MissingOperatorRealization note.
After regenerating bootstrap and lifting those exclusions, the Python
determinism matrix now covers 8/9 PROGRAM_FIXTURES (same as Go), and the
emit_omni_demo_fixtures_green closure test checks 8 fixtures instead of 5.
The sole remaining Python exclusion is recursive_function_call_six (Loop).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: apply cargo fmt
* fix(sg0/parse-manifest): register m1_5_emit_omni_demo_test.rs; refresh python.dag snapshot
Register `tests/boundary/m1_5_emit_omni_demo_test.rs` in
`EXPECTED_HAND_AUTHORED` with director-approved receipt (T-Emit lane
closure, sharp-heron-47). Refresh `parse_corpus_manifest.txt` to
reflect `python.dag`'s updated item count and hash after gaining 7
operator realizations (`mul`, `div`, `ne`, `lt`, `le`, `gt`, `ge`).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(t-emit): honest lane-closure receipt — ignore gate + hard toolchain assert
Split the single test into two:
- `emit_omni_demo_rust_roundtrip` (non-ignored): CI gate that runs
unconditionally; proves the omni fixture set emits valid, executable Rust.
- `emit_omni_demo_fixtures_green` (#[ignore]): T-Emit lane closure receipt.
Marked ignore because go/python3 are absent in CI. When run with
--ignored it asserts both toolchains are reachable and fails hard if
either is missing — a missing toolchain is an unmet receipt, not a skip.
Addresses review finding: prior implementation returned None for absent
toolchains and passed with 0/N targets checked, making it a useful local
harness but not a truthful lane-closure proof.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(t-emit): pin floor-div semantic constraint; fix tmp-dir leakage
python_int_div: add comment at definition site pinning the known
floor-vs-truncated divergence for negative dividends (-7/2 → -4 in
Python, -3 in Rust/Go). Omni fixtures don't exercise this case; the
constraint is pinned rather than fixed until a target-appropriate
truncating-div primitive is modeled.
m1_5_emit_omni_demo_test: replace bare temp-dir allocation with a RAII
`TmpDir` wrapper whose `Drop` impl calls `remove_dir_all`. Repeated
local runs with three toolchains × N fixtures no longer accumulate
`v3_emit_omni_roundtrip_*` directories indefinitely.
Regenerate bootstrap and refresh parse-corpus manifest for the
python.dag comment addition.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(python): correct OrderedRing.div to truncate toward zero via __v3_idiv
Python's `//` is floor division, which diverges from Rust/Go integer `/`
(truncate toward zero) for negative dividends (-7/2 → -4 in Python, -3
in Rust/Go). This is a semantic correctness violation for any DAG that
produces negative integer division.
Fix:
- Add `__v3_idiv(a, b)` preamble helper using `divmod`-based adjustment:
if the remainder is non-zero and operand signs differ, adds 1 to the
floor quotient to restore C-style truncation toward zero.
- Change `python_int_div` carrier from `"//"` to the full-expression
template `"__v3_idiv({lhs}, {rhs})"`.
- Extend the Python emitter's binary-op rendering to detect full-expression
carriers (those containing `{lhs}`) and render them directly instead
of inserting into the `({lhs} {op} {rhs})` infix template.
Regenerate bootstrap and refresh parse-corpus manifest.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: apply cargo fmt
* refactor(emit): dissolve binary_op dual authority; carriers are sole expression templates
`ExpressionSyntax.binary_op` and `OperatorRealization.carrier` both
claimed authority over how binary expressions are rendered. The emitter
had to reverse-engineer carrier string content (`carrier.contains("{lhs}")`)
to decide which path to take — a string-sentinel violation of the modeling
discipline (INVARIANTS.md P2, Practice 5-6).
Fix: make `OperatorRealization.carrier` the sole authority.
- Remove `binary_op` from `ExpressionSyntax` in `std/emit_model.dag`.
- Remove `binary_op` from `python_expressions`, `go_expressions`, and
`rust_expressions` in all three spec files.
- Convert all infix operator carriers ("+", "-", "*", "/", "==", "!=",
"<", "<=", ">", ">=", "&&", "||", "and", "or") to full-expression
templates: `"({lhs} + {rhs})"` etc. Python's `__v3_idiv` carrier
gains outer parens to match: `"(__v3_idiv({lhs}, {rhs}))"`.
- All three emitters (`emit.rs`, `python_target.rs`, `rust_target.rs`):
remove `binary_op` field from the expression-syntax binding struct,
remove the `syntax_field_string("binary_op")` load, and replace the
three-binding template call with a direct two-binding render on the
carrier. The `carrier.contains("{lhs}")` probe in python_target.rs
is also removed — it is no longer needed.
Every `OperatorRealization.carrier` is now self-describing: it is always
a full-expression template with `{lhs}` and `{rhs}` placeholders, with
no external format convention required to interpret it.
Regenerate bootstrap and refresh parse-corpus manifest.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(t-emit): lift recursive_function_call_six from Go/Python exclude lists
PR #692 landed Behavior::Loop emission for both Python and Go. The sole
remaining entry in GO_EMIT_EXCLUDE and PYTHON_EMIT_EXCLUDE was
recursive_function_call_six (blocked on Loop support). Both lists are now
empty; all 9 PROGRAM_FIXTURES are covered by the omni set and by the 5×
determinism matrix for every target.
Update stale comments in determinism_test.rs to reflect the current state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(ci): update carrier snapshot in m1_3_prb_rust_dag_bootstrap_loads_structurally
The test was asserting `carrier == "+"` (old infix symbol form). After the
carrier unification in the previous commit, rust_int_add's carrier is now
`"({lhs} + {rhs})"`. Update the assertion to match.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(emit): fail-closed on operator carriers missing {lhs}/{rhs} placeholders
`render_named_template` is silent when a carrier lacks the expected
placeholders — a carrier like "+" would emit literally "+" instead of a
binary expression. Add a load-time check in all three emitters that rejects
any OperatorRealization whose carrier does not contain both {lhs} and {rhs},
turning a silent runtime divergence into a fail-closed load error.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(python): always-on emit assertions for new Int operator carriers
The new Python Int operators (mul, div, ne, lt, le, gt, ge) only had
determinism coverage in CI — the rendered template content was never
asserted. Add emit_python_int_operators_use_correct_expression_templates:
five always-on (non-ignored) checks that verify each operator emits the
expected Python token/call without requiring a Python toolchain.
Specifically: * emits the * operator, / emits __v3_idiv(...), != emits !=,
< emits <, > emits >.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
Behavior::Loopis emitted in Python and Go by rendering the body DAG's result port, which preserves recursive self-calls natively supported by both targetslower.rs, both for recursive user functions (Cardinality and Descent bounds); collection folds go through callable realizations and never reach Loopport()/node()substrate accessor helpers to the Python roundtrip test prelude (thestd.substratehost functions used byunused_parameters.dagwhen walking DAG edges)#[ignore]from three tests that were blocked on Loop emissionWhy body-port rendering is correct semantics
Behavior::Loopwraps a recursive function whose body DAG already contains the recursive self-calls. In both the Cardinality (single recursion,lower.rs~3631) and Descent (mutual recursion cluster,lower.rs~382) cases,loop_node.bodyis the root node of the function body DAG — the same DAG the compiler proved terminates. Rendering the body node's result port preserves those recursive calls. Python and Go handle recursion natively; thesource/init/boundfields encode the termination proof, not operational iteration state.Each emit site documents the IR invariant and flags what must be updated if Loop is ever extended to non-recursive collection iteration.
Test plan
emit_python_module_marks_ownership_as_skipped_for_gc_target— passesemitted_python_lens_matches_emitted_rust_lens_on_reflected_programs— passes (Rust and Python produce identical unused-parameter sets)emit_go_lens_unused_parameters_module— passescargo test -p v3-compiler --test integration— running🤖 Generated with Claude Code