Implement symbol and compound capture patterns in the pure sink - #137
Implement symbol and compound capture patterns in the pure sink#137MesTTo wants to merge 2 commits into
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).
|
Hi! The reason for the specialization of the common case is performance. Please benchmark the pure sink specifically. |
|
benchmarked the pure sink on the PR tip vs base (45bdb9a), release, target-cpu=native, deterministic counters plus min-of-5 interleaved wall. the specialized arms aren't touched by the diff. the new path hangs off the pattern's leading byte class (symbol/arity), which previously hit the todo!(). measured: every pure-using program in kernel/resources (grounding, string_convert, ip_sudoku, decision_tree_learning_without_min_sink) is counter-identical base vs PR, and a 100k-row bare-var synthetic is counter-identical too (400,006 transitions both; wall 132 vs 140ms, winners split across runs, load noise). dtl-with-min-sink doesn't quiesce on either binary, excluded both sides. the new path vs the two-exec workaround it replaces (capture into a var, rematch with a second exec), same input, verified-identical result sets, N=100k: 200k vs 300k unifications, 100k vs 200k writes, 500,006 vs 800,009 transitions, 149 vs 213ms. the workaround also leaves the N tag facts in the space, not charged here. next to a comparable bare-var workload the compound arm costs ~1 extra transition per row (500,006 vs 400,006), so the bare-var specialization keeps earning its place, and the generic arm only takes the shapes that used to panic. reject path (ground-symbol guard) is cheaper than accept, 300,006 transitions at 100k, since rejection skips template application. |
|
I don't get why this is specialized to arity 3. |
|
i am not specializing on the pattern's arity there. the
that concatenation is not a well-formed expression on its own, so to read it back i push one i re-wrap rather than parse the three separately because of variable scope. parsed separately, each the pattern's own arity is unrestricted, which is the part i think the question is really about. the |
|
Right right, I've performed this trick elsewhere, too. Though I believe we should abstract this out for the sinks of this shape, and not create the intermediate 🤔 |
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.
|
done both, pushed as one commit. the intermediate is gone. the copy only existed so pub fn args(&self, dest: &mut Vec<Self>) {
match byte_item(*self.subsexpr().ptr) {
Tag::NewVar | Tag::VarRef(_) | Tag::SymbolSize(_) => { }
Tag::Arity(k) => { self.offset(1).subterms(k, dest) }
}
}and the sink splits the stored run in place: ExprEnv::new(0, Expr { ptr: full.as_ptr().cast_mut() }).subterms(3, &mut wargs);the de Bruijn threading is untouched, so the three still share one namespace and the pattern's verification: one thing i should correct from my own framing, since you said "for the sinks of this shape". i went it also composes with #138 without a conflict beyond the textual overlap: that PR replaces the |
Fixes #136.
A pure sink whose capture pattern led with a symbol or a compound hit the todo arms in
PureSink::finalizeand panicked (unrecognized sinkat the issue's commit,not yet implementedat current main). 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.The mechanism is the engine's own matching rather than a bespoke walker. The stored
(template pattern call)triple is wrapped in anArity(3)soExprEnv::argsgives the three subexpressions one shared variable namespace, the pattern unifies against the evaluated result withunify, and the template is instantiated withapply_eunder the same cycle checkdump_sexpruses. Data-side variables, repeated variables, and occurs rejection therefore behave exactly as in a rewrite over(mytag <pat>), which is the workaround this replaces:now yields
(R 1 2), matching the two-exec workaround byte for byte. A ground symbol pattern is the degenerate case of the same path and acts as a guard:(pure (ok $i) 321 (reverse_symbol $i))over(A 123)and(A 456)emits only(ok 123). Unbound template variables stay schematic, and each body match unifies its own instantiated pattern, so ground positions reject per firing.Two limitations remain. A fully constant template cannot emit below its own request root, because the request then covers the whole sink expression; the variable arms share that limitation, and this path traces and skips instead of panicking. And #135 (quotation treating every variable as new) is untouched, since that sits in evaluation upstream of the sink.
Validation:
mork testpasses in full, including four new cases: compound destructuring with nesting, ground and repeated-variable rejection with an accepting control, the symbol guard, and per-match multiplicity.unifications 201401, instructions 427949817; counter_machinesteps 1813 size 2753. The new path is unreachable unless a stored pattern leads with a symbol or arity byte, which previously panicked.git diff --checkclean; jscpd reports no duplication beyond the file's existing test scaffold template.