Repository navigation
TDFA(1): delay the tag writes of a closure to the outgoing transitions - #209
Conversation
a97dd16 to
0d43c9a
Compare
d8fef8e to
7c83851
Compare
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
The TDFA(1) delayed-write mechanics, runtime position primitives, PPX emission logic, and updated/added tests are consistent and coherent with the stated semantics (including eof cases).
Review effort: Lite
Findings: None
What changed in this PR
This PR upgrades Sedlex’s tagged determinization from TDFA(0) to TDFA(1) by delaying tag writes discovered in an epsilon-closure until the configuration actually takes a character transition (one-symbol lookahead). This improves capture semantics in loop/closure shapes (e.g., avoids repeatedly writing the same end tag on every loop iteration) while keeping the generated runtime operations correct around eof.
Changes:
- Implement TDFA(1) delayed-closure writes in the compiler (state keys include delayed writes; transitions materialize delayed writes; accepting
final_opsmaterialize delayed writes onto canonical cells). - Extend the runtime + PPX code generator to record “position before the consumed character” correctly, including transitions that may consume either a character or
eof. - Update/extend expect tests to reflect the new DOT/codegen output and add coverage for
eof-involved transitions.
| File | Description |
|---|---|
| test/codegen/test_realistic.ml | Updates the “realistic” codegen expect output to reflect delayed tag writes on loop exit transitions. |
| test/codegen/test_gen.ml | Updates many codegen expects for TDFA(1) tag placement and adds an expect for transitions that eof can take. |
| test/basic.ml | Adjusts memory-cell expectation for a discriminator case and adds behavior tests for submatch end positions around eof. |
| src/syntax/ppx_sedlex.ml | Adds position modes and emits set_mem_prev_pos / set_mem_pos_before depending on whether a transition may read eof. |
| src/lib/sedlexing.mli | Exposes new runtime primitives for recording pre-consumption positions in memory cells. |
| src/lib/sedlexing.ml | Implements __private__set_mem_prev_pos and __private__set_mem_pos_before. |
| src/compiler/sedlex.mli | Updates documentation around transition op timing and clarifies init_tags as always empty under delayed-closure writes. |
| src/compiler/sedlex.ml | Implements TDFA(1) delayed writes via config.delayed, updated closure/transition logic, and accepting-state finalization of delayed writes. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
91e6f84 to
1a3724f
Compare
|
Ah, you found the Trofimovich paper! Very cool! |
|
(When do you think you want to commit?) |
This is ready to merge, I think |
[eof | 'b'] is one character set in the NFA, so the DFA could have a transition taken both by end of input and by characters. Reading end of input does not advance the position, unlike reading a character, which makes such a transition awkward for anything that depends on the position. [split_moves] now splits them: a transition's character set is either [Cset.eof] or does not contain end of input. Both halves lead to the same state with the same operations. Only patterns mixing [eof] with characters in one alternation are affected (one more match arm); no existing expect output changes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
[__private__set_mem_prev_pos] records the current position minus one, the position before the code point [__private__next_int] just returned. Nothing emits it yet. The next commit attaches the tag operations of a state's epsilon closure to its outgoing transitions, where they must record the position before the character that selected the transition. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The tag writes met in the epsilon closure of a state were performed on the transition entering it (a TDFA(0)). Following Trofimovich, "Tagged Deterministic Finite Automata with Lookahead" (2017), each configuration now keeps them as delayed writes and performs them when it takes a character transition, i.e. once the next character is known. A configuration that cannot consume that character never performs them. - The end tag of [Plus digit as n] is written once, on the transition leaving the loop, not on every digit: the self-loop goal of the codegen tests is reached. - Fewer tags have two live values at once. Over test_gen.ml: 47 -> 29 writes, 29 -> 19 copies, 48 -> 40 cells, for 127 -> 130 states (states that differ only by their delayed writes cannot be identified). - Nothing runs before the automaton starts: the writes of the initial closure are delayed too, so [init_tags] goes away ([compile] asserts it). [eps_closure] records writes in [config.delayed]; [transitions] turns the delayed writes of a configuration that moves into the [Pending] addresses of that transition, so cell assignment and register moves are unchanged; [State_key] includes the delayed writes; [final_ops_of] sets those of the accepting configuration on the canonical cells. The operations of a transition execute before its character is consumed, but the generated code runs them after reading it, so [Set_position] compiles to [__private__set_mem_prev_pos] there. Reading end of input does not advance, so end-of-input transitions, which are separate since the previous commit but one, keep [__private__set_mem_pos]. Every captured value pinned in test/basic.ml is unchanged (one cell count drops). Patterns without captures generate byte-identical code. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Transition operations record the position before the character read, but reading end of input does not advance. Pin the sub-matches for a capture followed by [eof | 'b'] and by an optional tail then [eof]. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Turns the tagged DFA of #204 into a TDFA(1), after Trofimovich, "Tagged Deterministic Finite Automata with Lookahead" (2017). Stacked on #208 (base: the
pr4c-no-mark-snapshotbranch), four commits.The idea. In #204 the tag writes met in the epsilon closure of a state are performed on the transition entering it (TDFA(0)). Here each configuration keeps them as delayed writes and performs them when it takes a character transition, i.e. once the next character is known. A configuration that cannot consume that character never performs them.
(Plus digit as n), '.'is written once, on the'.'transition, not on every digit: the "self-loop tag delay" Goal of the codegen tests, obtained in general.test/codegen/test_gen.ml: writes 47 → 29, copies 29 → 19, cells 48 → 40, for 127 → 130 states (states that differ only by their delayed writes cannot be identified). Realistic lexer: 17 states before and after, writes 4 → 2.init_tagsis removed fromcompiledandcompiled_ir(sedlex.compilerhas not been released).Commits
eof | 'b'is one character set in the NFA, so a DFA transition could be taken both by end of input and by characters; reading end of input does not advance the position, unlike reading a character.split_movesnow splits such sets: a transition's character set is eitherCset.eofor does not contain end of input. One more match arm for those rare patterns, no existing expect output changes.__private__set_mem_prev_pos.configgainsdelayed; the closure records writes there;transitionsturns the delayed writes of a configuration that moves into the writes of that transition, so cell assignment and register moves are untouched; state keys include the delayed writes; final operations set those of the accepting configuration on the canonical cells. The operations of a transition execute before its character is consumed, but the generated code runs them after reading it, soSet_positioncompiles toset_mem_prev_poson character transitions and staysset_mem_poson end-of-input ones.Verification
test/basic.mlis unchanged (one cell count drops). Patterns without captures generate byte-identical code.eof, hence commit 4.#207 (register optimizations) is stacked on this PR.
🤖 Generated with Claude Code