Skip to content

Emit constant templates and rebase the result in the pure sink's pattern arm - #24

Open
imlvts wants to merge 4 commits into
MesTTo:pr/mork-pure-compound-capturefrom
imlvts:pure-compound-capture-followups
Open

Emit constant templates and rebase the result in the pure sink's pattern arm#24
imlvts wants to merge 4 commits into
MesTTo:pr/mork-pure-compound-capturefrom
imlvts:pure-compound-capture-followups

Conversation

@imlvts

@imlvts imlvts commented Aug 7, 2026

Copy link
Copy Markdown

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

(exec 0 (,) (O (pure yes 321 (reverse_symbol 123))))

reverse_symbol(123) is 321, so the guard accepts — and the result is then dropped. The trace shows the guard was never the problem:

pure at 'yes 321 [2] reverse_symbol 12<truncated 1>'
pure registering in ctx "[51]"
pattern guard result "321"          <- evaluated and matched
pure template within its request root, skipping

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() > rooted skip becomes a debug_assert.

This was reachable by exactly the idiom the arm advertises. sink_pure_symbol_guard only 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)

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, 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 takes call_env.v as its de Bruijn base, which subterms already computes. One line.

Benchmarks

Re: benchmarking the pure sink specifically — mork bench pure runs both capture arms and the rejecting guard over one workload (100k rows, one tuple evaluation each), so the specialization is a measurement rather than an argument:

arm unifications writes transitions wall
var (specialized) 100001 100000 100007 132–136 ms
compound (generic) 100001 100000 100008 173–181 ms
guard (rejecting) 200000 100000 200006 ~133 ms

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_calculus unifications 201401, instructions 427949817; counter_machine steps 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_identityQuotation does not handle variable reference correctly trueagi-io/MORK#135 through this arm.

MesTTo and others added 4 commits July 22, 2026 11:10
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.
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
MesTTo force-pushed the pr/mork-pure-compound-capture branch from 5d7b9ff to 99b1566 Compare August 12, 2026 23:55
@MesTTo
MesTTo force-pushed the pr/mork-pure-compound-capture branch from 99b1566 to 18a8a49 Compare August 23, 2026 23:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants