kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

gram.c (24063B)


      1 #include <kit/core.h>
      2 #include <kit/gram.h>
      3 #include <stddef.h>
      4 #include <stdint.h>
      5 #include <stdlib.h>
      6 #include <string.h>
      7 
      8 #include "driver.h"
      9 #include "env.h"
     10 
     11 /* `kit gram` — EBNF grammar compiler. Reads an EBNF grammar (FILE or stdin via
     12  * "-") and emits a C parser/lexer (`-o`/`--header`), dumps the parsed syntax
     13  * tree (`--dump-sexpr`), or synthesizes sample token streams / %machine traces.
     14  * Drives the public <kit/gram.h> API over a DriverEnv (heap + stderr diag sink
     15  * + file I/O); the library reports compile/validation errors through ctx->diag,
     16  * so this command only sets the exit status for those. */
     17 
     18 #define GRAM_TOOL "gram"
     19 
     20 typedef struct {
     21   const char* grammar;       /* input path, or "-" for stdin */
     22   const char* out_c;         /* -o            (default: <grammar>.c) */
     23   const char* out_h;         /* --header      (default: <grammar>.h) */
     24   const char* prefix;        /* --prefix      (default: from filename) */
     25   const char* sample_traces; /* --sample-traces NAME (a %machine) */
     26   int sample_tokens;
     27   int have_stop_prob;
     28   double stop_prob;
     29   size_t samples;
     30   uint64_t seed;
     31   size_t max_depth;
     32   size_t max_repeat;
     33   size_t max_tokens;
     34   int dump_sexpr;
     35   int multiline;
     36   int lexer_standalone;
     37   int fold_keywords;
     38   int position_lazy;
     39   int parser_codegen;
     40   int parser_recover;
     41 } GramArgs;
     42 
     43 typedef enum GramArgErrorCategory {
     44   GRAM_ARG_OK = 0,
     45   GRAM_ARG_UNKNOWN_OPTION,
     46   GRAM_ARG_MISSING_OPTION_VALUE,
     47   GRAM_ARG_INVALID_NUMERIC_VALUE,
     48   GRAM_ARG_DUPLICATE_GRAMMAR,
     49   GRAM_ARG_MISSING_GRAMMAR,
     50 } GramArgErrorCategory;
     51 
     52 typedef struct GramArgError {
     53   GramArgErrorCategory category;
     54   const char* option;
     55   const char* value;
     56   const char* previous;
     57 } GramArgError;
     58 
     59 void driver_help_gram(void) {
     60   driver_printf(
     61       "%.*s",
     62       KIT_SLICE_ARG(KIT_SLICE_LIT(
     63           "kit gram — EBNF parser / lexer generator\n"
     64           "\n"
     65           "USAGE\n"
     66           "  kit gram [options] grammar.ebnf\n"
     67           "  kit gram [options] -            (read grammar from stdin)\n"
     68           "\n"
     69           "OUTPUT\n"
     70           "  -o FILE             output .c            (default: <grammar>.c)\n"
     71           "  --header FILE       output .h            (default: <grammar>.h)\n"
     72           "  --prefix NAME       identifier prefix    (default: from "
     73           "filename)\n"
     74           "  --dump-sexpr        print the parsed EBNF syntax tree to stdout\n"
     75           "\n"
     76           "GRAMMAR\n"
     77           "  A rule has the form `name = expression;`. Quoted text is a literal\n"
     78           "  token and a bare name references another rule. The smallest\n"
     79           "  accepted grammar is:\n"
     80           "\n"
     81           "    start = \"hello\";\n"
     82           "\n"
     83           "  EBNF expressions may compose rule/literal sequences, alternatives,\n"
     84           "  grouping, optional terms, and repetition. Directive families used\n"
     85           "  by the release include `%lex NAME { ... }` lexer blocks, `%skip`\n"
     86           "  lexer handling, `%keywords` extraction, and `%machine NAME` token\n"
     87           "  automata. --sample-traces NAME selects a declared %machine. Use\n"
     88           "  --dump-sexpr to inspect how an input grammar was parsed.\n"
     89           "\n"
     90           "LEXER\n"
     91           "  --multiline         bake newline-aware ^/$ into the lexer\n"
     92           "  --lexer-standalone  emit a self-contained re2c-style scanner "
     93           "(C-only)\n"
     94           "  --fold-keywords     standalone: fold %keywords into the DFA "
     95           "(default: MPH)\n"
     96           "  --position-lazy     standalone: omit per-token line/col tracking\n"
     97           "\n"
     98           "PARSER\n"
     99           "  --parser-codegen    also emit a pull-based recursive-descent "
    100           "parser\n"
    101           "  --parser-recover    emit error-recovery machinery in the RD "
    102           "parser\n"
    103           "\n"
    104           "SYNTHESIS\n"
    105           "  --sample-tokens                 print sample token streams\n"
    106           "  --sample-traces NAME            print traces of a %machine\n"
    107           "  --samples N  --seed N  --stop-prob P\n"
    108           "  --max-depth N  --max-repeat N  --max-tokens N\n"
    109           "\n"
    110           "GENERATED C CONTRACT\n"
    111           "  Normal generation writes <grammar>.c and <grammar>.h unless -o\n"
    112           "  and --header override them. Grammar `-` reads stdin; syntax dumps\n"
    113           "  and samples write stdout, while diagnostics use stderr. Generated\n"
    114           "  parser headers include <kit/gram_parse.h>; compile with the shipped\n"
    115           "  include directory and link the shipped lib/libkit.a runtime.\n"
    116           "\n"
    117           "  With --parser-codegen, PREFIXstack_bounds sizes caller-owned\n"
    118           "  control/value stacks, PREFIXgenerate_scratch_count reports sample\n"
    119           "  scratch storage, and PREFIXparser_init initializes a caller-owned\n"
    120           "  KitGramParser. The generated header is the authority for exact\n"
    121           "  enum and symbol names.\n"
    122           "\n"
    123           "PATHS\n"
    124           "  Use -- before a leading-dash grammar file name.\n"
    125           "\n"
    126           "GETTING HELP\n"
    127           "  -h, --help        Show this help and exit\n"
    128           "  --version         Show Kit version and exit\n"
    129           "\n"
    130           "EXAMPLES\n"
    131           "  printf '%s\\n' 'start = \"hello\";' > grammar.ebnf\n"
    132           "  kit gram --dump-sexpr grammar.ebnf\n"
    133           "\n"
    134           "  kit gram -o lexer.c --header lexer.h --prefix demo grammar.ebnf\n"
    135           "  kit gram --lexer-standalone --parser-codegen \\\n"
    136           "    -o parser.c --header parser.h --prefix demop grammar.ebnf\n"
    137           "\n"
    138           "EXIT CODES\n"
    139           "  0   success           1   compile / I/O error   2   bad usage\n")));
    140 }
    141 
    142 /* ---- small parsers (driver helpers + rt strtod/strtoull) ------------------ */
    143 
    144 static int gram_parse_size(const char* s, size_t* out) {
    145   uint64_t v;
    146   if (driver_parse_u64(s, &v) != 0 || v > SIZE_MAX) return 0;
    147   *out = (size_t)v;
    148   return 1;
    149 }
    150 
    151 static int gram_parse_double(const char* s, double* out) {
    152   char* end = NULL;
    153   double v;
    154   if (!s || !*s) return 0;
    155   v = strtod(s, &end);
    156   if (*end || v < 0.0 || v > 1.0) return 0;
    157   *out = v;
    158   return 1;
    159 }
    160 
    161 /* opt-with-value: matches "--name VALUE" (consuming argv[i+1]) or "--name=VALUE"
    162  * (prefix len `plen` covers "--name="). Returns the value or NULL on no match;
    163  * on a match that needs the next arg but argc runs out, sets *err. */
    164 static const char* opt_val(int argc, char** argv, int* i, const char* name,
    165                            const char* eqname, size_t plen,
    166                            GramArgError* error) {
    167   if (driver_streq(argv[*i], name)) {
    168     if (++*i >= argc) {
    169       error->category = GRAM_ARG_MISSING_OPTION_VALUE;
    170       error->option = name;
    171       return NULL;
    172     }
    173     return argv[*i];
    174   }
    175   if (driver_strneq(argv[*i], eqname, plen)) return argv[*i] + plen;
    176   return NULL;
    177 }
    178 
    179 static int gram_invalid_numeric(GramArgError* error, const char* option,
    180                                 const char* value) {
    181   error->category = GRAM_ARG_INVALID_NUMERIC_VALUE;
    182   error->option = option;
    183   error->value = value;
    184   return 0;
    185 }
    186 
    187 static int gram_set_grammar(GramArgs* a, GramArgError* error,
    188                             const char* value) {
    189   if (a->grammar) {
    190     error->category = GRAM_ARG_DUPLICATE_GRAMMAR;
    191     error->previous = a->grammar;
    192     error->value = value;
    193     return 0;
    194   }
    195   a->grammar = value;
    196   return 1;
    197 }
    198 
    199 static int parse_args(int argc, char** argv, GramArgs* a,
    200                       GramArgError* error) {
    201   int options = 1;
    202   memset(a, 0, sizeof *a);
    203   memset(error, 0, sizeof *error);
    204   for (int i = 1; i < argc; i++) {
    205     const char* arg = argv[i];
    206     const char* v;
    207     if (options && driver_streq(arg, "--")) {
    208       options = 0;
    209     } else if (!options) {
    210       if (!gram_set_grammar(a, error, arg)) return 0;
    211     } else if (driver_streq(arg, "--dump-sexpr")) {
    212       a->dump_sexpr = 1;
    213     } else if (driver_streq(arg, "--multiline")) {
    214       a->multiline = 1;
    215     } else if (driver_streq(arg, "--lexer-standalone")) {
    216       a->lexer_standalone = 1;
    217     } else if (driver_streq(arg, "--fold-keywords")) {
    218       a->fold_keywords = 1;
    219     } else if (driver_streq(arg, "--position-lazy")) {
    220       a->position_lazy = 1;
    221     } else if (driver_streq(arg, "--parser-codegen")) {
    222       a->parser_codegen = 1;
    223     } else if (driver_streq(arg, "--parser-recover")) {
    224       a->parser_recover = 1;
    225     } else if (driver_streq(arg, "--sample-tokens")) {
    226       a->sample_tokens = 1;
    227     } else if ((v = opt_val(argc, argv, &i, "--sample-traces",
    228                             "--sample-traces=", 16, error))) {
    229       a->sample_traces = v;
    230     } else if (error->category) {
    231       return 0;
    232     } else if ((v = opt_val(argc, argv, &i, "--stop-prob", "--stop-prob=", 12,
    233                             error))) {
    234       if (!gram_parse_double(v, &a->stop_prob))
    235         return gram_invalid_numeric(error, "--stop-prob", v);
    236       a->have_stop_prob = 1;
    237     } else if (error->category) {
    238       return 0;
    239     } else if ((v = opt_val(argc, argv, &i, "--samples", "--samples=", 10,
    240                             error))) {
    241       if (!gram_parse_size(v, &a->samples))
    242         return gram_invalid_numeric(error, "--samples", v);
    243     } else if (error->category) {
    244       return 0;
    245     } else if ((v = opt_val(argc, argv, &i, "--seed", "--seed=", 7, error))) {
    246       if (driver_parse_u64(v, &a->seed) != 0)
    247         return gram_invalid_numeric(error, "--seed", v);
    248     } else if (error->category) {
    249       return 0;
    250     } else if ((v = opt_val(argc, argv, &i, "--max-depth", "--max-depth=", 12,
    251                             error))) {
    252       if (!gram_parse_size(v, &a->max_depth))
    253         return gram_invalid_numeric(error, "--max-depth", v);
    254     } else if (error->category) {
    255       return 0;
    256     } else if ((v = opt_val(argc, argv, &i, "--max-repeat", "--max-repeat=", 13,
    257                             error))) {
    258       if (!gram_parse_size(v, &a->max_repeat))
    259         return gram_invalid_numeric(error, "--max-repeat", v);
    260     } else if (error->category) {
    261       return 0;
    262     } else if ((v = opt_val(argc, argv, &i, "--max-tokens", "--max-tokens=", 13,
    263                             error))) {
    264       if (!gram_parse_size(v, &a->max_tokens))
    265         return gram_invalid_numeric(error, "--max-tokens", v);
    266     } else if (error->category) {
    267       return 0;
    268     } else if ((v = opt_val(argc, argv, &i, "-o", "-o", 2, error))) {
    269       a->out_c = v;
    270     } else if (error->category) {
    271       return 0;
    272     } else if ((v = opt_val(argc, argv, &i, "--header", "--header=", 9,
    273                             error))) {
    274       a->out_h = v;
    275     } else if (error->category) {
    276       return 0;
    277     } else if ((v = opt_val(argc, argv, &i, "--prefix", "--prefix=", 9,
    278                             error))) {
    279       a->prefix = v;
    280     } else if (error->category) {
    281       return 0;
    282     } else if (driver_streq(arg, "-")) {
    283       if (!gram_set_grammar(a, error, arg)) return 0;
    284     } else if (arg[0] == '-' && arg[1]) {
    285       error->category = GRAM_ARG_UNKNOWN_OPTION;
    286       error->option = arg;
    287       return 0;
    288     } else {
    289       if (!gram_set_grammar(a, error, arg)) return 0;
    290     }
    291   }
    292   if (!a->grammar) {
    293     error->category = GRAM_ARG_MISSING_GRAMMAR;
    294     return 0;
    295   }
    296   return 1;
    297 }
    298 
    299 static void gram_print_arg_error(const GramArgError* error) {
    300   switch (error->category) {
    301     case GRAM_ARG_UNKNOWN_OPTION: {
    302       const char* const options[] = {
    303           "--dump-sexpr",       "--multiline",      "--lexer-standalone",
    304           "--fold-keywords",    "--position-lazy",  "--parser-codegen",
    305           "--parser-recover",   "--sample-tokens",  "--sample-traces",
    306           "--stop-prob",        "--samples",        "--seed",
    307           "--max-depth",        "--max-repeat",     "--max-tokens",
    308           "-o",                 "--header",         "--prefix",
    309           "-h",                 "--help",           "--version",
    310       };
    311       DriverSuggestion suggestions[3];
    312       size_t n = driver_suggest_values(
    313           error->option, options, sizeof options / sizeof options[0],
    314           suggestions, 3);
    315       if (n)
    316         driver_errf(GRAM_TOOL, "unknown option: %s; did you mean '%s'?",
    317                     error->option, suggestions[0].value);
    318       else
    319         driver_errf(GRAM_TOOL, "unknown option: %s", error->option);
    320       break;
    321     }
    322     case GRAM_ARG_MISSING_OPTION_VALUE:
    323       driver_errf(GRAM_TOOL, "option requires an argument: %s", error->option);
    324       break;
    325     case GRAM_ARG_INVALID_NUMERIC_VALUE:
    326       driver_errf(GRAM_TOOL, "invalid numeric value '%s' for %s", error->value,
    327                   error->option);
    328       break;
    329     case GRAM_ARG_DUPLICATE_GRAMMAR:
    330       driver_errf(GRAM_TOOL, "multiple grammar inputs: %s and %s",
    331                   error->previous, error->value);
    332       break;
    333     case GRAM_ARG_MISSING_GRAMMAR:
    334       driver_errf(GRAM_TOOL, "missing grammar input");
    335       break;
    336     case GRAM_ARG_OK:
    337       driver_errf(GRAM_TOOL, "invalid command line");
    338       break;
    339   }
    340   driver_errf(GRAM_TOOL,
    341               "usage: kit gram [options] grammar.ebnf  (see --help)");
    342 }
    343 
    344 /* ---- owned-string helpers (env heap; freed via gram_free_str) ------------- */
    345 
    346 static char* gram_dup_n(DriverEnv* env, const char* s, size_t n) {
    347   char* out = driver_alloc(env, n + 1);
    348   if (!out) return NULL;
    349   driver_memcpy(out, s, n);
    350   out[n] = '\0';
    351   return out;
    352 }
    353 
    354 static char* gram_dup(DriverEnv* env, const char* s) {
    355   return gram_dup_n(env, s, driver_strlen(s));
    356 }
    357 
    358 static void gram_free_str(DriverEnv* env, char* s) {
    359   if (s) driver_free(env, s, driver_strlen(s) + 1);
    360 }
    361 
    362 /* "<dir>/foo.ebnf" -> "foo" (basename without the final extension). */
    363 static char* gram_path_stem(DriverEnv* env, const char* path) {
    364   const char* base = driver_basename(path);
    365   const char* dot = NULL;
    366   for (const char* p = base; *p; p++)
    367     if (*p == '.') dot = p;
    368   size_t n = (dot && dot > base) ? (size_t)(dot - base) : driver_strlen(base);
    369   return gram_dup_n(env, base, n);
    370 }
    371 
    372 /* Replace the final extension of `path` with `suffix` (e.g. ".c"). */
    373 static char* gram_replace_suffix(DriverEnv* env, const char* path,
    374                                  const char* suffix) {
    375   const char* dot = NULL;
    376   for (const char* p = path; *p; p++)
    377     if (*p == '.') dot = p;
    378   size_t n = dot ? (size_t)(dot - path) : driver_strlen(path);
    379   size_t slen = driver_strlen(suffix);
    380   char* out = driver_alloc(env, n + slen + 1);
    381   if (!out) return NULL;
    382   driver_memcpy(out, path, n);
    383   driver_memcpy(out + n, suffix, slen + 1);
    384   return out;
    385 }
    386 
    387 /* Sanitized identifier prefix derived from the grammar filename + '_'. */
    388 static char* gram_default_prefix(DriverEnv* env, const char* path) {
    389   char* stem = gram_path_stem(env, path);
    390   if (!stem) return NULL;
    391   size_t slen = driver_strlen(stem);
    392   char* out = driver_alloc(env, slen + 3);
    393   if (!out) {
    394     gram_free_str(env, stem);
    395     return NULL;
    396   }
    397   size_t len = 0;
    398   if (!stem[0] || (stem[0] >= '0' && stem[0] <= '9')) out[len++] = '_';
    399   for (size_t i = 0; stem[i]; i++) {
    400     char c = stem[i];
    401     int ok = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
    402              (c >= '0' && c <= '9') || c == '_';
    403     out[len++] = ok ? c : '_';
    404   }
    405   out[len++] = '_';
    406   out[len] = '\0';
    407   gram_free_str(env, stem);
    408   return out;
    409 }
    410 
    411 /* Best-effort mkdir -p of `path`'s parent directory (posix_open_writer needs the
    412  * directory to exist for its temp+rename). A path with no '/' lands in cwd. */
    413 static void gram_mkparent(DriverEnv* env, const char* path) {
    414   const char* slash = NULL;
    415   for (const char* p = path; *p; p++)
    416     if (*p == '/') slash = p;
    417   if (!slash || slash == path) return;
    418   char* dir = gram_dup_n(env, path, (size_t)(slash - path));
    419   if (!dir) return;
    420   (void)driver_mkdir_p(env, dir);
    421   gram_free_str(env, dir);
    422 }
    423 
    424 /* ---- token-stream / machine-trace synthesis ------------------------------- */
    425 
    426 static int sample_tokens(DriverEnv* env, const KitGramCompiled* compiled,
    427                          const GramArgs* a, const char* diag_path) {
    428   const KitGramGrammar* g = kit_gram_parser_grammar(compiled);
    429   if (!g) {
    430     driver_errf(GRAM_TOOL, "%s: grammar has no parser to sample", diag_path);
    431     return 0;
    432   }
    433   size_t depth = a->max_depth ? a->max_depth : 8;
    434   size_t ctl_cap = 0, val_cap = 0;
    435   kit_gram_stack_bounds(g, depth + 4, &ctl_cap, &val_cap);
    436   if (!ctl_cap) ctl_cap = 1;
    437   if (!val_cap) val_cap = 1;
    438   size_t scratch_cap = kit_gram_parser_generate_scratch_count(g);
    439   if (!scratch_cap) scratch_cap = 1;
    440 
    441   KitGramSlot* ctl = driver_alloc(env, ctl_cap * sizeof *ctl);
    442   KitGramSem* vals = driver_alloc(env, val_cap * sizeof *vals);
    443   size_t* scratch = driver_alloc(env, scratch_cap * sizeof *scratch);
    444   int rc = 1;
    445   if (!ctl || !vals || !scratch) {
    446     driver_errf(GRAM_TOOL, "out of memory");
    447     if (scratch) driver_free(env, scratch, scratch_cap * sizeof *scratch);
    448     if (vals) driver_free(env, vals, val_cap * sizeof *vals);
    449     if (ctl) driver_free(env, ctl, ctl_cap * sizeof *ctl);
    450     return 0;
    451   }
    452   size_t samples = a->samples ? a->samples : 1;
    453   for (size_t i = 0; i < samples && rc; i++) {
    454     KitGramParser ps;
    455     KitGramConfig pcfg = {.ctl_stack = ctl,
    456                           .ctl_cap = ctl_cap,
    457                           .val_stack = vals,
    458                           .val_cap = val_cap};
    459     kit_gram_parser_init(&ps, g, &pcfg);
    460     KitGramGenConfig gcfg = {.seed = a->seed + i,
    461                              .max_depth = a->max_depth,
    462                              .max_repeat = a->max_repeat,
    463                              .max_tokens = a->max_tokens,
    464                              .scratch = scratch,
    465                              .scratch_cap = scratch_cap};
    466     int first = 1;
    467     for (;;) {
    468       KitGramToken tok;
    469       KitGramGenStatus st = kit_gram_parser_generate_next(&ps, &gcfg, &tok);
    470       if (st == KIT_GRAM_GEN_TOKEN) {
    471         const char* name = kit_gram_token_name(compiled, tok.kind);
    472         driver_printf("%s%s", first ? "" : " ", name ? name : "?");
    473         first = 0;
    474         continue;
    475       }
    476       if (st == KIT_GRAM_GEN_DONE) {
    477         driver_printf("\n");
    478         break;
    479       }
    480       if (st == KIT_GRAM_GEN_LIMIT)
    481         driver_errf(GRAM_TOOL, "%s:0:0: synthesis token budget too small",
    482                     diag_path);
    483       else
    484         driver_errf(GRAM_TOOL, "%s:0:0: token generation failed", diag_path);
    485       rc = 0;
    486     }
    487   }
    488   driver_free(env, scratch, scratch_cap * sizeof *scratch);
    489   driver_free(env, vals, val_cap * sizeof *vals);
    490   driver_free(env, ctl, ctl_cap * sizeof *ctl);
    491   return rc;
    492 }
    493 
    494 static int sample_traces(DriverEnv* env, const KitGramCompiled* compiled,
    495                          const GramArgs* a, const char* diag_path) {
    496   size_t mi;
    497   if (!kit_gram_find_machine(compiled, a->sample_traces, &mi)) {
    498     driver_errf(GRAM_TOOL, "%s:0:0: no %%machine named '%s'", diag_path,
    499                 a->sample_traces);
    500     return 0;
    501   }
    502   double stop_prob = a->have_stop_prob ? a->stop_prob : 0.5;
    503   size_t max_repeat = a->max_repeat ? a->max_repeat : 2;
    504   size_t max_tokens = a->max_tokens ? a->max_tokens : 128;
    505   size_t samples = a->samples ? a->samples : 1;
    506   size_t cap = max_tokens ? max_tokens : 1;
    507   const char** buf = driver_alloc(env, cap * sizeof *buf);
    508   if (!buf) {
    509     driver_errf(GRAM_TOOL, "out of memory");
    510     return 0;
    511   }
    512   uint64_t seed = a->seed;
    513   int rc = 1;
    514   for (size_t i = 0; i < samples; i++) {
    515     size_t n = 0;
    516     KitGramGenStatus st =
    517         kit_gram_machine_generate(compiled, mi, 0, &seed, stop_prob, max_repeat,
    518                                   max_tokens, buf, cap, &n);
    519     if (st == KIT_GRAM_GEN_ERROR) {
    520       driver_errf(GRAM_TOOL, "%s:0:0: trace generation failed", diag_path);
    521       rc = 0;
    522       break;
    523     }
    524     size_t shown = n < cap ? n : cap;
    525     for (size_t j = 0; j < shown; j++)
    526       driver_printf("%s%s", j ? " " : "", buf[j]);
    527     driver_printf("\n");
    528     if (st == KIT_GRAM_GEN_LIMIT)
    529       driver_errf(GRAM_TOOL, "%s:0:0: note: trace truncated at max-tokens=%zu",
    530                   diag_path, max_tokens);
    531   }
    532   driver_free(env, buf, cap * sizeof *buf);
    533   return rc;
    534 }
    535 
    536 /* ---- entry ---------------------------------------------------------------- */
    537 
    538 int driver_gram(int argc, char** argv) {
    539   DriverEnv env;
    540   KitContext ctx;
    541   GramArgs a;
    542   GramArgError arg_error;
    543   DriverLoad ld = {0};
    544   uint8_t* sbuf = NULL;
    545   size_t sbuf_len = 0;
    546   KitSlice text = KIT_SLICE_NULL;
    547   KitGramCompiled* compiled = NULL;
    548   char* out_c = NULL;
    549   char* out_h = NULL;
    550   char* prefix = NULL;
    551   int loaded = 0, rc = 2;
    552 
    553   if (driver_argv_wants_help(argc, argv, 1)) {
    554     driver_help_gram();
    555     return 0;
    556   }
    557   if (!parse_args(argc, argv, &a, &arg_error)) {
    558     gram_print_arg_error(&arg_error);
    559     return 2;
    560   }
    561 
    562   driver_env_init(&env);
    563   ctx = driver_env_to_context(&env);
    564 
    565   int from_stdin = driver_streq(a.grammar, "-");
    566   const char* diag_path = from_stdin ? "<stdin>" : a.grammar;
    567   const char* derive_from = from_stdin ? "stdin" : a.grammar;
    568 
    569   if (from_stdin) {
    570     if (!driver_read_stdin(&env, &sbuf, &sbuf_len)) {
    571       driver_errf(GRAM_TOOL, "cannot read stdin");
    572       rc = 1;
    573       goto done;
    574     }
    575     text.data = sbuf;
    576     text.len = sbuf_len;
    577   } else {
    578     if (driver_load_bytes(&env.file_io, GRAM_TOOL, a.grammar, &ld, &text) != 0) {
    579       rc = 1;
    580       goto done;
    581     }
    582     loaded = 1;
    583   }
    584 
    585   KitGramOptions opts = {.multiline = a.multiline != 0,
    586                          .lexer_standalone = a.lexer_standalone != 0,
    587                          .fold_keywords = a.fold_keywords != 0,
    588                          .position_lazy = a.position_lazy != 0,
    589                          .parser_codegen = a.parser_codegen != 0,
    590                          .parser_recover = a.parser_recover != 0};
    591   KitSlice path = kit_slice_cstr(diag_path);
    592 
    593   /* --dump-sexpr: stream the parsed syntax tree to stdout (+ trailing \n). */
    594   if (a.dump_sexpr) {
    595     KitWriter* w = driver_stdout_writer(&env);
    596     if (!w) {
    597       driver_errf(GRAM_TOOL, "cannot open stdout");
    598       rc = 1;
    599       goto done;
    600     }
    601     KitStatus st = kit_gram_dump_sexpr(&ctx, text, path, &opts, w);
    602     if (st == KIT_OK) kit_writer_write(w, "\n", 1);
    603     kit_writer_close(w);
    604     rc = st == KIT_OK ? 0 : 1;
    605     goto done;
    606   }
    607 
    608   if (kit_gram_compile_text(&ctx, text, path, &opts, &compiled) != KIT_OK) {
    609     rc = 1;
    610     goto done;
    611   }
    612 
    613   if (a.sample_tokens) {
    614     rc = sample_tokens(&env, compiled, &a, diag_path) ? 0 : 1;
    615     goto done;
    616   }
    617   if (a.sample_traces) {
    618     rc = sample_traces(&env, compiled, &a, diag_path) ? 0 : 1;
    619     goto done;
    620   }
    621 
    622   /* Default: emit the generated C header + source. */
    623   out_c = a.out_c ? gram_dup(&env, a.out_c)
    624                   : gram_replace_suffix(&env, derive_from, ".c");
    625   out_h = a.out_h ? gram_dup(&env, a.out_h)
    626                   : gram_replace_suffix(&env, derive_from, ".h");
    627   prefix = a.prefix ? gram_dup(&env, a.prefix)
    628                     : gram_default_prefix(&env, derive_from);
    629   if (!out_c || !out_h || !prefix) {
    630     driver_errf(GRAM_TOOL, "out of memory");
    631     rc = 1;
    632     goto done;
    633   }
    634 
    635   gram_mkparent(&env, out_h);
    636   gram_mkparent(&env, out_c);
    637   KitWriter* hw = NULL;
    638   KitWriter* sw = NULL;
    639   if (ctx.file_io->open_writer(ctx.file_io->user, out_h, &hw) != KIT_OK) {
    640     driver_errf(GRAM_TOOL, "cannot open output: %s", out_h);
    641     rc = 1;
    642     goto done;
    643   }
    644   if (ctx.file_io->open_writer(ctx.file_io->user, out_c, &sw) != KIT_OK) {
    645     driver_errf(GRAM_TOOL, "cannot open output: %s", out_c);
    646     driver_writer_abort(hw);
    647     kit_writer_close(hw);
    648     rc = 1;
    649     goto done;
    650   }
    651   KitGramEmitOptions emit = {
    652       .header_path = out_h, .source_path = out_c, .prefix = prefix};
    653   KitStatus est = kit_gram_emit_c(compiled, &emit, hw, sw);
    654   if (est != KIT_OK) {
    655     driver_writer_abort(hw);
    656     driver_writer_abort(sw);
    657   }
    658   kit_writer_close(hw);
    659   kit_writer_close(sw);
    660   rc = est == KIT_OK ? 0 : 1;
    661 
    662 done:
    663   if (compiled) kit_gram_free(compiled);
    664   gram_free_str(&env, out_c);
    665   gram_free_str(&env, out_h);
    666   gram_free_str(&env, prefix);
    667   if (sbuf) driver_free(&env, sbuf, sbuf_len);
    668   if (loaded) driver_release_bytes(&env.file_io, &ld);
    669   driver_env_fini(&env);
    670   return rc;
    671 }