commit 6a21d4a06c4b95ff4f586305994be0628b49d280
parent b40b8fb7be7f3af9feab1fcdb703b8715f854e37
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 16 Jun 2026 21:22:48 -0700
doc(gram): refresh import plan for the gramgen library shift
The standalone parser/lexer-generator being imported moved from
/Users/ryan/code/ll1 to /Users/ryan/code/gram and was renamed+extended
(ll_/llgen_ -> gram_/gramgen_/GRAM_, GRAMAPI marker). Rewrite the plan
against the real current surface and rename LLGEN_IMPORT.md -> GRAM_IMPORT.md.
- Replace the obsolete ll_* rename tables with the actual gram_*/gramgen_*/
gramregex_*/GRAM_* surface and a uniform kit_gram_/KitGram/KIT_GRAM_ scheme
(GRAMAPI -> KIT_API, GRAM_OPAQUE_TYPE -> KIT_GRAM_OPAQUE_TYPE).
- Document the new subsystems: match API, token machines (%machine),
sublexers, standalone re2c-style scanner, parser-codegen, lazy positions
(gram_pos.h), Unicode helpers, regex, sample-token/trace synthesis,
keyword extraction.
- Update file moves (split gen/ lex pipelines), driver flags, and the test map.
- Record settled decisions: full-surface one-pass import; generator imported
verbatim (renames only, no refactor); diagnostics via KitContext.diag;
UCD data vendored in-tree; drop the Python reference generator (one-time fork).
Diffstat:
3 files changed, 585 insertions(+), 391 deletions(-)
diff --git a/doc/plan/GRAM_IMPORT.md b/doc/plan/GRAM_IMPORT.md
@@ -0,0 +1,584 @@
+# Gram Import Plan
+
+This is the import plan for folding `/Users/ryan/code/gram` (the standalone
+`gramgen` project, which builds `libgram.a` + the `gramgen` CLI) into libkit and
+exposing it through the kit driver. The goal is not to make `gram` a language
+frontend. It should land as a reusable parser/lexer-generator subsystem: EBNF
+in, parse / lexer / token-machine tables out, with allocation-free push runtimes
+for generated parsers and lexers.
+
+The standalone project already has the right broad shape for libkit: explicit
+allocation (a caller-supplied `gramgen_allocator`), no hidden CLI dependency in
+the generator API, immutable generated tables, caller-owned runtime state, and a
+`GRAMAPI` visibility marker that already distinguishes the public surface. The
+import work is mostly namespace, host-boundary, build-gating, and file-layout
+discipline.
+
+**This is a one-time fork.** Once the import lands, the kit copy is the source of
+truth and the standalone `/Users/ryan/code/gram` tree is abandoned. We are free
+to rename aggressively, restructure files, and drop the Python reference
+generator entirely; we do not need to stay re-syncable with upstream.
+
+## What Shifted Since the First Draft
+
+This plan originally targeted a project called `ll1` with `ll_*` / `llgen_*`
+symbols. That project was renamed and substantially extended; the import target
+is now `/Users/ryan/code/gram`. Concrete differences that this refresh accounts
+for:
+
+- **Names are already clean.** Public symbols are `gram_*` (runtime),
+ `gramgen_*` (compiler), `gramregex_*` (single-pattern), `GRAM_*`
+ (macros/enums), tagged with the `GRAMAPI` visibility attribute. The old
+ `ll_*` rename table is obsolete; the rename tables below target the real
+ current spellings. The destination spelling is unchanged in spirit:
+ `KitGram` / `kit_gram_` / `KIT_GRAM_`.
+- **New runtime subsystems** beyond the parser/lexer:
+ - **Match API** (`gram_match_*`, `gram_matcher_*`): a re2::Set-style
+ longest-match scanning surface (`find` / `anchored` / `full` + a
+ non-overlapping match iterator) over a compiled lexer grammar.
+ - **Token machines** (`%machine`): a regular language over an *abstract
+ symbol* alphabet, compiled codegen-only to a steppable DFA
+ (`gram_fsm_*`), plus an in-memory AST-directed sampler.
+ - **Sublexers**: a grammar may declare several `%lex` blocks (one main +
+ named blocks); each is its own DFA reachable via `gramgen_lexer_*`.
+ - **Standalone scanner output**: `lexer_standalone` emits a self-contained
+ re2c-style tokenizer with the DFA table dropped (C-only), with selectable
+ `%keywords` strategy (minimal-perfect-hash by default, `fold_keywords` to
+ bake them into the DFA) and an optional `position_lazy` mode.
+ - **Lazy positions** (`grampos.h`): header-only `offset → (line, col)`
+ indexes (byte and UTF-8 flavors) so a lexer can run with positions off and
+ recover them on demand.
+ - **Parser codegen** (`parser_codegen`): an optional pull-based
+ recursive-descent parser emitted alongside the push driver, with optional
+ error recovery (`parser_recover`).
+ - **Token-stream synthesis**: the CLI can emit bounded sample token streams
+ from the parser grammar alone (`--sample-tokens`) and machine traces
+ (`--sample-traces`).
+ - **Keyword extraction**: string-literal tokens shadowed by a general "host"
+ recognizer (e.g. keywords under `IDENT`) are pulled out of the DFA and
+ resolved by a minimal perfect hash (CHD) at scan time.
+- **The split lexer-compiler pipeline**: `gen/` now has a shared NFA→DFA core
+ (`gramgen_lex_dfa.c`), an alphabet-neutral range-set NFA
+ (`gramgen_lex_range.c`), and separate byte / scalar / unicode / token
+ pipelines.
+- **The Python generator is now the *default*** in the standalone Makefile, kept
+ byte-identical to the C generator by parity tests. Because this is a one-time
+ fork, **we drop the Python path entirely** and build only the C generator;
+ generated meta tables are checked in so no generator bootstrap is needed.
+- **`NO_UNICODE` slim build**: upstream can compile out all `%lex :utf8`
+ support (`-DGRAMGEN_NO_UNICODE`), dropping the scalar/unicode pipelines and
+ the ~0.5 MB property tables.
+
+## Decision Summary
+
+- Driver command: `gram`.
+- Public compiler API header: `<kit/gram.h>` (was `gramgen.h`).
+- Public parser runtime header: `<kit/gram_parse.h>` (was `gramparse.h`).
+- Public lexer + match + machine runtime header: `<kit/gram_lex.h>` (was
+ `gramlex.h`).
+- Public lazy-position header (header-only): `<kit/gram_pos.h>` (was
+ `grampos.h`).
+- Public Unicode-helper header: `<kit/gram_unicode.h>` (was `gramunicode.h`).
+- Generated-code support headers: `<kit/support/gram_parse_tables.h>` and
+ `<kit/support/gram_lex_tables.h>`.
+- Visibility marker: fold `GRAMAPI` into kit's existing `KIT_API`
+ (`<kit/core.h>`); the `GRAM_OPAQUE_TYPE` helper macro becomes
+ `KIT_GRAM_OPAQUE_TYPE` and lives in `<kit/gram_parse.h>`. Drop `gramapi.h`.
+- Library subsystem gate: `KIT_GRAM_ENABLED`.
+- Driver tool gate: `KIT_TOOL_GRAM_ENABLED`.
+- Implementation directory: `src/gram/`.
+- Tests: `test/gram/` plus a driver smoke lane.
+- Generated meta tables checked in (`src/gram/meta_tables.c` / `.h`); no Python,
+ no network, no UCD regeneration in the normal build.
+- **First-import scope: full surface, one pass.** Wire up every subsystem
+ (parser, lexer, match API, token machines, sublexers, standalone scanner,
+ parser-codegen, lazy positions, regex) in a single landing — no
+ minimal-core-first staging.
+- **Generator imported verbatim.** `gramgen.c` lands as one `src/gram/`
+ translation unit; apply the rename scheme but do **not** restructure or split
+ it. A clean-factor pass, if ever wanted, is a separate future effort.
+- **Diagnostics route through `KitContext.diag`.** Drop the
+ `gramgen_diagnostic` out-param entirely; there is no bespoke kit error channel.
+- **UCD source data is vendored in-tree** at `data/ucd/17.0.0/` (checksummed) so
+ property-table regeneration stays offline and reproducible.
+
+`gram` / `gramgen` remain source-project names only. They should not appear in
+the libkit API, the installed command name, or user-facing help.
+
+## Boundaries
+
+`gram` is a library subsystem, not a `lang/` frontend. It does not register a
+`KitFrontendVTable`, does not emit `KitCg`, and does not participate in
+source-to-object compilation unless a future frontend chooses to use it
+internally. Its public surface is consumed by embedders, the driver command, and
+generated C files.
+
+The driver remains the only hosted layer. File reads, directory creation,
+stdout, stderr, and CLI allocation policy live in `driver/cmd/gram.c` and the
+hosted driver environment. The library accepts input as `KitSlice`, writes
+output to `KitWriter`, allocates through `KitContext.heap`, and reports
+diagnostics through `KitContext.diag`.
+
+The runtimes are allocation-free and caller-owned by construction, which already
+matches kit discipline:
+
+- Generated grammar tables (`gram_grammar`, `gram_lex_grammar`, the DFA/keyword
+ tables, machine tables) are immutable static data and are fine as globals.
+- Mutable parser, lexer, input, matcher, and match-iterator state lives in
+ caller-owned opaque, stack-allocatable objects (`gram_parser`, `gram_lexer`,
+ `gram_lex_input`, `gram_matcher`, `gram_match_iter`, …), sized by
+ `_Static_assert`-guarded `*_SIZE` constants. These rename mechanically; no
+ context struct is needed because the caller already owns every byte.
+- The match API and token-machine stepping are header-only over those tables.
+- `grampos.h` is entirely header-only `static inline`; it allocates nothing and
+ the caller owns the offset array and the indexed buffer.
+
+The one host-boundary rewrite is the *generator*, which today takes a
+`gramgen_allocator` and reports a single `gramgen_diagnostic` out-param, and
+whose CLI does its own `malloc`/`fopen`/`fprintf`/`exit`. That moves onto
+`KitContext` (heap + diag) and `KitWriter` (see Phase 3).
+
+## File Moves
+
+Import the C generator, runtime, generated meta/property tables, and tests. Do
+**not** import the Python generator.
+
+| Current file | Kit destination | Notes |
+|--------------|-----------------|-------|
+| `include/gramgen.h` | `include/kit/gram.h` | Public compiler API. |
+| `include/gramparse.h` | `include/kit/gram_parse.h` | Public parser runtime API; also hosts `KIT_GRAM_OPAQUE_TYPE`. |
+| `include/gramlex.h` | `include/kit/gram_lex.h` | Public lexer + match + token-machine runtime API. |
+| `include/grampos.h` | `include/kit/gram_pos.h` | Public header-only lazy line/col index. |
+| `include/gramunicode.h` | `include/kit/gram_unicode.h` | Public UTF-8 / Unicode scalar helpers. |
+| `include/gramapi.h` | *(dropped)* | `GRAMAPI`→`KIT_API` (from `<kit/core.h>`); `GRAM_OPAQUE_TYPE`→`KIT_GRAM_OPAQUE_TYPE` in `gram_parse.h`. |
+| `include/gramparse_tables.h` | `include/kit/support/gram_parse_tables.h` | Generated-code support, not ordinary embedder API. |
+| `include/gramlex_tables.h` | `include/kit/support/gram_lex_tables.h` | Generated-code support, not ordinary embedder API. |
+| `include/gramunicode_props.h` | `src/gram/unicode_props.h` | Private generated property-resolver interface (generation-time use). Promote to a public header only if a runtime need appears. |
+| `runtime/gramparse.c` | `src/gram/parse_runtime.c` | Implements `<kit/gram_parse.h>` (push LL(1)/Pratt driver). |
+| `runtime/gramlex.c` | `src/gram/lex_runtime.c` | Implements `<kit/gram_lex.h>` (DFA scanner + match API). |
+| `runtime/gramunicode.c` | `src/gram/unicode.c` | UTF-8 encode/decode + scalar/position helpers. |
+| `runtime/gramunicode_props.c` | `src/gram/unicode_props.c` | Checked-in generated UCD 17.0.0 property tables (~0.5 MB). |
+| `gen/gramgen.c` | `src/gram/generator.c` | Public API impl: front end, parser-table generation, C emitter (~210 KB). Imported as one file, renames only — deliberately not refactored. |
+| `gen/gramgen_ll1.c` | `src/gram/ll1.c` | LL(1), Pratt, FIRST/FOLLOW, validation. |
+| `gen/gramgen_lex_dfa.c` / `.h` | `src/gram/lex_dfa.c` / `lex_dfa.h` | Shared NFA→DFA core (subset construction, minimization, relabel). |
+| `gen/gramgen_lex_range.c` / `.h` | `src/gram/lex_range.c` / `lex_range.h` | Alphabet-neutral range-set NFA (survives `NO_UNICODE`). |
+| `gen/gramgen_lex_byte.c` | `src/gram/lex_byte.c` | Byte lexer pipeline + mode-independent lexer driver. |
+| `gen/gramgen_lex_scalar.c` | `src/gram/lex_scalar.c` | UTF-8 scalar pipeline (excluded under `NO_UNICODE`). |
+| `gen/gramgen_lex_unicode.c` | `src/gram/lex_unicode.c` | Unicode set algebra + property resolver (excluded under `NO_UNICODE`). |
+| `gen/gramgen_lex_tokens.c` | `src/gram/lex_tokens.c` | `%machine` token-alphabet (abstract-symbol) DFA pipeline. |
+| `gen/gramgen_internal.h` | `src/gram/internal.h` | Private to `src/gram/*.c`. |
+| `gen/meta.ebnf` | `src/gram/meta.ebnf` | Source grammar for the generator's own parser; not consumed by normal builds. |
+| generated meta `.c`/`.h` | `src/gram/meta_tables.c`, `src/gram/meta_tables.h` | Check in (generated with the `kit_gram_meta_` prefix) so libkit builds with no generator bootstrap. |
+| `gen/gramgen_cli.c` | `driver/cmd/gram.c` | Rewrite as a Kit driver command over public `<kit/gram.h>`. |
+| `gen/gramgen.py` | *(dropped)* | One-time fork: not imported. |
+| `tools/gen_unicode_props.py` | `scripts/gen_gram_unicode_props.py` | Regeneration helper only; not in the build. |
+| `data/ucd/17.0.0/*` | `data/ucd/17.0.0/*` | Vendored + checksummed; kept in-tree for offline, reproducible `regen-gram-unicode-props`. |
+| `test/*.c`, `test/*.ebnf` | `test/gram/*` | Library tests and fixtures (see Tests). |
+| `test/errors/*` | `test/gram/errors/*` | Parser/lexer error fixtures. |
+| `test/machine_errors/*` | `test/gram/machine_errors/*` | `%machine` error fixtures. |
+| `test/nounicode/*` | `test/gram/nounicode/*` | `NO_UNICODE`-rejection fixtures. |
+| `test/realistic/*` | `test/gram/realistic/*` | JSON / C-like / sharp-edge end-to-end grammars. |
+| `test/standalone/*` | `test/gram/standalone/*` | Standalone-scanner + sublexer fixtures. |
+
+## Public Renames
+
+The standalone `gram_*` / `gramgen_*` / `GRAM_*` names are clean and namespaced,
+but they still violate kit's enforced public-symbol discipline:
+`scripts/lib_reloc_defined_prefixes.py` relocatably links `libkit.a` and fails
+the build on any exported definition whose name is not in
+`ALLOWED_PREFIXES = ("Kit", "kit_", "KIT")`. So every public definition in the
+imported subsystem must be respelled.
+
+### Rename scheme
+
+Apply uniformly across the runtime, the generator's C emitter, and the
+checked-in generated meta tables:
+
+| Current spelling | Kit spelling | Example |
+|------------------|--------------|---------|
+| `GRAMAPI` (visibility attr) | `KIT_API` (from `<kit/core.h>`) | `KIT_API void kit_gram_parser_init(...)` |
+| `GRAM_OPAQUE_TYPE` (macro) | `KIT_GRAM_OPAQUE_TYPE` | declared in `<kit/gram_parse.h>` |
+| `gram_<snake>` / `gramgen_<snake>` *type* | `KitGram<Pascal>` | `gram_parser`→`KitGramParser`, `gramgen_compiled`→`KitGramCompiled` |
+| `gram_<snake>` / `gramgen_<snake>` *function* | `kit_gram_<snake>` | `gram_parser_init`→`kit_gram_parser_init`, `gramgen_compile_text`→`kit_gram_compile_text` |
+| `gramregex_<snake>` | `kit_gram_regex_<snake>` | `gramregex_compile`→`kit_gram_regex_compile` |
+| `GRAM_<UPPER>` (macro / enumerator) | `KIT_GRAM_<UPPER>` | `GRAM_SLOT_SIZE`→`KIT_GRAM_SLOT_SIZE`, `GRAM_PARSE_ACCEPT`→`KIT_GRAM_PARSE_ACCEPT` |
+
+Generated grammar-specific token/rule enums (`TOK_*`, `R_*`, and the
+user-chosen `<prefix>_*`) are *user* artifacts controlled by the generated
+prefix; they are not libkit exports and keep their shape.
+
+### Compiler API (`<kit/gram.h>`)
+
+| Current name | Kit name |
+|--------------|----------|
+| `gramgen_options` | `KitGramOptions` |
+| `gramgen_compiled` | `KitGramCompiled` |
+| `gramgen_diagnostic` | removed — report through `KitContext.diag` |
+| `gramgen_allocator` | removed — allocate through `KitContext.heap` |
+| `gramgen_writer` / `gramgen_write_fn` | removed — stream to `KitWriter` |
+| `gramgen_text` / `gramgen_text_free` | removed — use `kit_writer_mem` for owned in-memory text |
+| `gramgen_compile_text` | `kit_gram_compile_text` |
+| `gramgen_compiled_free` | `kit_gram_free` |
+| `gramgen_dump_sexpr_text` | `kit_gram_dump_sexpr` (writes a `KitWriter`) |
+| `gramgen_generate_c` | `kit_gram_emit_c` (writes header + source `KitWriter`s) |
+| `gramgen_parser_grammar` / `gramgen_lexer_grammar` | `kit_gram_parser_grammar` / `kit_gram_lexer_grammar` |
+| `gramgen_lexer_count` / `_name` / `_grammar_at` / `gramgen_find_lexer` | `kit_gram_lexer_count` / `_name` / `_grammar_at` / `kit_gram_find_lexer` |
+| `gramgen_token_count` / `_name` / `_display` / `gramgen_find_token` | `kit_gram_token_count` / `_name` / `_display` / `kit_gram_find_token` |
+| `gramgen_rule_count` / `_name` / `gramgen_find_rule` | `kit_gram_rule_count` / `_name` / `kit_gram_find_rule` |
+| `gramgen_machine_count` / `_name` / `_rule_count` / `_rule_name` / `_generate` / `gramgen_find_machine` | `kit_gram_machine_*` / `kit_gram_find_machine` |
+| `gramregex_compile` / `gramregex_grammar` / `gramregex_kind` | `kit_gram_regex_compile` / `_grammar` / `_kind` |
+
+The generator API takes a `const KitContext*` and `KitSlice` inputs; the
+allocator becomes `ctx->heap`; C output streams to caller-provided `KitWriter`s.
+
+Proposed core shape:
+
+```c
+typedef struct KitGramCompiled KitGramCompiled;
+
+typedef struct KitGramOptions {
+ KitSlice name; /* optional grammar name override */
+ bool multiline; /* bake newline-aware ^/$ into the lexer */
+ bool parser_codegen;/* also emit a pull-based recursive-descent parser */
+ bool parser_recover;/* error-recovery machinery in the RD parser */
+ bool lexer_standalone; /* self-contained re2c-style scanner (C-only) */
+ bool fold_keywords; /* standalone: fold %keywords into the DFA */
+ bool position_lazy; /* standalone: drop per-token line/col tracking */
+} KitGramOptions;
+
+typedef struct KitGramEmitOptions {
+ KitSlice header_path;
+ KitSlice source_path;
+ KitSlice prefix;
+} KitGramEmitOptions;
+
+KIT_API KitStatus kit_gram_compile_text(const KitContext* ctx,
+ KitSlice text, KitSlice path,
+ const KitGramOptions* opts,
+ KitGramCompiled** out);
+KIT_API void kit_gram_free(KitGramCompiled*);
+
+KIT_API KitStatus kit_gram_emit_c(const KitGramCompiled*,
+ const KitGramEmitOptions* opts,
+ KitWriter* header, KitWriter* source);
+KIT_API KitStatus kit_gram_dump_sexpr(const KitContext* ctx, KitSlice text,
+ KitSlice path,
+ const KitGramOptions* opts,
+ KitWriter* out);
+```
+
+### Parser runtime API (`<kit/gram_parse.h>`)
+
+Types: `gram_tok_kind`→`KitGramTokenKind`, `gram_rule_id`→`KitGramRuleId`,
+`gram_sem`→`KitGramSem`, `gram_token`→`KitGramToken`, `gram_error`→
+`KitGramError`, `gram_err_action`→`KitGramErrorAction`, `gram_actions`→
+`KitGramActions`, `gram_slot`→`KitGramSlot`, `gram_config`→`KitGramConfig`,
+`gram_grammar`→`KitGramGrammar`, `gram_parser`→`KitGramParser`,
+`gram_status`→`KitGramStatus`, `gram_frame`→`KitGramFrame`,
+`gram_gen_status`→`KitGramGenStatus`, `gram_gen_config`→`KitGramGenConfig`.
+
+Constants/enumerators: `GRAM_SLOT_SIZE`→`KIT_GRAM_SLOT_SIZE`,
+`GRAM_PARSER_SIZE`→`KIT_GRAM_PARSER_SIZE`, `GRAM_NEED_MORE`→
+`KIT_GRAM_NEED_MORE`, `GRAM_PARSE_ACCEPT`→`KIT_GRAM_PARSE_ACCEPT`,
+`GRAM_PARSE_ERROR`→`KIT_GRAM_PARSE_ERROR`, `GRAM_ABORT`/`GRAM_SKIP`/`GRAM_RESYNC`
+→`KIT_GRAM_ABORT`/`KIT_GRAM_SKIP`/`KIT_GRAM_RESYNC`,
+`GRAM_GEN_TOKEN`/`_DONE`/`_LIMIT`/`_ERROR`→`KIT_GRAM_GEN_*`.
+
+Functions (representative): `gram_parser_init`/`_push`/`_push_n`/`_finish`/
+`_result`/`_errors`/`_overflowed`/`_depth`/`_context`/`_expects`/`_frames`/
+`_frame`/`_ctl_hwm`/`_val_hwm`/`_rule_name`/`_tok_name`,
+`gram_parser_generate_next`/`gram_parser_generate_scratch_count`,
+`gram_stack_bounds` → all `kit_gram_*` (e.g. `kit_gram_parser_init`,
+`kit_gram_stack_bounds`).
+
+### Lexer + match + machine runtime API (`<kit/gram_lex.h>`)
+
+Types: `gram_lex_grammar`→`KitGramLexGrammar`, `gram_lexer`→`KitGramLexer`,
+`gram_lex_input`→`KitGramLexInput`, `gram_lex_input_span`→`KitGramLexInputSpan`,
+`gram_lex_input_config`→`KitGramLexInputConfig`, `gram_lex_release`→
+`KitGramLexRelease`, `gram_lex_config`→`KitGramLexConfig`, `gram_lex_status`→
+`KitGramLexStatus`, `gram_lex_error`→`KitGramLexError`, `gram_lex_segment`→
+`KitGramLexSegment`, `gram_lex_lexeme`→`KitGramLexLexeme`,
+`gram_lex_hook_result`→`KitGramLexHookResult`, `gram_lex_token_hook`→
+`KitGramLexTokenHook`, `gram_matcher`→`KitGramMatcher`, `gram_match`→
+`KitGramMatch`, `gram_match_opts`→`KitGramMatchOpts`, `gram_match_iter`→
+`KitGramMatchIter`, `gram_fsm_status`→`KitGramFsmStatus`.
+
+Constants/enumerators: `GRAM_LEXER_SIZE`/`GRAM_LEX_INPUT_SIZE`/
+`GRAM_LEX_LEXEME_SIZE`/`GRAM_MATCHER_SIZE`/`GRAM_MATCH_ITER_SIZE`→`KIT_GRAM_*`;
+`GRAM_LEX_TOKEN`/`_NEED_MORE`/`_EOF`/`_ERROR`→`KIT_GRAM_LEX_*`;
+`GRAM_LEX_HOOK_KEEP`/`_SKIP`/`_ERROR`→`KIT_GRAM_LEX_HOOK_*`;
+`GRAM_FSM_OK`/`GRAM_FSM_DEAD`→`KIT_GRAM_FSM_*`.
+
+Functions (representative): `gram_lex_input_init`/`_push`/`_finish`/`_error`,
+`gram_lexer_init`/`_next`/`_lexeme`, `gram_lex_lexeme_next`,
+`gram_lex_keyword_rewrite`, `gram_matcher_bind`, `gram_match_anchored`/`_full`/
+`_find`, `gram_match_iter_init`/`_next` → all `kit_gram_*`.
+
+### Lazy positions (`<kit/gram_pos.h>`)
+
+Header-only `static inline`. Types `gram_line_index`→`KitGramLineIndex`,
+`gram_uline_index`→`KitGramULineIndex`. Functions `gram_line_count`/
+`gram_line_index_build`/`gram_position` and `gram_uline_count`/
+`gram_uline_index_build`/`gram_uposition` → `kit_gram_*`. The internal helpers
+(`gram__cp_advance`, `gram__fold_pos`, `gram__scalar`) become `kit_gram__*`
+(or file-static) — they are implementation detail, not exported, but should be
+respelled for consistency.
+
+### Unicode helpers (`<kit/gram_unicode.h>`)
+
+Types `gram_utf8_status`→`KitGramUtf8Status`, `gram_unicode_pos`→
+`KitGramUnicodePos`. Constant `GRAM_UNICODE_MAX_SCALAR`→
+`KIT_GRAM_UNICODE_MAX_SCALAR`; enumerators `GRAM_UTF8_OK`/`_NEED_MORE`/`_INVALID`
+→`KIT_GRAM_UTF8_*`. Functions `gram_unicode_is_*`, `gram_utf8_encode*`/
+`gram_utf8_decode_one`, `gram_unicode_pos_*`, `gram_unicode_fold_property_key`,
+`gram_utf8_is_cont` → `kit_gram_*`.
+
+### Generated-code support API
+
+The generated-support headers (`<kit/support/gram_parse_tables.h>`,
+`<kit/support/gram_lex_tables.h>`) also rename, since generated C and the
+checked-in meta tables include them:
+
+| Current name | Kit name |
+|--------------|----------|
+| `gram_sym_kind` (`GRAM_S_TERM`/`_RULE`/`_REP`/`_OPT`) | `KitGramSymKind` (`KIT_GRAM_S_*`) |
+| `gram_sym` / `gram_prod` / `gram_rule` | `KitGramSym` / `KitGramProd` / `KitGramRule` |
+| `gram_pratt_op_role` (`GRAM_PO_PREFIX`…`GRAM_PO_CIRCUMFIX`) | `KitGramPrattOpRole` (`KIT_GRAM_PO_*`) |
+| `gram_pratt_op` / `gram_pratt` | `KitGramPrattOp` / `KitGramPratt` |
+| `GRAM_TERM(t)` / `GRAM_RULE(r)` (authoring macros) | `KIT_GRAM_TERM` / `KIT_GRAM_RULE` |
+| `gram_lex_input_mode` (`GRAM_LEX_INPUT_BYTES`/`_UTF8`/`_TOKENS`) | `KitGramLexInputMode` (`KIT_GRAM_LEX_INPUT_*`) |
+| `gram_lex_anchor` (`GRAM_LEX_ANCHOR_NONE`/`_TEXT`/`_LINE`) | `KitGramLexAnchor` (`KIT_GRAM_LEX_ANCHOR_*`) |
+| `gram_lex_accept` / `gram_lex_keyword` / `gram_lex_keyword_table` | `KitGramLexAccept` / `KitGramLexKeyword` / `KitGramLexKeywordTable` |
+| `gram_lex_kw_hash64` (shared MPH hash) | `kit_gram_lex_kw_hash64` |
+| `GRAM_LEX_DEAD` / `GRAM_LEX_ACCEPT_NONE` | `KIT_GRAM_LEX_DEAD` / `KIT_GRAM_LEX_ACCEPT_NONE` |
+
+## Include Rewrites
+
+The generator should emit installed-style includes:
+
+```c
+/* generated header */
+#include <kit/gram_parse.h>
+#include <kit/gram_lex.h> /* only when a generated lexer exists */
+
+/* generated source */
+#include "generated_name.h"
+#include <kit/support/gram_parse_tables.h>
+#include <kit/support/gram_lex_tables.h> /* only when needed */
+```
+
+Two emit modes need care:
+
+- **Standalone scanner** (`lexer_standalone`): the DFA table is dropped and the
+ generated scanner is self-contained C, so it must *not* pull in
+ `<kit/gram_lex.h>`'s runtime-link surface beyond what it actually uses (it may
+ still reference `kit_gram_lex_keyword_rewrite` and the keyword tables).
+- **Parser-only grammars** (no `%lex`): omit the lexer includes entirely.
+
+Private `src/gram/*.c` files include `internal.h` and the public headers they
+implement. They must not include driver headers.
+
+## Driver Command
+
+The user-facing command is:
+
+```text
+kit gram [opts] grammar.ebnf
+ -o FILE output .c (default: <grammar>.c)
+ --header FILE output .h (default: <grammar>.h)
+ --prefix NAME identifier prefix (default: derived from filename)
+ --dump-sexpr print the parsed EBNF syntax tree instead of generating
+ --multiline bake newline-aware ^/$ into the lexer
+ --lexer-standalone emit a self-contained re2c-style scanner (C-only)
+ --fold-keywords standalone: fold %keywords into the DFA (default: MPH)
+ --position-lazy standalone: omit per-token line/col tracking
+ --parser-codegen also emit a pull-based recursive-descent parser
+ --parser-recover emit error-recovery machinery in the RD parser
+ --sample-tokens [--samples N] [--seed N]
+ [--max-depth N] [--max-repeat N] [--max-tokens N]
+ --sample-traces NAME [--samples N] [--seed N] ... (a %machine name)
+```
+
+`-` in place of the grammar path reads the grammar from stdin (default output
+names and prefix then derive from `stdin`).
+
+The first import should preserve existing behavior:
+
+- Default `.c` / `.h` output paths: replace the input suffix.
+- Default prefix: derive from the input basename (sanitized) and append `_`.
+- `--dump-sexpr`: write the meta-grammar dump to stdout.
+- Exit code `0`: success; `1`: compile/diagnostic/I/O failure; `2`: bad usage.
+
+The driver implementation uses `DriverEnv` for `KitContext`, file reads, writer
+opening, diagnostics, and memory. It must not call `malloc`, `free`, `fopen`,
+`fprintf`, or `exit` directly. The sample/trace synthesis paths use the public
+`kit_gram_parser_generate_next` / `kit_gram_machine_generate` entry points.
+
+Driver integration points:
+
+- Add `KIT_TOOL_GRAM_ENABLED` to `include/kit/config.h`.
+- Add `driver/cmd/gram.c`.
+- Add `driver_gram` and `driver_help_gram` to `driver/driver.h`.
+- Add the `gram` row to `driver/main.c`, gated by `KIT_TOOL_GRAM_ENABLED`.
+- Add `$(call tool-cmd,GRAM,gram)` to `mk/driver_srcs.mk`.
+- Keep it in `DRIVER_GROUP_OTHER` for now. It is a developer tool, not a default
+ drop-in binutils/toolchain symlink.
+
+## Build Integration
+
+Library integration points:
+
+- Add `KIT_GRAM_ENABLED` to `include/kit/config.h` as an optional library
+ subsystem.
+- Add `LIB_SRCS_GRAM := $(shell find src/gram -name '*.c' ...)` to
+ `mk/lib_srcs.mk`, included only when `KIT_GRAM_ENABLED` is `1`.
+- Add weak public stubs in `src/api/config_stubs.c` for gated-out public API
+ entry points (`kit_gram_*`). Because the whole subsystem (runtime + generator)
+ is gated together, the public runtime symbols need stubs too when gated out.
+- Keep `src/gram/meta_tables.c` checked in. Normal `make lib` must not require
+ Python, network access, UCD regeneration, or a bootstrap `gram` binary.
+- Add regeneration-only maintenance targets later, e.g. `make regen-gram-meta`
+ (re-emit `meta_tables.{c,h}` from `src/gram/meta.ebnf` with prefix
+ `kit_gram_meta_`, using the just-built `kit gram`) and
+ `make regen-gram-unicode-props` (re-run `scripts/gen_gram_unicode_props.py`
+ against the vendored UCD and diff for drift).
+
+The first import gates runtime and generator together under `KIT_GRAM_ENABLED`.
+If size-sensitive embeddings later need the generated parser runtime without the
+generator compiler, split into `KIT_GRAM_RUNTIME_ENABLED` (runtime + support
+headers) and `KIT_GRAM_ENABLED` (generator, depending on runtime). Do not
+introduce that split until a real embedding benefits; it adds config and stub
+surface.
+
+Upstream's `NO_UNICODE` slim build compiles out `%lex :utf8` (the scalar +
+unicode pipelines and the ~0.5 MB property tables). If kit wants that knob, model
+it as `KIT_GRAM_UNICODE_ENABLED` (default on) that excludes `lex_scalar.c`,
+`lex_unicode.c`, and `unicode_props.c` and makes the generator reject utf8 mode
+with a clear diagnostic. Treat it as a later size-tuning option, not part of the
+first import.
+
+## Implementation Phases
+
+### Phase 0: Freeze Standalone Behavior
+
+- Run the current `gram` test suite (`make test` in `/Users/ryan/code/gram`)
+ with the **C** generator (`GRAMGEN=./build/gramgen_c`) and save the passing
+ command set in the import notes — the C generator is what we keep.
+- Generate and check in `meta_tables.{c,h}` from `meta.ebnf`.
+- Confirm generated output for representative grammars (calc, features, pratt,
+ unicode, keywords, anchors, machine) is stable.
+
+### Phase 1: Mechanical Import, Private Names Still Allowed Internally
+
+- Move files into the destinations above.
+- Keep behavior unchanged while fixing include paths.
+- Add `test/gram` fixtures and a `make test-gram` target, initially allowed to
+ fail until the namespace rewrite lands.
+
+### Phase 2: Public Namespace Rewrite
+
+- Apply the rename scheme to every public `gram_*`, `gramgen_*`, `gramregex_*`,
+ and `GRAM_*` symbol, plus `GRAMAPI`→`KIT_API` and
+ `GRAM_OPAQUE_TYPE`→`KIT_GRAM_OPAQUE_TYPE`.
+- Update the C emitter in `generator.c` so emitted C and the checked-in meta
+ tables use the new names. `generator.c` stays a single translation unit —
+ renames only, no structural split.
+- Run `make test-lib-deps` to catch any leaked public symbol outside `Kit`,
+ `kit_`, or `KIT` — the relocatable-link check is the gate here.
+
+### Phase 3: Kit Context Rewrite
+
+- Replace `gramgen_allocator` with `KitContext.heap`.
+- Replace `gramgen_generate_c` + `gramgen_writer` with `kit_gram_emit_c` over
+ `KitWriter`; replace `gramgen_dump_sexpr_text` + `gramgen_text` with
+ `kit_gram_dump_sexpr` over a `KitWriter` (callers that want owned text use
+ `kit_writer_mem`).
+- Replace the `gramgen_diagnostic` out-param with `KitContext.diag` reporting.
+- Remove hosted libc calls from imported library code (they live only in the
+ driver CLI today, but verify the generator core is clean).
+- The runtimes already avoid `setjmp`/`longjmp` and allocation; confirm the
+ generator's OOM and validation-abort paths unwind via explicit status rather
+ than `exit` once the CLI scaffolding is gone.
+
+### Phase 4: Driver Command
+
+- Port `gen/gramgen_cli.c` to `driver/cmd/gram.c` using the public API only.
+- Add help text consistent with other driver commands.
+- Add a focused driver test: invoke `kit gram` on a small grammar, compile the
+ generated C against libkit, and run the parser and lexer.
+
+### Phase 5: Cleanup and Documentation
+
+- Document the stable runtime API in the public headers.
+- Add a durable design doc under `doc/` only after the subsystem ships (the
+ upstream `doc/DESIGN.md` is a good seed for the algorithms).
+- Add `gram` to `README.md` and `doc/DESIGN.md` capability lists after the
+ command works.
+- Delete `src/gram/meta.ebnf`-only build scaffolding that is not needed at
+ normal build time; keep `meta.ebnf` for the regen target.
+
+## Tests
+
+Targeted tests should land with the import:
+
+- `make test-gram`: direct API compile, table introspection, generated parser,
+ generated lexer, sublexers, Pratt grammar, mixfix Pratt, UTF-8 lexer, match
+ API, token machines, lazy positions, parser-codegen, keyword extraction, and
+ the error fixtures.
+- `make test-driver-gram`: CLI generation and generated-code compile/run smoke.
+- `make test-lib-deps`: symbol discipline (relocatable-link prefix check) and no
+ accidental hosted dependencies.
+
+Map standalone tests as follows:
+
+| Standalone test | Kit test |
+|-----------------|----------|
+| `test/test_gramgen_api.c` | `test/gram/api_test.c` |
+| `test/test_calc.c` | `test/gram/calc_test.c` |
+| `test/test_features.c` | `test/gram/features_test.c` |
+| `test/test_lexer.c` | `test/gram/lexer_test.c` |
+| `test/test_lexer_standalone.c` | `test/gram/lexer_standalone_test.c` |
+| `test/test_pratt_calc.c` | `test/gram/pratt_calc_test.c` |
+| `test/test_pratt_mixfix.c` | `test/gram/pratt_mixfix_test.c` |
+| `test/test_unicode_support.c` | `test/gram/unicode_support_test.c` |
+| `test/test_unicode_lexer.c` | `test/gram/unicode_lexer_test.c` |
+| `test/test_utf8_runtime.c` | `test/gram/utf8_runtime_test.c` |
+| `test/test_match.c` | `test/gram/match_test.c` |
+| `test/test_machine.c` | `test/gram/machine_test.c` |
+| `test/test_position.c` | `test/gram/position_test.c` |
+| `test/test_parser_codegen.c` | `test/gram/parser_codegen_test.c` |
+| `test/test_keywords.c` | `test/gram/keywords_test.c` |
+| `test/test_desugar.c` | `test/gram/desugar_test.c` |
+| `test/test_sublexer_table.c` | `test/gram/sublexer_table_test.c` |
+| `test/realistic/*` | `test/gram/realistic/*` |
+| `test/standalone/*` | `test/gram/standalone/*` |
+| `test/errors/*.ebnf` | `test/gram/errors/*.ebnf` |
+| `test/machine_errors/*.ebnf` | `test/gram/machine_errors/*.ebnf` |
+| `test/nounicode/*.ebnf` | `test/gram/nounicode/*.ebnf` |
+
+Prefer red-green import steps:
+
+1. Add the test target and fixtures first.
+2. Import the runtime until hand-written/generated tables parse again.
+3. Import the generator until direct API tests pass.
+4. Add the driver command and smoke test last.
+
+## Open Questions
+
+These are non-blocking — each has a stated default; revisit only if an embedding
+forces it. (Scope, generator factoring, the diagnostics channel, and in-tree UCD
+data are settled in the Decision Summary.)
+
+- Should generated table-layout headers be documented as stable ABI, or merely
+ stable enough for C emitted by the same libkit version? The first import should
+ promise only same-version compatibility.
+- Should the runtime/generator gate split (and the `NO_UNICODE`-equivalent
+ `KIT_GRAM_UNICODE_ENABLED`) ship in the first import? The plan says no until an
+ embedding needs the size savings.
+- Should `<kit/gram_unicode.h>` be a public header, or private to `src/gram`?
+ Upstream tags it `GRAMAPI`; decide based on whether any generated *runtime*
+ (not just generation-time) code needs it. `gram_pos.h` is clearly worth
+ exposing (header-only, standalone-useful).
+- Should `gram` eventually support non-C output modes? The API should not bake in
+ more than `emit_c` today, but the command can grow `--emit=` later.
diff --git a/doc/plan/LLGEN_IMPORT.md b/doc/plan/LLGEN_IMPORT.md
@@ -1,390 +0,0 @@
-# Gram Import Plan
-
-This is the import plan for folding `/Users/ryan/code/ll1` into libkit and
-exposing it through the kit driver. The goal is not to make `ll1` a language
-frontend. It should land as a reusable parser/lexer-generator subsystem: EBNF in,
-parse and lexer tables out, with allocation-free push runtimes for generated
-parsers and lexers.
-
-The standalone project already has the right broad shape for libkit: explicit
-allocation, no hidden CLI dependency in the generator API, immutable generated
-tables, and caller-owned runtime state. The import work is mostly namespace,
-host-boundary, build-gating, and file-layout discipline.
-
-## Decision Summary
-
-- Driver command: `gram`.
-- Public generator API header: `<kit/gram.h>`.
-- Public parser runtime header: `<kit/gram_parse.h>`.
-- Public lexer runtime header: `<kit/gram_lex.h>`.
-- Generated-code support headers: `<kit/support/gram_parse_tables.h>` and
- `<kit/support/gram_lex_tables.h>`.
-- Library subsystem gate: `KIT_GRAM_ENABLED`.
-- Driver tool gate: `KIT_TOOL_GRAM_ENABLED`.
-- Implementation directory: `src/gram/`.
-- Tests: `test/gram/` plus a driver smoke lane.
-
-`ll1` remains a source repository/project name only. It should not appear in the
-libkit API, installed command name, or user-facing help.
-
-## Boundaries
-
-`gram` is a library subsystem, not a `lang/` frontend. It does not register a
-`KitFrontendVTable`, does not emit `KitCg`, and does not participate in
-source-to-object compilation unless a future frontend chooses to use it
-internally. Its public surface is consumed by embedders, the driver command, and
-generated C files.
-
-The driver remains the only hosted layer. File reads, directory creation, stdout,
-stderr, and CLI allocation policy live in `driver/cmd/gram.c` and the hosted
-driver environment. The library accepts input as `KitSlice`, writes output to
-`KitWriter`, allocates through `KitContext.heap`, and reports diagnostics through
-`KitContext.diag`.
-
-Generated grammar tables are immutable static data and are fine as globals.
-Mutable parser, lexer, and generator state must hang off caller-owned runtime
-objects or explicit libkit handles.
-
-## File Moves
-
-Import the C implementation, generated meta tables, runtime, and tests. Do not
-make the Python generator part of the normal libkit build.
-
-| Current file | Kit destination | Notes |
-|--------------|-----------------|-------|
-| `include/llgen.h` | `include/kit/gram.h` | Public generator API, renamed to Kit types and functions. |
-| `include/llparse.h` | `include/kit/gram_parse.h` | Public parser runtime API. |
-| `include/lllex.h` | `include/kit/gram_lex.h` | Public lexer runtime API. |
-| `include/llparse_tables.h` | `include/kit/support/gram_parse_tables.h` | Public generated-code support, not ordinary embedder API. |
-| `include/lllex_tables.h` | `include/kit/support/gram_lex_tables.h` | Public generated-code support, not ordinary embedder API. |
-| `include/llunicode.h` | `src/gram/unicode.h` | Private helper; expose later as `<kit/unicode.h>` only if there is a broader API need. |
-| `include/llunicode_props.h` | `src/gram/unicode_props.h` | Private generated Unicode property resolver. |
-| `runtime/llparse.c` | `src/gram/parse_runtime.c` | Implements `<kit/gram_parse.h>`. |
-| `runtime/lllex.c` | `src/gram/lex_runtime.c` | Implements `<kit/gram_lex.h>`. |
-| `runtime/llunicode.c` | `src/gram/unicode.c` | Private Unicode helpers for generator and lexer runtime. |
-| `runtime/llunicode_props.c` | `src/gram/unicode_props.c` | Checked-in generated property tables. |
-| `gen/llgen.c` | `src/gram/generator.c` | Public API implementation and C emitter after Kit context rewrite. |
-| `gen/llgen_ll1.c` | `src/gram/ll1.c` | LL(1), Pratt, FIRST/FOLLOW, and validation. |
-| `gen/llgen_lex_byte.c` | `src/gram/lex_byte.c` | Byte-mode lexer compiler. |
-| `gen/llgen_lex_unicode.c` | `src/gram/lex_unicode.c` | Unicode-mode lexer compiler. |
-| `gen/llgen_internal.h` | `src/gram/internal.h` | Private to `src/gram/*.c`. |
-| `gen/meta.ebnf` | `src/gram/meta.ebnf` | Source grammar for the generator's own parser; not consumed by normal builds. |
-| generated `meta` `.c/.h` | `src/gram/meta_tables.c`, `src/gram/meta_tables.h` | Check in generated tables so libkit does not need Python or a previous `gram` to build. |
-| `gen/llgen_cli.c` | `driver/cmd/gram.c` | Rewrite as a Kit driver command using public `<kit/gram.h>`. |
-| `tools/gen_unicode_props.py` | `scripts/gen_gram_unicode_props.py` | Regeneration helper only; not in the build. |
-| `data/ucd/17.0.0/*` | `data/ucd/17.0.0/*` or `test/gram/ucd/17.0.0/*` | Keep if we want reproducible Unicode-table regeneration in-tree. |
-| `test/*.c`, `test/*.ebnf` | `test/gram/*` | Library tests and fixtures. |
-| `test/errors/*` | `test/gram/errors/*` | Error fixtures. |
-
-`gen/llgen.py` should not be imported into libkit. If parity against the Python
-reference is still useful during the transition, keep it temporarily as
-`scripts/gram_ref.py` and exclude it from release/build dependencies. Delete it
-once the imported C generator is trusted.
-
-## Public Renames
-
-The standalone `ll_*` and `llgen_*` names are too short for libkit and would
-violate the public symbol discipline. Public definitions for the imported
-subsystem must use `KitGram`, `kit_gram_`, or `KIT_GRAM_`.
-
-### Generator API
-
-| Standalone name | Kit name |
-|-----------------|----------|
-| `llgen_options` | `KitGramOptions` |
-| `llgen_compiled` | `KitGramCompiled` |
-| `llgen_codegen` | Remove or narrow; prefer `KitWriter` outputs. |
-| `llgen_compile_text` | `kit_gram_compile_text` |
-| `llgen_compiled_free` | `kit_gram_free` |
-| `llgen_dump_sexpr_text` | `kit_gram_dump_sexpr` |
-| `llgen_generate_c` | `kit_gram_emit_c` |
-| `llgen_parser_grammar` | `kit_gram_parser_grammar` |
-| `llgen_lexer_grammar` | `kit_gram_lexer_grammar` |
-| `llgen_token_count` | `kit_gram_token_count` |
-| `llgen_token_name` | `kit_gram_token_name` |
-| `llgen_token_display` | `kit_gram_token_display` |
-| `llgen_find_token` | `kit_gram_find_token` |
-| `llgen_rule_count` | `kit_gram_rule_count` |
-| `llgen_rule_name` | `kit_gram_rule_name` |
-| `llgen_find_rule` | `kit_gram_find_rule` |
-
-The generator API should take a `const KitContext*` and `KitSlice` inputs. It
-should not expose `llgen_allocator`; the allocator becomes `ctx->heap`. C output
-should stream to caller-provided `KitWriter`s. Callers that want owned in-memory
-text can use `kit_writer_mem`.
-
-Proposed core shape:
-
-```c
-typedef struct KitGramCompiled KitGramCompiled;
-
-typedef struct KitGramOptions {
- KitSlice name; /* optional grammar name override */
-} KitGramOptions;
-
-typedef struct KitGramEmitOptions {
- KitSlice header_path;
- KitSlice source_path;
- KitSlice prefix;
-} KitGramEmitOptions;
-
-KIT_API KitStatus kit_gram_compile_text(const KitContext* ctx,
- KitSlice text, KitSlice path,
- const KitGramOptions* opts,
- KitGramCompiled** out);
-KIT_API void kit_gram_free(KitGramCompiled*);
-
-KIT_API KitStatus kit_gram_emit_c(const KitGramCompiled*,
- const KitGramEmitOptions* opts,
- KitWriter* header, KitWriter* source);
-KIT_API KitStatus kit_gram_dump_sexpr(const KitContext* ctx, KitSlice text,
- KitSlice path,
- const KitGramOptions* opts,
- KitWriter* out);
-```
-
-### Parser Runtime API
-
-| Standalone name | Kit name |
-|-----------------|----------|
-| `ll_tok_kind` | `KitGramTokenKind` |
-| `ll_rule_id` | `KitGramRuleId` |
-| `ll_sem` | `KitGramSem` |
-| `ll_token` | `KitGramToken` |
-| `ll_error` | `KitGramParseError` |
-| `ll_err_action` | `KitGramErrorAction` |
-| `LL_ABORT` | `KIT_GRAM_ERROR_ABORT` |
-| `LL_SKIP` | `KIT_GRAM_ERROR_SKIP` |
-| `LL_RESYNC` | `KIT_GRAM_ERROR_RESYNC` |
-| `ll_actions` | `KitGramActions` |
-| `ll_slot` | `KitGramSlot` |
-| `LL_SLOT_SIZE` | `KIT_GRAM_SLOT_SIZE` |
-| `ll_config` | `KitGramParserConfig` |
-| `ll_grammar` | `KitGramGrammar` |
-| `ll_parser` | `KitGramParser` |
-| `LL_PARSER_SIZE` | `KIT_GRAM_PARSER_SIZE` |
-| `ll_status` | `KitGramParseStatus` |
-| `LL_NEED_MORE` | `KIT_GRAM_PARSE_NEED_MORE` |
-| `LL_PARSE_ACCEPT` | `KIT_GRAM_PARSE_ACCEPT` |
-| `LL_PARSE_ERROR` | `KIT_GRAM_PARSE_ERROR` |
-| `ll_parser_init` | `kit_gram_parser_init` |
-| `ll_parser_push` | `kit_gram_parser_push` |
-| `ll_parser_finish` | `kit_gram_parser_finish` |
-| `ll_parser_result` | `kit_gram_parser_result` |
-| `ll_stack_bounds` | `kit_gram_stack_bounds` |
-
-### Lexer Runtime API
-
-| Standalone name | Kit name |
-|-----------------|----------|
-| `ll_lex_grammar` | `KitGramLexGrammar` |
-| `ll_lexer` | `KitGramLexer` |
-| `LL_LEXER_SIZE` | `KIT_GRAM_LEXER_SIZE` |
-| `ll_lex_config` | `KitGramLexConfig` |
-| `ll_lex_status` | `KitGramLexStatus` |
-| `LL_LEX_TOKEN` | `KIT_GRAM_LEX_TOKEN` |
-| `LL_LEX_NEED_MORE` | `KIT_GRAM_LEX_NEED_MORE` |
-| `LL_LEX_EOF` | `KIT_GRAM_LEX_EOF` |
-| `LL_LEX_ERROR` | `KIT_GRAM_LEX_ERROR` |
-| `ll_lex_error` | `KitGramLexError` |
-| `ll_lexer_init` | `kit_gram_lexer_init` |
-| `ll_lexer_push` | `kit_gram_lexer_push` |
-| `ll_lexer_finish` | `kit_gram_lexer_finish` |
-| `ll_lexer_next` | `kit_gram_lexer_next` |
-| `ll_lexer_error` | `kit_gram_lexer_error` |
-
-### Generated-Code Support API
-
-The generated-code support headers should use Kit names too:
-
-| Standalone name | Kit name |
-|-----------------|----------|
-| `ll_sym_kind` | `KitGramSymKind` |
-| `LL_S_TERM` | `KIT_GRAM_SYM_TERM` |
-| `LL_S_RULE` | `KIT_GRAM_SYM_RULE` |
-| `LL_S_REP` | `KIT_GRAM_SYM_REP` |
-| `LL_S_OPT` | `KIT_GRAM_SYM_OPT` |
-| `ll_sym` | `KitGramSym` |
-| `ll_prod` | `KitGramProd` |
-| `ll_pratt_op` | `KitGramPrattOp` |
-| `ll_pratt` | `KitGramPratt` |
-| `ll_rule` | `KitGramRule` |
-| `LL_TERM` | `KIT_GRAM_TERM` |
-| `LL_RULE` | `KIT_GRAM_RULE` |
-| `ll_lex_accept` | `KitGramLexAccept` |
-| `LL_LEX_DEAD` | `KIT_GRAM_LEX_DEAD` |
-| `LL_LEX_ACCEPT_NONE` | `KIT_GRAM_LEX_ACCEPT_NONE` |
-
-Generated grammar-specific token/rule enums (`TOK_*`, `R_*`, and
-`<prefix>_*`) are user artifacts. They may keep their current shape because they
-are controlled by the generated prefix and are not libkit exports.
-
-## Include Rewrites
-
-The generator should emit installed-style includes:
-
-```c
-/* generated header */
-#include <kit/gram_parse.h>
-#include <kit/gram_lex.h> /* only when a generated lexer exists */
-
-/* generated source */
-#include "generated_name.h"
-#include <kit/support/gram_parse_tables.h>
-#include <kit/support/gram_lex_tables.h> /* only when needed */
-```
-
-Private `src/gram/*.c` files include `internal.h` and the public headers they
-implement. They must not include driver headers.
-
-## Driver Command
-
-The user-facing command is:
-
-```text
-kit gram [--dump-sexpr] [--prefix PREFIX] [-o OUT.c] [--header OUT.h] grammar.ebnf
-```
-
-The first import should preserve existing behavior:
-
-- Default `.c` output path: replace the input suffix with `.c`.
-- Default `.h` output path: replace the input suffix with `.h`.
-- Default prefix: derive from the input basename and append `_`.
-- `--dump-sexpr`: write the meta-grammar dump to stdout.
-- Exit code `0`: success.
-- Exit code `1`: compile, diagnostic, or I/O failure.
-- Exit code `2`: bad command-line usage.
-
-The driver implementation should use `DriverEnv` for `KitContext`, file reads,
-writer opening, diagnostics, and memory. The command should not call `malloc`,
-`free`, `fopen`, `fprintf`, or `exit` directly.
-
-Driver integration points:
-
-- Add `KIT_TOOL_GRAM_ENABLED` to `include/kit/config.h`.
-- Add `driver/cmd/gram.c`.
-- Add `driver_gram` and `driver_help_gram` to `driver/driver.h`.
-- Add the `gram` row to `driver/main.c`, gated by `KIT_TOOL_GRAM_ENABLED`.
-- Add `$(call tool-cmd,GRAM,gram)` to `mk/driver_srcs.mk`.
-- Keep it in `DRIVER_GROUP_OTHER` for now. It is a developer tool, not a
- default drop-in binutils/toolchain symlink.
-
-## Build Integration
-
-Library integration points:
-
-- Add `KIT_GRAM_ENABLED` to `include/kit/config.h` as an optional library
- subsystem.
-- Add `LIB_SRCS_GRAM := $(shell find src/gram -name '*.c' ...)` to
- `mk/lib_srcs.mk`, and include it only when `KIT_GRAM_ENABLED` is `1`.
-- Add weak public stubs in `src/api/config_stubs.c` for gated-out generator API
- entry points. Runtime stubs may be omitted if the runtime is considered part
- of the generated-code ABI and the whole subsystem is always enabled in the
- default build; if it is gated, public runtime symbols need stubs too.
-- Keep `src/gram/meta_tables.c` checked in. Normal `make lib` must not require
- Python, network access, UCD regeneration, or a bootstrap `gram` binary.
-- Add a regeneration-only maintenance target later, for example
- `make regen-gram-meta` and `make regen-gram-unicode-props`.
-
-The first import can gate runtime and generator together under
-`KIT_GRAM_ENABLED`. If size-sensitive embeddings need generated parser runtime
-without the generator compiler, split later into:
-
-- `KIT_GRAM_RUNTIME_ENABLED`: parser/lexer runtime plus support headers.
-- `KIT_GRAM_ENABLED`: generator compiler, depending on runtime.
-
-Do not introduce that split until there is a real embedding that benefits from
-it; it adds config and stub surface.
-
-## Implementation Phases
-
-### Phase 0: Freeze Standalone Behavior
-
-- Run the current `ll1` test suite and save the passing command set in the
- import notes.
-- Generate and check in `meta_tables.c` / `meta_tables.h` from `meta.ebnf`.
-- Confirm generated output for representative grammars is stable.
-
-### Phase 1: Mechanical Import, Private Names Still Allowed Internally
-
-- Move files into the destinations above.
-- Keep behavior unchanged while fixing include paths.
-- Add `test/gram` fixtures and a `make test-gram` target, initially allowed to
- fail until the namespace rewrite lands.
-
-### Phase 2: Public Namespace Rewrite
-
-- Rename every public `ll_*`, `llgen_*`, and `LL_*` symbol to `KitGram`,
- `kit_gram_`, or `KIT_GRAM_` spelling.
-- Update emitted C and generated meta tables to use the new names.
-- Run `make test-lib-deps` to catch leaked public symbols outside `Kit`,
- `kit_`, or `KIT`.
-
-### Phase 3: Kit Context Rewrite
-
-- Replace `llgen_allocator` with `KitContext.heap`.
-- Replace generated text return structs with `KitWriter` outputs.
-- Replace direct diagnostics with `KitContext.diag`.
-- Remove hosted libc calls from imported library code.
-- Keep `setjmp`/`longjmp` only if the existing frontend panic pattern accepts
- it for this subsystem; otherwise convert OOM and validation aborts to explicit
- `KitStatus` unwinding.
-
-### Phase 4: Driver Command
-
-- Port `gen/llgen_cli.c` to `driver/cmd/gram.c`.
-- Use the public generator API only.
-- Add help text consistent with other driver commands.
-- Add a focused driver test: invoke `kit gram` on a small grammar, compile the
- generated C against libkit, and run the parser.
-
-### Phase 5: Cleanup and Documentation
-
-- Document the stable runtime API in the public headers.
-- Add a durable design doc under `doc/` only after the subsystem ships.
-- Add `gram` to `README.md` and `doc/DESIGN.md` capability lists after the
- command works.
-- Remove temporary compatibility shims and any retained Python parity path.
-
-## Tests
-
-Targeted tests should land with the import:
-
-- `make test-gram`: direct API compile, table introspection, generated parser,
- generated lexer, Pratt grammar, UTF-8 lexer, and error fixtures.
-- `make test-driver-gram`: CLI generation and generated-code compile/run smoke.
-- `make test-lib-deps`: symbol discipline and no accidental hosted dependencies.
-
-Map standalone tests as follows:
-
-| Standalone test | Kit test |
-|-----------------|----------|
-| `test/test_llgen_api.c` | `test/gram/api_test.c` |
-| `test/test_calc.c` | `test/gram/calc_test.c` |
-| `test/test_features.c` | `test/gram/features_test.c` |
-| `test/test_lexer.c` | `test/gram/lexer_test.c` |
-| `test/test_pratt_calc.c` | `test/gram/pratt_test.c` |
-| `test/test_unicode_support.c` | `test/gram/unicode_test.c` |
-| `test/test_unicode_lexer.c` | `test/gram/unicode_lexer_test.c` |
-| `test/test_utf8_runtime.c` | `test/gram/utf8_runtime_test.c` |
-| `test/errors/*.ebnf` | `test/gram/errors/*.ebnf` |
-
-Prefer red-green import steps:
-
-1. Add the test target and fixtures first.
-2. Import the runtime until hand-written/generated tables parse again.
-3. Import the generator until direct API tests pass.
-4. Add the driver command and smoke test last.
-
-## Open Questions
-
-- Should generated table-layout headers be documented as stable ABI, or merely
- stable enough for C emitted by the same libkit version? The first import should
- promise only same-version compatibility.
-- Should Unicode UCD source data live in-tree permanently, or should only the
- generated property tables be checked in? Keeping the data improves
- reproducible regeneration but increases repository size.
-- Should the runtime/generator gate be split immediately? The plan says no until
- an embedding needs runtime-only size savings.
-- Should `gram` eventually support non-C output modes? The API should not bake
- in more than `emit_c` today, but the command can grow `--emit=` later.
diff --git a/doc/plan/README.md b/doc/plan/README.md
@@ -18,7 +18,7 @@ shrinks to whatever remains open (and is deleted once nothing remains open).
| [ARM32.md](ARM32.md) | 32-bit ARM (`arm-none-eabi`, ARMv7-M/ARMv7E-M Thumb-2, Cortex-M3/M4/M7) freestanding backend: Phase 1 (walking skeleton) is landed; Phase 2 tracks the remaining ops, the -O1 known-frame path, 64-bit, atomics, TLS, and the `qemu-system-arm` cross-test lane. | [../ARCH.md](../ARCH.md), [../PORT.md](../PORT.md) |
| [SYSROOTS.md](SYSROOTS.md) | Cross-compile sysroot packaging: minimal per-target stubs/headers/CRT objects distributed via `.kpkg` for the support set. Design complete, implementation not yet started. | — |
| [BUILD.md](BUILD.md) | A new content-addressed build coordinator (Bazel/Nix-style incremental builds layered on the CAS) — storage state machine, caching algorithm, recipe protocol. Design, not yet built. Distinct from `../BUILD.md` (kit's own Makefile build). | — (new subsystem) |
-| [LLGEN_IMPORT.md](LLGEN_IMPORT.md) | Importing the standalone LL(1)/Pratt parser and lexer generator into libkit, including public API renames, file moves, build gates, and a `kit llgen` command. Not yet started. | — |
+| [GRAM_IMPORT.md](GRAM_IMPORT.md) | One-time import of the standalone `gramgen` EBNF parser/lexer/token-machine generator (`/Users/ryan/code/gram`) into libkit: full `kit_gram_*`/`KitGram`/`KIT_GRAM_` rename, file moves, build gates, and a `kit gram` command. Not yet started. | — |
| [TODO.md](TODO.md) | Open deferred fixes and code smells, plus terse backlog folded from retired plan docs (arch-backend parity, Wasm object backend, Windows x64 self-host, bootstrap breadth). Completed items are removed instead of checked off. A current backlog, not a roadmap. | — |
Speculative, not-committed designs (no code, parked) live in [`../ideas/`](../ideas/)