Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 36 additions & 37 deletions doc/reports/aplr1/aplr1.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ used interchangeably.

Knuth discovered the LR(k) algorithm [^knuth1965] for parsing [deterministic context-free
grammars](https://en.wikipedia.org/wiki/Deterministic_context-free_grammar) over six decades ago and
showed that in practice LR(1) is a reasonable practical restriction, but at the time even LR(1) was
too computationally intensive to be usable. The LALR(1) algorithm [^deremer1969] is not as capable
as canonical LR(1), but despite superior alternatives since discovered, LALR(1) remains the *de
facto* LR(1)-family algorithm of choice. The relative obscurity of PGM LR(1) [^pager1977] appears to
be due to a combination of confusion surrounding related lane tracing research
showed that LR(1) is a reasonable restriction, but at the time even LR(1) was too computationally
intensive to be usable. The LALR(1) algorithm [^deremer1969] is not as capable as canonical LR(1),
but despite superior alternatives since discovered, LALR(1) remains the *de facto* LR(1)-family
algorithm of choice. The relative obscurity of PGM LR(1) [^pager1977] appears to be due to a
combination of confusion surrounding related lane tracing research
[^pager1973][^spector1981][^spector1988], along with having been overshadowed by the rapid
dissemination of the LALR(1) algorithm via the [Yacc](https://en.wikipedia.org/wiki/Yacc) parser
generator. The limited adoption of IELR(1) is due at least in part to its conceptual complexity,
Expand Down Expand Up @@ -145,8 +145,8 @@ which can be remerged without changing the recognized language, then performs al
applies the remerging algorithm to a canonical LR(1) automaton, but Hocc also uses the remerging
algorithm by default for automata generated by IELR⁺(1) and PGM LR(1). Earlier versions of Hocc
implemented a simpler iterative state pair remerging algorithm [^evans2024], discovered
independently by [^lenka2006]. The iterative algorithm has the disadvantage of being incapable of
remerging non-singleton cyclic subgraphs.
independently by Lenka and Kumar [^lenka2006]. The iterative algorithm has the disadvantage of being
incapable of remerging non-singleton cyclic subgraphs.

There is one subtle consequence of state merging that affects all of the above algorithms, namely
that it is possible for a sequence of reduce actions to lead to a state that contains no action for
Expand Down Expand Up @@ -313,10 +313,9 @@ A work queue state nub is processed by 1) popping it from the front of the work
computing goto sets, such that work queue and state nub set insertion may result.

Canonical LR(1) isocore merge compatibility is maximally strict, in that kernels must be isokernels
in order to be merged. Merging two isokernels results in an isokernel; thus merging is a no-op in
practice. Such strict compatibility commonly results in nearly identical states which could in
practice be merged without changing the grammar recognized by the automaton. APLR(1) removes
(nearly) all such redundancy.
in order to be merged. Merging two isokernels results in an isokernel; thus merging is a no-op. Such
strict compatibility commonly results in nearly identical states which could be merged without
changing the grammar recognized by the automaton. APLR(1) removes (nearly) all such redundancy.

### State generation

Expand Down Expand Up @@ -645,11 +644,11 @@ for all the grammars, so long as conflict resolution is enabled. There is no pra
disable conflict resolution for APLR(1) nor IELR⁺(1); these benchmarks are included only to provide
additional insight into how the algorithms operate and what their weaknesses are. APLR(1)
performance is susceptible to highly interconnected subgraphs; the more branching in a subgraph, the
more repeated graph searching must occur. Similarly for IELR⁺(1), the more interconnected a conflict
contribution graph is, the more traversal must occur to reach contribution closure. The main
difference is that APLR(1) is impervious to conflicts that are resolved in the corresponding LR(1)
automaton, whereas IELR⁺(1) must trace all reduce-dominant conflicts, regardless of whether those
conflicts are resolved in the corresponding LR(1) automaton.
more repeated graph exploration must occur. Similarly for IELR⁺(1), the more interconnected a
conflict contribution graph is, the more graph traversal must occur to reach contribution closure.
The main qualitative difference is that APLR(1) is impervious to conflicts that are resolved in the
corresponding LR(1) automaton, whereas IELR⁺(1) must trace all reduce-dominant conflicts, regardless
of whether those conflicts are resolved in the corresponding LR(1) automaton.

The OCaml grammar transcription started as an educational tool to understand why the nascent Hemlock
grammar was causing generation performance problems for IELR⁺(1), and perhaps to learn how my
Expand All @@ -673,9 +672,9 @@ implementation is certainly nothing alike.
The case for deploying automata generated by APLR(1) is open-and-shut; no room for significant
improvement remains. What about generation performance though? The experiments presented in this
report include some extreme cases, in particular the OCaml grammar with conflict resolution
disabled. However, the egregiously long generation times do not correspond to practical use cases.
Thus far during actual use, the only grammar that posed a problem was a stripped-down subset of
OCaml with no precedence/associativity yet specified. In that case the workaround was to revert to
disabled. However, the egregiously long generation times do not correspond to common use cases. Thus
far during actual use, the only grammar that posed a problem was a stripped-down subset of OCaml
with no precedence/associativity yet specified. In that case the workaround was to revert to
canonical LR(1) until enough ambiguity was removed to make APLR(1) sufficiently fast. IELR⁺(1)
offers an alternative mitigation, but it is hard to recommend implementing IELR⁺(1) just for such a
purpose.
Expand All @@ -689,16 +688,16 @@ remergeability be fully constructed (i.e. complete automaton closure). It would
automaton closure from within the subgraph remergeability search (though this would remain a
non-monotonic automaton construction algorithm), and for most grammars this would avoid the need to
ever manifest the full canonical LR(1) automaton. However, cyclically complex automata such as that
for the unresolved OCaml grammar would see little practical benefit. Nonetheless, this could be
advantageous in the common case, but this remains open research because Hocc would require
significant refactoring to close on states rather than state nubs.
for the unresolved OCaml grammar would see little benefit. Nonetheless, this could be advantageous
in the common case, but remains open research because Hocc would require significant refactoring to
close on states rather than state nubs.

The impetus for APLR(1) stemmed from IELR⁺(1), first as a mitigation for unnecessary state splits,
later out of desperation for a simpler algorithm. The pair-at-a-time remerging algorithm
The motivation for APLR(1) stemmed from IELR⁺(1), as a mitigation for unnecessary state splits, as
well as from despair over implementation complexity. The pair-at-a-time remerging algorithm
[^evans2024][^lenka2006] was trivially derived from analysis of Hocc's precautionary splits for the
Gpic grammar, versus the Bison-generated automaton which lacked the splits. Having implemented that
limited remerging algorithm, the possibility of a more general algorithm immediately came to mind,
and others have independently reached this same epiphany. François Pottier added a `TODO` note (in
and others have independently made this observation. François Pottier added a `TODO` note (in
French) to the [Menhir source repository](https://gitlab.inria.fr/fpottier/menhir/) in 2015; quoted
below is a later version of the note as it was translated to English and demoted to a `TODO-NOT!`,
with a cautionary performance addendum:
Expand All @@ -708,16 +707,16 @@ with a cautionary performance addendum:
> bit costly: 8 seconds for OCaml's grammar.)

So the concept of of APLR(1) was independently formulated long before Hocc came along. It is an
unfortunate coincidence that the OCaml grammar may be the most extreme practical adversarial test
unfortunate coincidence that the OCaml grammar may be the most extreme uncontrived adversarial test
case for APLR(1) in existence.

As obvious as APLR(1) is in concept, the algorithmic details only seem obvious in retrospect.
As obvious as APLR(1) is in concept, the algorithmic details only seem obvious to me in retrospect.
Remergeability was clearly a [boolean satisfiability
problem](https://en.wikipedia.org/wiki/Boolean_satisfiability_problem), but it took years to make
the key observation that efficient implementation is possible via transitive subgraph equivalence
testing. Incidentally, this insight came while working on precise automaton tracing garbage
collection, which is related only in that it also requires a thorough understanding of automaton
graph structure. LR(1)-family automata are fundamentally [deterministic finite automata
the key observation that efficient implementation is possible via composable transitive subgraph
equivalence testing. Incidentally, this insight came while working on precise automaton tracing
garbage collection, which is related only in that it also requires a thorough understanding of
automaton graph structure. LR(1)-family automata are fundamentally [deterministic finite automata
(DFAs)](https://en.wikipedia.org/wiki/Deterministic_finite_automaton), but PDA-related nuances
complicate practical algorithms.

Expand All @@ -739,12 +738,12 @@ parsers were too confusing to make sense of until APLR(1) results agreed with IE
grammar.

It is difficult to build high confidence in IELR⁺(1) results because bugs can manifest as missing
state splits, and there is no practical way to spot these omissions without APLR(1). Aside from
APLR(1) being conceptually much simpler than IELR⁺(1), APLR(1) bugs tend to be messy and loud,
whereas IELR⁺(1) bugs tend to be clean and silent. APLR(1) stabilized within days; IELR⁺(1) took
four years and the advent of APLR(1) to stabilize. In terms of implementation complexity, line
counts are a limited proxy measure, but the Hocc implementation of IELR⁺(1) requires ~1600 lines of
code (LOC), whereas APLR(1) requires ~700 LOC.
state splits, and there is no easy way to spot these omissions without APLR(1). Aside from APLR(1)
being conceptually much simpler than IELR⁺(1), APLR(1) bugs tend to be messy and loud, whereas
IELR⁺(1) bugs tend to be clean and silent. APLR(1) stabilized within days; IELR⁺(1) took four years
and the advent of APLR(1) to stabilize. In terms of implementation complexity, line counts are a
limited proxy measure, but the Hocc implementation of IELR⁺(1) requires ~1600 lines of code (LOC),
whereas APLR(1) requires ~700 LOC.

What should an aspiring LR(1)-family parser generator author with limited time implement in 2026?
Opinions may differ, but the correct answer is APLR(1).
Expand Down
Loading