Emit constant templates and rebase the result in the pure sink's pattern arm - #24
Open
imlvts wants to merge 4 commits into
Open
Conversation
A pure sink whose capture pattern led with a symbol or a compound hit the todo arms in PureSink::finalize and panicked (sinks.rs:1145, reported in issue 136). Both classes now run one rejection-pattern path: evaluate the call, unify the result against the pattern, and emit the template under the resulting bindings; a result that does not unify emits nothing. Wrapping the stored (template pattern call) triple in an Arity(3) gives the three subexpressions one shared variable namespace, so the pattern's VarRefs resolve to the template's introductions, and the engine's own unify and apply_e do the matching: data-side variables, repeated variables, and occurs rejection behave exactly as in a rewrite over (mytag <pat>). (exec 0 (,) (O (pure (R $x $y) ($x $y) (tuple 1 2)))) now yields (R 1 2), matching the two-exec workaround byte for byte. Unbound template variables stay schematic, and each body match unifies its own instantiated pattern, so ground positions reject per firing. A fully constant template still cannot emit below its own request root; the variable arms share that limitation. Four test cases cover destructuring and nesting, ground and repeated-variable rejection with an accepting control, the symbol guard, and per-match multiplicity. The full mork test suite passes and bench counters are unchanged (process_calculus unifications 201401, instructions 427949817; counter_machine steps 1813 size 2753).
The pure sink copied each stored (template pattern call) triple under a synthetic Arity(3) byte purely so args had an arity tag to read k off. Lift the per-subterm loop out of args into subterms(k), which takes the count as an argument and starts at the first subterm, and args becomes that after stepping over its arity byte. The sink then splits the run in place. The de Bruijn threading is unchanged, so the three still share one namespace and the pattern's VarRefs still resolve to the template's introductions. mork test is byte-identical, bench counters are unchanged (process_calculus unifications 201401, instructions 427949817), and 20k compound-capture rows retire 711,383 fewer instructions; the saving per row tracks triple length (35.6 short, 46.1 long), which is the copy.
# Conflicts: # kernel/src/main.rs
Two follow-ups to the compound capture arm, both of which produced a wrong answer rather than a panic. A constant template emitted nothing. With the write root now taken from the template rather than from the whole `(pure ...)` expression, an instantiation always extends the root by at least one byte, so the arm's `pbuffer.len() > rooted` skip is unreachable and becomes a debug_assert. `(pure yes 321 (reverse_symbol 123))` evaluated and matched, then dropped its result; it now emits `yes`. This is the shape the arm advertises as a guard, so it was reachable by exactly the idiom it was written for -- the symbol-guard test only passed because `(ok $i)` happens to carry a query-bound variable. The result was unified in a fresh namespace (trueagi-io#135). eval preserves the numbering of the expression it was handed, so a variable introduced inside the call comes back carrying its index in the sink expression's namespace rather than one relative to the result; a fresh namespace leaves that reference dangling, and a dangling reference prints as a fresh variable. The result is a subexpression of the stored triple, so it takes `call_env.v` as its de Bruijn base -- which `subterms` has already computed. `mork bench pure` runs the two arms and the rejecting guard over the same workload, since the reason the bare-var arm exists is performance and that should be a measurement. At 100k rows the two arms are within one transition of each other (100007 vs 100008, identical unifications and writes), but the specialization is consistently ~25% faster in wall clock, 132-136ms against 173-181ms over three interleaved runs. So the split earns its place for reasons the deterministic counters do not show, and trueagi-io#135 has to be fixed once per mechanism rather than by folding the arms together. Counters are reported as deltas because they are process-cumulative statics. process_calculus unifications 201401, instructions 427949817, unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DiDbwsxCa2oS8aBdKTSoS7
MesTTo
force-pushed
the
pr/mork-pure-compound-capture
branch
from
August 12, 2026 23:55
5d7b9ff to
99b1566
Compare
MesTTo
force-pushed
the
pr/mork-pure-compound-capture
branch
from
August 23, 2026 23:55
99b1566 to
18a8a49
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two follow-ups to the compound-capture arm added in trueagi-io#137. Both produce a wrong answer rather than a panic, so neither is caught by that arm's existing tests.
A constant template emitted nothing
reverse_symbol(123)is321, so the guard accepts — and the result is then dropped. The trace shows the guard was never the problem:request()took the write root from the prefix of the whole(pure <template> <pattern> <call>). That coincides with the template's own prefix only while the template holds a variable — the prefix stops there and never reaches the pattern. With a constant template it runs past the template into the pattern and the call, so the root covers the entire expression and there is nothing below it to write.The root now comes from the template alone (in the stacked commit below), so an instantiation always extends it by at least one byte and the
pbuffer.len() > rootedskip becomes adebug_assert.This was reachable by exactly the idiom the arm advertises.
sink_pure_symbol_guardonly passes because(ok $i)happens to carry a query-bound variable; write the same guard without one and it silently emits nothing.The result was unified in a fresh namespace (trueagi-io#135)
evalpreserves the numbering of the expression it was handed, so a variable introduced inside the call comes back carrying its index in the sink expression's namespace, not one relative to the result. Unifying it as standalone leaves the reference dangling, and a dangling reference prints as a fresh variable —($a $a)came out as($a $b). The result is a subexpression of the stored triple, so it now takescall_env.vas its de Bruijn base, whichsubtermsalready computes. One line.Benchmarks
Re: benchmarking the pure sink specifically —
mork bench pureruns both capture arms and the rejecting guard over one workload (100k rows, onetupleevaluation each), so the specialization is a measurement rather than an argument:var(specialized)compound(generic)guard(rejecting)The two arms are within one transition of each other, but the specialization is consistently ~25% faster in wall clock (min of three interleaved runs). So it earns its place for reasons the deterministic counters don't capture — which is also why trueagi-io#135 is fixed once per mechanism here rather than by folding the arms together. Counters are reported as deltas because they're process-cumulative statics.
No regression:
process_calculusunifications 201401, instructions 427949817;counter_machinesteps 1813 size 2753; both unchanged.Tests
sink_pure_constant_template_guard— constant symbol and constant compound templates, with three controls that separate "the guard rejected" from "the template was dropped": an accepting guard with a variable template, a constant template through the NewVar arm, and a rejecting guard with a variable template.sink_pure_compound_quoted_variable_identity— Quotation does not handle variable reference correctly trueagi-io/MORK#135 through this arm.