diff --git a/doc/reports/aplr1/aplr1.md b/doc/reports/aplr1/aplr1.md index 6dcee7124..3659cb9cc 100644 --- a/doc/reports/aplr1/aplr1.md +++ b/doc/reports/aplr1/aplr1.md @@ -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, @@ -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 @@ -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 @@ -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 @@ -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. @@ -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: @@ -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. @@ -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).