packages feed

rtk-0.12: CHANGELOG.md

# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.12] - 2026-07-02

### Changed
- Widened GHC support: the boot-library bounds now span GHC 9.4 through 9.14
  (`base < 4.23`, `template-haskell < 2.25`, `containers < 0.9`,
  `time < 1.16`). The endpoints are CI-tested — the pinned 9.4.7 toolchain
  runs the full battery and the newest-GHC canary builds `-Werror` and runs
  the cabal suites on 9.14.1. 0.11 capped `base` at 4.19 (GHC 9.6), which
  made the package uninstallable on current compilers and failed Hackage's
  own doc builder (GHC 9.8). Version bounds are now also stated once per
  dependency instead of repeated across components, per Hackage guidance —
  the executable's own stale `base < 4.19` copy is exactly the drift this
  prevents

### Added
- Block layout for generated pretty-printers (task 9b), the "pretty" layer on
  top of 9a's correct-not-pretty printer. `rtk --generate-pp --pp-layout=block`
  emits a `<Name>PP.hs` that indents and line-breaks bracket-structured
  languages so the output reads like hand-written source; `--pp-layout=flat`
  (the default) keeps 9a's one-line-per-construct output byte-for-byte.
  Indentation is derived purely structurally from *block lists* — statement /
  declaration lists (an `STMany` with no separator over a statement-shaped
  element, or a `;`-separated list) — which wrap their elements in an
  indented, line-broken block. The flanking delimiters (`{` `}` for C/Java,
  `begin` `end` for PL/0) need no special handling and there is no bracket
  charset baked into rtk: they render inline and the list supplies the indent,
  so a grammar with no block lists simply degrades to flat. The generated
  module stays dependency-free (it carries an in-module ~20-line layout
  engine; no `pretty`/`prettyprinter` pulled into users' code).

  Correctness is inherited and unbreakable: layout is whitespace, every corpus
  grammar ignores whitespace, so block layout cannot change the parse —
  `parse (print ast) == ast` holds by construction. The round-trip test
  (`make test-pp`) gains block-mode cases (a small bracket grammar `block.pg`
  and the c-compiler tutorial `c.pg`) that stay green, and `block.pg` opts into
  a block-mode PP golden (`test/golden/block/BlockPP.hs`, gated through
  `TestSupport.ppGoldenGrammars`) so the indented output is pinned and
  reviewable. A forward guard (`GenPP.layoutSensitive`, `TODO(#95)`) falls back
  to flat should a layout-sensitive (offside) lexer ever land (task 12); it is
  a no-op today.

  Honest scope — this is heuristic readability for bracket/terminator
  languages, not universal beauty. `c.pg` and `block.pg` read idiomatically;
  PL/0 reads well bar a minor wart (its top-level `procedure` list, being a
  no-separator block list, indents one level); Java's statement/class bodies
  indent correctly but its top-level preamble (package/import/type decls,
  which are not a single broken list) runs together and multi-line doc-comment
  tokens disturb the indentation, so Java is best left on flat for now. All of
  these still round-trip green in block mode — only quality varies, never
  correctness. Alignment, operator-aware wrapping and configurable width are
  explicitly out of scope (later opt-in layers).
- Pretty-printer generation (task 9), the "emit" third of "Rewrite ToolKit".
  `rtk --generate-pp <grammar> <out>` emits an opt-in fifth artifact
  `<Name>PP.hs`: a module of `pp<Type>` functions that turn the AST RTK
  generates for a grammar back into source text. `--debug-pp-spec` dumps the
  printer to stdout. The generated module depends only on `base` (it imports
  its grammar's `<Name>Parser` AST and `Data.List`) — no rtk imports, no new
  packages, the same dependency discipline as the generated lexer/parser/
  quasi-quoter. The flag is off by default, so the default invocation is
  byte-for-byte unchanged and non-users see zero golden churn.

  v1 is correctness-first, not pretty: exactly one space between tokens, no
  indentation or alignment, and parenthesization is whatever the grammar's
  own structure produces (no precedence/paren-insertion engine). The single
  guarantee is the semantic round-trip `parse (print ast) == ast` — NOT
  byte-faithful reproduction; comments and the original whitespace are lost
  (the AST is lossy). That guarantee is enforced by a generated round-trip
  test over a corpus (`make test-pp`, wired into CI): it parses each fragment,
  prints, reparses, and asserts equality. 7b's position-transparent `RtkPos`
  makes the reparsed AST (different source positions) compare `==` to the
  original with no stripping, so under-parenthesization or a dropped token
  becomes a failing test rather than a silently wrong program. The grammars
  `p` and `sandbox` opt in to a PP golden snapshot (`<Name>PP.hs` in
  `test/golden/`, gated through `TestSupport.ppGoldenGrammars` and compiled
  by `make test-compile-goldens`); every other grammar stays opted out.

  Dogfooding capstone: the printer RTK generates for its own grammar language
  (`grammar.pg`) compiles and round-trips representative grammar ASTs — string
  literals, regexes, typed/func/lifted rules, repetition/option/delimiter
  forms and an imports block — so **RTK prints its own language**. Known v1
  limitation: `grammar.pg`'s *own source* does not fully round-trip, because
  its `str`/`regexplit`/`bigstr` rules use repetition over an
  un-parenthesized alternation (e.g. `([^\\'] | backslash .)*`), which a
  structural printer without a paren engine re-associates on reparse. This is
  precisely the under-parenthesization the round-trip oracle is built to
  catch; auto-parenthesization is a deliberately separate later task.

  Supersedes #84 (predated the 8a–8c AST migration and pursued a "smart"
  layout-hint design out of scope for the correctness-first v1).

### Changed
- The pipeline computes directly over the generated AST (task 8c): the
  historic `InitialGrammar`/`IRule`/`IClause` types — the hand-written
  parsed-grammar representation dating back to the original `Parser.y` —
  are retired. String-literal normalization, clause normalization and the
  lexical-clause translation in GenX now consume `GrammarParser`'s
  `Grammar`/`Rule`/`Clause` (compiled from the `test/golden/grammar/`
  snapshot), the same types the compiled-in `GrammarQQ` quotes produce, so
  normalization-level rewrites can be expressed as quasi-quoted patterns
  (task 8d). The hand-written reference front end stays as the equivalence
  oracle: `Parser.y`'s actions construct generated-AST values (same
  constructors, spines and first-symbol positions), and the AST-equality
  suite now also projects and compares the position of every node, since
  `RtkPos` is equality-transparent. The `ASTAdapter` conversion layer is
  replaced by `src/generated/Frontend.hs` (parse entry points, generated-AST
  helpers, and the now-shared token-text cleanup: both lexers keep raw token
  text and a single `cleanGrammarTokens` pass strips delimiters and
  processes escapes after parsing). Generated artifacts are byte-identical
  for every corpus grammar. The bootstrap coupling is now structural — a
  grammar.pg change that reshapes the AST breaks the in-tree build until the
  snapshot is re-accepted and the pipeline's matches are fixed; see
  "Changing the grammar language" in BOOTSTRAP.md
- Normalization diagnostics point at the offending CLAUSE instead of the
  rule header: the generated AST carries a source position on every node,
  so errors like "a lifted (,) clause cannot be mixed with other clauses",
  "repetition over the lexical rule ...", and the constructor-label checks
  (case, reserved prefixes, duplicates — including the "first used at"
  cross-reference) now report the line and column of the clause or label
  itself, still with the rule named as context

### Added
- Rewrites as quasi-quoted patterns (task 8d) — the headline: the "rewrite
  toolkit" finally means it. Every generated `<Name>QQ` now comes with a
  documented, tested rewrite recipe ("Rewriting parsed Java" in
  docs/java-quasi-quotation-tests.md, exercised by `make
  test-java-rewrite`): a rewrite is an ordinary function whose match arms
  are quasi-quoted patterns and whose results are quasi-quoted
  expressions, applied to every node of any AST value with SYB's
  `everywhere`/`extT` — the packages generated code already depends on, so
  there is no new API surface and no change to generated artifacts. The
  same patterns drive queries via `everything`/`mkQ`. rtk eats this
  cooking: its own pipeline now matches clause shapes with its own
  compiled-in quoter — `Frontend.altElems`/`seqElems` flatten the `'|'`
  and juxtaposition spines by matching `[clause| $cl1 | $cl2 |]` /
  `[clause| $cl1 $cl2 |]`, `StringLiterals` picks string literals out of
  syntax rules with `[clause| $StrLit:s |]`, and `Normalize`'s
  repetition/option shaping matches `[clause| $cl1 * ~ $cl2 |]`-style
  patterns — each conversion landed golden-neutral (artifacts
  byte-identical, bootstrap fixed point intact). Steps where the
  constructor spelling reads better (wildcard tests, scalar-`Name`
  binders, state-threading plumbing) deliberately stay plain; the
  conversion record and the discovered pattern-matching boundaries live
  in docs/qq-grammar-rewrites-plan.md §8d
- Named constructors (task 8a): an alternative may carry a leading label —
  `Expr = Add: Expr '+' Term | Term ;` — that names its generated AST
  constructor (`Add RtkPos Expr Term`) instead of the positional
  `Ctr__<Rule>__<index>` default, so code and quasi-quote patterns written
  against generated ASTs survive alternative reordering and insertion. The
  label binds tighter than `|`, scopes over one alternative's sequence, and
  works inside parenthesized groups (naming the extracted group's
  constructor, including under repetition through the list-proxy
  machinery). Unlabeled alternatives keep today's generated names
  byte-for-byte. Misuse is rejected with positioned diagnostics: names
  must start uppercase, the generated-name prefixes `Ctr__`/`Anti_` are
  reserved, explicit names must be unique across the whole grammar (all
  constructors share one generated Haskell module, and the shared-type
  constructor deduplication in GenAST would otherwise silently merge
  duplicates), and lifted (`,Rule`) alternatives — which produce no
  constructor — cannot be named, nor can anything in a lexical rule.
  Surface syntax chosen over a trailing `@ctor(Name)` annotation after
  proving LALR feasibility: the labeled grammar.pg's generated parser has
  0 happy conflicts (as before) and the reference `Parser.y` keeps exactly
  its 4 pre-existing state-6 reduce/reduce conflicts
- The generated quasi-quoter for RTK's own grammar language (`GrammarQQ`) is
  compiled into rtk, beside `GrammarLexer`/`GrammarParser` in the snapshot
  front-end component — possible since 0.11 made generated quasi-quoters
  dependency-light (`template-haskell`, a GHC boot library, is the only
  dependency the quasi-quoter adds). Grammar fragments can now be quoted
  in-tree, and the unit suite smoke-tests it: `[clause| Name * |]` parses at
  rtk's own compile time, `$cl`-style metavariables splice and bind through
  the `Anti_*` constructors in both expression and pattern context. The
  quotes produce the generated AST types, not the pipeline's
  `InitialGrammar`; migrating the pipeline onto the generated AST so
  normalization rewrites can be quasi-quoted is the follow-up (tasks 8c/8d
  in docs/qq-grammar-rewrites-plan.md)
- PL/0 compiler tutorial (`tutorials/pl0-compiler/`): the language of Brian
  Callahan's "Let's write a compiler" tutorial series, at the series'
  parts 1-3 ("validator") milestone. Baseline Wirth PL/0 — empty statements
  via `StatementOpt`, unary signs, `{ ... }` comments — generated from
  `pl0.pg` into a conflict-free Happy parser that is byte-identical under
  both front ends. Ships a validator driver (`Main.hs`), a valid/invalid
  test corpus with positioned-diagnostic checks (`run_tests.sh`), and a
  quasi-quotation test suite (`TestQQ.hs`) exercising construction,
  splicing, pattern matching with metavariables (a miniature PL/0 → C code
  generator) and SYB-based AST rewrite rules; wired into CI via
  `make -C tutorials/pl0-compiler test`
- Lisp interpreter tutorial (`tutorials/lisp-interpreter/`): Peter
  Norvig's lis.py ported to RTK — the reader is generated from a 15-line
  grammar (`scheme.pg`), and the interpreter's special-form dispatch and
  macro expansion (`let`, `when`, `unless`, `and`, `or`) are written as
  quasi-quotation patterns; `make -C tutorials/lisp-interpreter test`
  (wired into CI) runs Norvig's own test cases plus the examples, and the
  tutorial's README is a section-by-section walkthrough against the
  original essay

### Changed
- grammar.pg names every constructor-producing alternative (`RuleSimple`,
  `Star`, `Labeled`, `Ref`, ...), so the generated grammar front end's AST
  reads like prose and `src/generated/ASTAdapter.hs` no longer references
  any positional `Ctr__*` constructor — the ergonomics prerequisite for
  retiring `InitialGrammar` (task 8c). The two `?`-rules whose
  empty/present alternatives are synthesized (and therefore unnameable) —
  `ImportsOpt` and `OptDelim` — are restructured away: the imports block is
  inlined into `Grammar`'s two alternatives (`GrammarDef`/`GrammarImports`)
  and the `~` delimiter into `Star`/`StarDelim`/`Plus`/`PlusDelim`. The
  accepted language is unchanged; only grammar.pg's own AST shape moved

### Fixed
- The reference lexer (`Lexer.x`) now accepts underscores in identifiers,
  as grammar.pg's `id = [a-zA-Z][A-Za-z0-9_]*` has always specified; it
  used to stop at the first `_` with a lexical error while the generated
  (default) front end accepted the name — an invisible front-end
  divergence until label names like `Mk_1` exercised it

## [0.11] - 2026-06-12

Highlights: RTK is now self-hosting by default — grammar files are parsed
with the front end RTK generated from its own grammar description,
`test-grammars/grammar.pg` is the authoritative definition of the grammar
language, and the hand-written front end is a reference oracle selected by
`--use-handwritten`. Errors are structured, positioned diagnostics rendered
in GNU one-line style; generated ASTs carry equality-transparent source
positions. A reworked test architecture — golden snapshots, dual-front-end
equivalence, a compile gate over every snapshot — enforces all of it.
Quasi-quoters gained a `$$` escape and honest context errors; packaging
gained PVP bounds; CI a pinned, cached toolchain.

### Added
- Early-warning CI job (`ghc-latest`): builds with `-Werror` and runs the
  cabal test suites once against the newest GHC series the CI runner image
  ships (currently 9.14), relaxing only rtk's own bounds on the GHC boot
  libraries. Warnings a new GHC promotes into `-Wall` (or turns on by
  default) now surface on every push instead of at the next toolchain bump;
  the pinned primary job stays authoritative. rtk's own code is kept
  warning-clean portably: pattern matches instead of `head` (9.8 warns on
  every `head`/`tail` use, `-Wx-partial`), and the no-op `Typeable`
  derivings dropped (9.14 promotes `-Wderiving-typeable` into `-Wall`;
  explicit `Typeable` deriving has been meaningless since GHC 7.10). The
  compiled-in generated snapshot gets a scoped, GHC-gated `-Wno-x-partial`
  (generated code is not edited to placate
  linters - the lexer generator emitting `drop 1` instead of `tail` is
  tracked as follow-up work, since it churns every golden)
- Provenance banner: every generated artifact (`<Name>Lexer.x`,
  `<Name>Parser.y`, `<Name>QQ.hs`) now opens with `-- Generated by RTK from
  grammar '<Name>'. Do not edit by hand.` The banner deliberately names the
  grammar rather than the grammar file (a path would make the output - and
  the golden snapshots - depend on where the grammar lives) and carries no
  rtk version (a version would couple every release bump to a full golden
  churn)
- `make test-compile-goldens`, wired into CI after the cabal test suites:
  runs `alex -g` and `happy --ghc` over every checked-in golden
  `<Name>Lexer.x`/`<Name>Parser.y` pair and typechecks the result with
  `ghc -fno-code`. The golden suite diffs text only and sat green while the
  debug-test and t1 snapshots did not compile; the gate closes that
  test-architecture hole (the `<Name>QQ.hs` goldens are gated too, since
  generated quasi-quoters dropped their regex dependency — see Fixed)
- Source positions in generated ASTs: every constructor that generated
  parsers build (except the quasi-quotation-only `Anti_*` splice
  constructors) now stores the position of its alternative's first symbol
  in a leading `RtkPos` field. `RtkPos` is transparent for equality and
  ordering (`==` always holds, `compare` is always `EQ`), so ASTs that
  differ only in source positions compare equal — in particular a
  quasi-quote parsed at compile time still matches the same construct
  parsed at run time, and quasi-quote *patterns* wildcard every position
  field while *expressions* may embed them. Payload-carrying `%token`
  bindings now bind the whole positioned token, with generated `tkVal_*`
  extractors recovering the payload in semantic actions; a generated
  `RtkPosOf` class projects the position of any symbol (tokens,
  nonterminals, lists, optionals). Generated code stays dependency-free.
  The self-hosted front end now maps the rule constructors' positions into
  `getIRulePos` (previously `Nothing` under `--use-generated`), so
  pipeline diagnostics carry real `FILE:LINE:COL:` positions with both
  front ends, and the dual-front-end AST equality suite compares positions
  too. The hand-written parser's position for the `'.' id ':' …` rule form
  moved from the identifier to the leading dot (a rule's position is where
  the rule starts), aligning it with first-symbol capture; no corpus
  grammar uses that form
- `--debug-rule RULENAME` is back, this time with a real implementation
  (it was removed together with the other never-implemented placeholder
  flags): it traces one rule through the pipeline, printing only that
  rule's representation after each stage — its mentions in the token
  stream (with positions), the matching `IRule`s after parsing and after
  string normalization (making literal-to-`!tok_*` rewrites visible), and
  the matching rule groups/lexical rules after clause normalization and
  constructor filling — instead of full-grammar dumps. `--expand-rule`
  shows a rule's final expanded form; this flag shows its evolution. When
  the rule is missing at a stage the trace says so and lists up to five
  case-insensitive near matches (normalization renames things — `Rule_N`,
  `ListElem_*`, `tok_*`); a rule found at no stage at all fails the run
  with exit code 1, so typos don't go unnoticed in scripts. Composes with
  `--debug-stage` (stop early) and works under `--use-generated`, where
  the token stage is internal to the generated front end and the trace
  starts after parsing
- Self-hosting milestone (Prototype 2 closed): `rtk --use-generated` parses
  grammar files with the lexer/parser RTK generated from
  `test-grammars/grammar.pg`. The generated modules are compiled into rtk
  straight from the checked-in golden snapshot (`test/golden/grammar/`, the
  bootstrap stage produced by the previous rtk binary, kept current by `make
  accept-golden`), and `src/generated/ASTAdapter.hs` converts the generated
  AST to the hand-written `InitialGrammar`; everything after parsing is the
  same shared pipeline. The golden suite now runs every grammar in
  `test-grammars/` through BOTH front ends and requires byte-identical
  artifacts, and the unit suite asserts AST equality (positions stripped)
  for every grammar. The fixed point holds: `rtk --use-generated
  test-grammars/grammar.pg out/` regenerates `test/golden/grammar/` exactly.
  Accepted divergences (documented in BOOTSTRAP.md): errors carry positions
  in the message text rather than structured diagnostics, `getIRulePos` is
  not captured (both converge in task 7b), no nested `/* /* */ */` comments
  (#25), no concatenation of adjacent `"""…"""` blocks. The harness also
  surfaced two constructs the hand-written parser accepts beyond
  grammar.pg's own definition of the language — empty alternatives
  (`Gd = | ExpI ;` in haskell.pg) and redundant parentheses kept as
  semantic grouping (`(ImportStatement)*` in java.pg, `(A B) C` in t1.pg) —
  so those three grammars are pinned in the test suites
  (`frontEndDivergentGrammars`) with a guard that fails once they stop
  diverging; resolving which front end defines the language is follow-up
  work
- Generated parsers, lexers and quasi-quoters report errors with `Either`
  instead of throwing (the deferred "Stage G" of the diagnostics
  migration): generated parsers use `%monad { Either String }`, so
  `parse<Name> :: [PosToken] -> Either String <AST>` mirrors the shape of
  the hand-written grammar parser; generated lexers expose
  `scanTokens :: String -> Either String [PosToken]` (`alexScanTokens`
  stays as a throwing compatibility wrapper); and the generated
  quasi-quoters route lexer, parser and unknown-metavariable failures
  through `fail` in `TH.Q`, so a bad quasi-quote is now a positioned GHC
  compile error at the splice site instead of a runtime crash during
  Template Haskell expansion. Generated code stays dependency-free
- Duplicate rule definitions are rejected: defining the same rule name twice
  in a grammar is now a normalization error carrying both source positions
  (e.g. `g.pg:3:1: error: in rule 'Foo': rule 'Foo' is defined more than
  once (first definition at line 2, column 1)`), instead of the definitions
  being silently merged into one rule group with extra alternatives
  (closes #20). `test-grammars/debug-test.pg`, the one grammar that relied
  on this, now defines `IfStatement` once with `|` alternatives
- A `$$` escape in quasi-quote bodies: `$$name` now produces the literal
  text `$name` instead of being rewritten as a metavariable (or rejected).
  Previously the generated quasi-quoter rewrote `$ident` everywhere in a
  quote body — including inside the quoted language's own string literals —
  mangling programs. Each `$$` pair directly before a metavariable stands
  for one literal `$` (so `$$$x` is a literal `$` followed by the
  metavariable `$x`). The unknown-metavariable error now names the
  offending `$name`, lists the known shortcuts and points at the `$$`
  escape (see `docs/why-qq-limitations.md`)
- Hackage packaging hygiene: PVP version bounds on all dependencies and on
  the `alex`/`happy` build tools, `Tested-With` now lists GHC 9.4.7 and
  9.6.4 (the versions actually exercised locally and in CI), and the
  packages required by RTK-generated code are documented in the README and
  the cabal description
- Structured diagnostics: grammar errors are now `Diagnostic` values
  (message plus optional source position and context) threaded through
  `Either`, instead of being thrown with `error`. The grammar-processing
  pipeline functions (`scanTokens`, `parse`, `normalizeTopLevelClauses`,
  `genX`/`genY`/`genAST`/`genQ`) are total for user-caused failures
- One-line GNU-style error reporting: `rtk` prints `FILE:LINE:COL: error:
  MESSAGE` to stderr and exits 1 for a bad grammar, with no Haskell exception
  or call stack (closes #23 and #31)
- A lifted (`,`) clause under `*`/`+`/`?` (e.g. `Foo = ,Bar* ;`) is now
  rejected with a clear error during normalization, instead of silently
  generating broken code
- A reference to an unknown rule now names both the unknown rule and the type
  that references it
- Source positions on tokens: the lexer now returns `PosToken` values
  (token plus line/column), both in the hand-written lexer and in all
  generated lexers
- Parse errors report line, column and a human-readable description of the
  unexpected token (e.g. `Parse error at line 2, column 1: unexpected
  identifier 'Foo'`); generated parsers report positions too instead of
  dumping the remaining token list
- Errors at end of input carry the position where the input ended, in both
  the hand-written and generated parsers
- Grammar normalization errors name the offending rule and its source
  position (e.g. `Grammar error in rule 'Foo' (at line 2, column 1): ...`)
- Lexer-generation errors name the lexical rule they occur in

### Changed
- **RTK is now self-hosting by default — the headline of this release.**
  Grammar files are parsed with the front end RTK generated from its own
  grammar description, and `test-grammars/grammar.pg` is the authoritative
  definition of the grammar language. The hand-written `Lexer.x`/`Parser.y`
  are demoted to a reference oracle selected with the new
  `--use-handwritten` flag (`--use-generated` remains accepted and is now a
  no-op); the equivalence harness keeps holding both front ends to identical
  artifacts and equal ASTs for every corpus grammar, and the bootstrap fixed
  point now holds for a *default* invocation: `rtk test-grammars/grammar.pg
  out/` reproduces `test/golden/grammar/` byte-for-byte. Changes to the
  grammar language land in grammar.pg (plus regenerated goldens) first; the
  hand-written files follow only to keep the harness green. See BOOTSTRAP.md
- Front-end error parity (prerequisite of the default flip): generated
  lexers and parsers now encode failures as machine-splittable
  `LINE:COL:message` (previously prose like `Parse error at line L, column
  C: …`), the shared `Diagnostics.diagnosticFromPositioned` splits the
  encoding into a positioned diagnostic, and rtk renders the same GNU-style
  `FILE:LINE:COL: error: …` line under both front ends — for lexical errors
  the stderr line is identical character for character; for parse errors the
  position is identical and only the token wording differs (generated
  parsers render tokens generically). Generated quasi-quoters re-render the
  encoding human-readably (`line L, column C: …`) in their `fail` path, as
  do the standalone test drivers. Every golden `<Name>Lexer.x`/`<Name>Parser.y`
  changed uniformly with the new error templates
- The two historic front-end divergences are resolved and the
  pinned-divergence list (`TestSupport.frontEndDivergentGrammars`) is empty:
  the hand-written reference parser now defines the same language as
  grammar.pg — it rejects empty alternatives (`R = | X ;`) and lifts
  redundant parenthesis groups exactly like grammar.pg's
  `Clause5 = '(' ,Clause ')'` (single-leaf groups collapse, pure-sequence
  groups merge at the head of a sequence, alternation groups merge as the
  first alternative); haskell.pg's `Gd = | ExpI ;` was rewritten as the
  equivalent `Gd = ExpI? ;`. java.pg, t1.pg and haskell.pg artifacts changed
  accordingly (fewer proxy sub-rules, e.g. java.pg's `(ImportStatement)*`
  now generates a plain list rule)
- The core pipeline data types (`InitialGrammar`, `IClause`,
  `NormalGrammar`, …) moved from the footer of the hand-written `Parser.y`
  into the new front-end-agnostic `Syntax` module; a demoted parser no
  longer owns the project's core types. Pure move, no behavior change
- `--debug-tokens` in the default (generated) front end now dumps the
  generated lexer's token stream (it previously printed nothing under
  `--use-generated`)
- The Java grammar parses exactly ONE `ModifierList` per declaration. The
  class/interface/enum/`@interface` rules no longer begin with their own
  nullable list; `TypeDeclRest` (ex-`NestedTypeDeclaration`, now used at the
  top level too) carries everything after the modifiers, and the list lives
  on the enclosing rule: `TypeDeclaration = OptDocComment ModifierList
  TypeDeclRest`, `FieldDeclaration = OptDocComment ModifierList
  (MemberDeclaration | TypeDeclRest | StaticInitializer) | ';'`. The nested
  nullable lists used to produce 14 shift/reduce conflicts (after the outer
  list, every modifier/annotation token chose between extending it and
  epsilon-starting the inner one) whose shift resolution parsed
  `public class A {}` with `public` on the OUTER list and a confusing,
  always-empty inner list; that always-empty `ModifierList` field is gone
  from the class/interface/enum/annotation AST constructors, and the
  generated quasi-quoter renames `nestedTypeDeclaration` to `typeDeclRest`.
  Together with the restored dangling-else conflict (see Fixed) the
  generated `JavaParser.y` is down to 18 shift/reduce + 0 reduce/reduce
  conflicts (from 32 + 0), and java.pg now opens with a complete inventory
  of all 18 - each family with its automaton items and why the shift is the
  correct Java reading (dangling else; catch/finally attach to the nearest
  try; member id vs empty TypeParameters; greedy CompoundName `.`;
  bracket-list `[` shifts; `<` commits to type arguments; the QQ bootstrap
  dummy bracket) - replacing the stale "~14"/"~13"/"32" conflict comments.
  The commons-lang corpus success sets are unchanged (14/16 main/tests
  files), so the parser blacklists stay as they are
- The makefile test pipeline now runs alex with `-g` (the GHC backend), so
  generated lexers in `test-out/` compile as compact string-encoded tables
  instead of ~half-a-million-line pattern matches. GHC's compile of the
  Java lexer drops from minutes to seconds with identical token streams
  (verified against the `test-lex-java` goldens). The library's own lexers
  already used `-g` via cabal's preprocessor; this closes the gap for the
  makefile-driven tests (#27)
- The `$Type:var` splice alternative is now attached to a minimal set of
  rules of a shared-type group instead of to every rule. The normalizer
  builds the group's lift graph (rule A → rule B when B has an alternative
  that is exactly the single nonterminal A, ignoring nullable clauses) and
  greedily picks attach points whose unit-closure covers every rule the
  grammar demands from some position; a splice token reduces at an attach
  point and climbs the chain to the level its position requires. For
  java.pg's 18-rule Expression chain the single attach point is
  `PrimaryNoPostfix`, which removes the 806 reduce/reduce conflicts
  (of 883) the per-rule alternatives used to cause, along with the three
  "rule ... is unused" happy warnings, and makes the parse of a splice
  independent of conflict-resolution accidents. Types declared by a
  single rule are unaffected. A
  group whose demanded rules cannot be covered from one attach point gets
  several; if their closures overlap, the overlap can reintroduce
  reduce/reduce conflicts between the splice reductions (inherent to such
  grammars; normalization has no warning channel to report them)

### Removed
- The `--debug-format` option: its only honest format was the `pretty`
  default - `compact` merely stripped newlines into an unreadable single
  line (and the advertised `json`/`tree` formats had already been removed
  as never-implemented). Debug output is always pretty-printed now;
  `DEBUG_OPTIONS.md` and `test-debug-options.sh` follow
- The orphan `PrintGrammar` module: a dead pretty-printing draft predating
  the current pipeline, listed in the cabal file but imported by nothing
  (pretty-printing returns properly as a tracked feature)
- The makefile's `Windows_NT` branch: its `dist/build/rtk/rtk.exe` was the
  cabal-v1 path, dead for years - the makefile is Unix-only and now says so
- The superseded textual bootstrap comparison: `compare-bootstrap.sh`, the
  `make test-bootstrap` target and its informational CI step. Behavioral
  equivalence of the two front ends is enforced by the golden/unit harness
  on every test run; textual identity of the generated `.x`/`.y` with the
  hand-written ones was never the goal
- Unimplemented CLI options that were advertised in `--help` but had no
  effect: `--debug-rule`, `--compare-stages`, `--memory-stats`,
  `--debug-output-dir`, `--debug-log`, `--interactive`, the placeholder
  `json`/`tree` debug formats, and the `--use-generated` stub that only
  printed an error

### Fixed
- Generated quasi-quoters' `quoteType`/`quoteDec` no longer lie: they used
  to silently return dummies (`TH.ListT` / `[]`), so a quote in a type or
  declaration context compiled into nonsense. Both now `fail` in `TH.Q`,
  making the misuse a GHC compile error at the splice site ("this
  quasi-quoter cannot be used in a type/declaration context")
- A `$name` metavariable at the end of a line (or of the whole quote body)
  silently stayed unrewritten, failing with a confusing parse error at the
  `$`: the generated quasi-quoter's metavariable scan used regex-posix,
  whose default newline option made the terminator class never match `\n`.
  The scan is now a plain character function with the documented semantics
  preserved (`$$` escapes, explicit `$Type:name` passthrough, the
  unknown-metavariable error). Generated QQ modules consequently import no
  regex packages at all — `regex-posix`/`regex-base` (which were never
  declared rtk dependencies and compiled only as transitive flukes) are
  gone from every user's generated-code dependency footprint, and the
  README/cabal generated-code dependency lists shrank accordingly
- Writing into a missing output directory no longer escapes as a raw
  `IOException`: `rtk g.pg /tmp/does/not/exist` now creates the directory
  and succeeds; IO failures that remain (no permission, a file where the
  directory should be) are rendered as the usual one-line GNU-style
  diagnostic and exit 1
- A literal backslash inside a `[...]` character class is now emitted
  Alex-escaped (`\` → `\\`) by the lexer generator; it used to pass through
  raw, where Alex set syntax reads it as an escape, forcing grammars onto
  the `[\x5C]` hex spelling (issue #95's smaller finding). The `\n \t \r \f
  \v` pairs that token post-processing preserves keep passing through bare.
  java.pg drops its hex workarounds for a `backslash` charset macro and a
  plain `[^\"\\\n\r]` negated class; generated Java token streams are
  byte-identical (the `test-lex-java` goldens are untouched). One
  grammar-lexer limitation remains, documented at the macro definition: a
  class ending in a backslash must also end its source line, because `\]`
  lexes as an escape pair
- `GenAST`'s unreachable backstop for lifted clauses claimed "lifted rules
  are not yet implemented" - a missing feature it is not; reworded to the
  `Internal error (GenAST): …` convention (Normalize rejects or filters
  every lifted position before AST generation)
- A type declared only through rule annotations (`Thing : Item = …` with no
  rule named `Thing`) could not be referenced: a plain `S = Thing ;`, a list
  element `Thing* ~ ','`, or merely the QQ start wrapper (which references
  every public type by name) failed generation with `reference to unknown
  rule 'Thing'`. rtk now synthesizes the cover rule that grammar authors
  wrote by hand for this (java.pg's
  `Expression : Expression = AssignmentExpression ;`), in lifted form —
  `Thing : Thing = ,Item1 | ,Item2 ;` — so the type gets a nonterminal
  without an extra AST constructor. Synthesis is demand-driven (a grammar
  that never references the bare type is generated byte-identically) and
  happens on the parsed grammar before normalization, so the cover takes
  part in the splice attach-point machinery like a hand-written one: the
  `$Thing:var` alternative lands on one annotated rule and climbs to the
  type through the cover, and the covered type gets a top-level quoter.
  LALR conflicts of the synthesized cover are exactly those of its
  hand-written equivalent (overlapping FIRST sets among the annotated
  rules remain the grammar's own, and happy still reports them). The
  remaining unresolvable case — referencing a lexical rule's *value* type,
  which declares no syntax type — keeps a diagnostic that now explains the
  type-named-rule convention. New corpus grammar `test-grammars/i14.pg`
  pins all reference shapes in the golden suite and compile gate, and
  `make test-i14-qq` (wired into CI) exercises quoters and splices against
  the covered types (#14)
- A grammar whose start rule is a repetition (`Start = Item* ;`) generated a
  parser that did not compile: the QQ start-wrapper alternatives extended
  the start group as a `data` declaration while the repetition typed the
  same nonterminal as the alias `[Item]` (plus a dummy-token shift/reduce
  conflict against the empty list). Such an alias start group now skips the
  QQ entry-point machinery entirely — no dummy tokens, wrapper alternatives
  or top-level quoters, since an alias has no constructor to project a quote
  through — while element-level `$Type:var` splices keep working. Groups
  merely *referenced* by a wrapper alternative (grammar.pg's
  `RuleList = Rule*`) were always well-typed and are unchanged. The
  debug-test golden, uncompilable until now, is regenerated (#34)
- Repetition over a terminal silently generated uncompilable code:
  `Foo = 'x'* ;` emitted a degenerate self-recursive `data Foo` against
  list-building parser actions, and `Foo = num* ;` an invalid lowercase
  `data num`. Decided as by-design and rejected during normalization with a
  positioned diagnostic: a list element must be a syntax rule, because the
  element type hosts the list's splice constructor — wrap the terminal in a
  syntax rule and repeat that rule instead. A lifted `(,X)` element under
  `*`/`+` is rejected the same way instead of crashing the generator (#28)
- The synthesized QQ start wrapper is now emitted directly adjacent to the
  rule it wraps: list-element proxy rules could separate the two same-named
  rule blocks in the generated `.y`, which happy rejects outright
  ("Multiple rules for 'A'", t1.pg's golden was affected)
- A brace-less nested `if` parses again: `IfStatement`'s then-branch was
  `StatementWithoutIf`, so valid Java like `if (a) if (b) f(); else g();`
  was rejected with "unexpected 'if'". The then-branch is a full `Statement`
  and `StatementWithoutIf` is inlined away (`Statement` absorbs its
  alternatives; nothing else referenced it). This re-surfaces the classic
  dangling-else shift/reduce conflict, which happy resolves by shifting:
  `else` binds to the NEAREST `if`, exactly the JLS 14.5 rule (verified on
  the printed AST; regression coverage via
  `test-grammars/java/test-nested-if.java` / `make test-java-nested-if` and
  a quasi-quotation construction case). The exclusion never even avoided
  that conflict - it already existed through loop bodies in then-position,
  e.g. `if (a) while (b) if (c) f(); else g();` - it only rejected the
  direct nested form
- The Java grammar's last structural LALR ambiguity - deciding between a
  type and an expression after a `CompoundName` at statement start or after
  `(` - is resolved JLS-style (`happy` now reports 0 reduce/reduce
  conflicts on the generated `JavaParser.y`, down from 2). Types and
  declarators take only empty bracket pairs (`Dims`/`NonEmptyDims`,
  anchored on the unreduced name so `[` is a plain shift); only array
  creation takes sizing expressions; `CastExpression` uses the JLS trick
  (`'(' Expression ')' UnaryExpressionNotPlusMinus` for reference casts
  plus explicit primitive/generic/array alternatives); and array access is
  anchored on `CompoundName` in `PrimaryNoPostfix`. `a[0] = 1;`,
  `x = (a);`, `x = (Foo) y;`, `x = (List<String>) y;`, `((Map) c).get(k);`
  and `new int[3][];` now parse; JLS-invalid `int[3] x;` declarations are
  now rejected. Known residual limitation (shared by LALR Java parsers
  generally): `(a < b)` as a parenthesized primary mis-commits to a
  generic cast on `<` (conditions like `if (a < b)` are unaffected)
- `make test-p` works again: the target had a hand-rolled recipe that
  compiled the driver with bare `ghc` outside the cabal package environment,
  failing with `Could not find module 'Data.Generics'` in clean
  environments. It now uses the generic `make-test-rule` like its sibling
  targets (`cabal exec -- ghc --make -itest-out ...`); the driver binary is
  consequently named `test-out/p-main` instead of `test-out/p-rtk`. The
  compile failure had masked a latent bug in the driver itself:
  `p-main.hs`'s `subst` bound `e1 = subst id e1 i` etc., self-referential
  recursive lets that shadow the pattern variables and diverge (`<<loop>>`)
  on the first fold expression, and it had no base case. The lets now bind
  fresh names and a catch-all clause terminates the recursion, so the
  driver prints the substituted AST
- `'\f'` and `'\v'` escapes in grammar string and `[...]` regex literals now
  reach the generated lexer as bare Alex escapes; previously token
  post-processing (`unBackQuote`) stripped them to the literal letters
  `f`/`v` even though `GenX.isAlexEscape` was ready to emit them, so the two
  escape sets disagreed. The preserved set (`\n \t \r \f \v`) is now pinned
  to `isAlexEscape` by a unit test
- `test-grammars/haskell.pg` is self-consistent again: minimal `Pat` and
  `QOp` rules were added for the previously dangling references, so RTK
  generation no longer aborts on it (progress on issue #30; the grammar is
  still far from full Haskell). The haskell special cases in the test suites
  were dropped and golden snapshots added
- A grammar whose first rule is lexical (or has a data-type annotation
  different from the rule name) no longer crashes with `fromJust: Nothing`;
  it reports that the first rule must be a syntax rule, or resolves the
  annotated type correctly
- Internal `fromJust` calls in code generation replaced with descriptive
  internal-error messages
- Invalid clauses in lexical-rule macros now abort generation with an error
  instead of writing the error text into the generated lexer
- User-facing errors no longer print a GHC call stack
- `--debug-stage` now exits with a success status after stopping at the
  requested stage instead of reporting failure via `error`
- `--profile-stages` timings now force each stage's result to normal form,
  so per-stage durations are no longer skewed by lazy evaluation

### Documentation
- Replaced the README "Grammar Format" example, which used Happy-style
  semantic actions that RTK cannot parse, with a verified `.pg` example
- Removed the stale `Claude.MD` (a case-colliding near-duplicate of
  `CLAUDE.md`; its Quick Reference table was folded into `CLAUDE.md`) and the
  stray root `test-simple-return.java` duplicate

## [0.10] - 2025-12-03

### Added
- MIT license with generated code exemption
- Full Java grammar support with comprehensive parsing tests
- Quasi-quotation support for embedding parsed syntax in Haskell
- Debug options for grammar development and troubleshooting
- Bootstrap self-hosting capability (RTK can parse its own grammar format)

### Fixed
- Alex escape sequence generation in GenX.hs
- Java grammar lexer patterns for complete test coverage

## [0.9] - Initial Development

### Added
- Core grammar specification format (.pg files)
- Alex lexer generation (GenX.hs)
- Happy parser generation (GenY.hs)
- AST generation (GenAST.hs)
- Quasi-quotation generation (GenQ.hs)
- Grammar normalization and transformation