Skip to content

Register optimizations for the tagged DFA: reuse per tag, conflict-free tags in their canonical cell - #207

Draft
hhugo wants to merge 3 commits into
ocaml-community:masterfrom
hhugo:pr4b-register-optims
Draft

hhugo wants to merge 3 commits into
ocaml-community:masterfrom
hhugo:pr4b-register-optims

Conversation

@hhugo

@hhugo hhugo commented Sep 20, 2026 •

Copy link
Copy Markdown
Collaborator

Register optimizations for the tagged DFA. Stacked on #209 (base: the pr4d-tdfa1 branch), three commits. Pure optimization: every captured value in test/basic.ml is unchanged; generated code and cell counts change.

After #204 every tag write gets a fresh working register and every accepting state copies the accepting path's registers into the canonical cells read by the bindings: two cells per tag and a copy per tag on accept. This PR brings ordinary patterns back to one cell per tag and no copy. Over test/codegen/test_gen.ml: copies 21 → 0, cells 42 → 23; realistic lexer: copies 2 → 0, cells 4 → 2.

Commits

  1. Reuse working registers per logical tag. A new write takes a register the tag already used that the candidate state does not hold, else a fresh one. Registers are never shared between tags.

  2. Write conflict-free tags directly to their canonical cell. A tag is conflicted when some state holds two registers for it. The registers of every other tag are renamed into its canonical cell once all states are built, and the copies that become no-ops are dropped. Two kinds of conflict-free tags still keep a working register, because writing their canonical cell at the wrong time would corrupt a value read later:

    • tags a transition could set between the mark of one of their rule's accepting states and a later failure (the rule's action then reads its canonical cells);
    • tags an accepting path sets through a delayed write (TDFA(1): delay the tag writes of a closure to the outgoing transitions #209) while another path of the same state holds them, since that path's register would be the canonical cell.

    So collapsing does not rely on Sedlexing.mark saving the cells (Stop copying the capture cells in Sedlexing.mark and backtrack #208).

  3. Tests: a codegen test for each of the three reasons a tag keeps working registers, a behaviour test for the last one, and the Current/Goal notes back to one cell per tag.

Verification

The #202 oracle, run locally with the TDFA(1) interpreter and no cell snapshot: no disagreement on the hand-written cases nor on 5M random cases. With either of the two conditions of commit 2 disabled, the same sweep finds wrong captures.

🤖 Generated with Claude Code

hhugo and others added 3 commits September 29, 2026 00:20
[Registers] keeps, per logical tag, the pool of working registers
allocated so far. [assign_cells] hands a [Pending] write a pooled
register that the candidate state does not already hold
([cells_in_use]) and only allocates a fresh one when none is free.
Pools of distinct tags stay disjoint, which the collapse of
conflict-free tags in the next commit relies on.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A tag is conflicted when some DFA state holds two distinct registers
for it: two live NFA paths recorded different values
([Registers.note_conflicts]). Every other tag only needs one register
at a time, so [Registers.collapse_conflict_free] renames its whole pool
to its canonical cell and [rename_ops] drops the copies that become
no-ops: its writes go straight to the cell the binding code reads, with
no final copy. The surviving working registers are compacted above the
canonical cells.

Two kinds of conflict-free tags still keep a working register, because
writing their canonical cell at the wrong time would corrupt a value
that is read later:

- tags that a transition could set between the mark of one of their
  rule's accepting states and a later failure, after which the rule's
  action reads its canonical cells ([note_clobbered]): those set on a
  transition into a non-accepting state reachable, through
  non-accepting states, from an accepting state of their rule;
- tags that an accepting configuration sets through a delayed write
  while another configuration of the same state holds them: the final
  operations write the canonical cell, which would be that other
  path's register ([final_ops_of]).

Collapsing is thus correct without Sedlexing.mark saving the cells.

Over test/codegen/test_gen.ml: copies 21 -> 0, cells 42 -> 23.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Codegen tests for the three reasons a tag does not live in its
canonical cell: two live starts for one capture (conflicted: two
registers, a parallel move on the loop, a copy on accept), a tag
recorded again after an accepting state of its rule on the way to a
possible failure, and a tag that an accepting path sets while another
path holds it. A behaviour test pins the sub-matches of the last shape.

The Current/Goal notes of the optimization-tracking tests go back to
one cell per tag.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

1 participant