kit

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

commit 72567eb207a5b848e9066eba1837f61d96b96004
parent 9ff956a2e8556a872bec0633637982a51b4b88e7
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Thu, 16 Jul 2026 10:12:54 -0700

gram: report structured command-line errors

Diffstat:
Mdriver/cmd/gram.c | 188++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mtest/gram/run_driver.sh | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 196 insertions(+), 45 deletions(-)

diff --git a/driver/cmd/gram.c b/driver/cmd/gram.c @@ -40,6 +40,22 @@ typedef struct { int parser_recover; } GramArgs; +typedef enum GramArgErrorCategory { + GRAM_ARG_OK = 0, + GRAM_ARG_UNKNOWN_OPTION, + GRAM_ARG_MISSING_OPTION_VALUE, + GRAM_ARG_INVALID_NUMERIC_VALUE, + GRAM_ARG_DUPLICATE_GRAMMAR, + GRAM_ARG_MISSING_GRAMMAR, +} GramArgErrorCategory; + +typedef struct GramArgError { + GramArgErrorCategory category; + const char* option; + const char* value; + const char* previous; +} GramArgError; + void driver_help_gram(void) { driver_printf( "%.*s", @@ -104,11 +120,8 @@ void driver_help_gram(void) { " KitGramParser. The generated header is the authority for exact\n" " enum and symbol names.\n" "\n" - "CURRENT CLI LIMITS\n" - " The common `--` end-of-options marker is not accepted here; spell\n" - " a leading-dash grammar as ./-grammar.ebnf. Unknown flags or a\n" - " missing option argument currently print only the generic usage\n" - " line, so compare the invocation with this option list.\n" + "PATHS\n" + " Use -- before a leading-dash grammar file name.\n" "\n" "GETTING HELP\n" " -h, --help Show this help and exit\n" @@ -144,7 +157,7 @@ void driver_help_gram(void) { static int gram_parse_size(const char* s, size_t* out) { uint64_t v; - if (driver_parse_u64(s, &v) != 0) return 0; + if (driver_parse_u64(s, &v) != 0 || v > SIZE_MAX) return 0; *out = (size_t)v; return 1; } @@ -163,10 +176,12 @@ static int gram_parse_double(const char* s, double* out) { * (prefix len `plen` covers "--name="). Returns the value or NULL on no match; * on a match that needs the next arg but argc runs out, sets *err. */ static const char* opt_val(int argc, char** argv, int* i, const char* name, - const char* eqname, size_t plen, int* err) { + const char* eqname, size_t plen, + GramArgError* error) { if (driver_streq(argv[*i], name)) { if (++*i >= argc) { - *err = 1; + error->category = GRAM_ARG_MISSING_OPTION_VALUE; + error->option = name; return NULL; } return argv[*i]; @@ -175,13 +190,39 @@ static const char* opt_val(int argc, char** argv, int* i, const char* name, return NULL; } -static int parse_args(int argc, char** argv, GramArgs* a) { +static int gram_invalid_numeric(GramArgError* error, const char* option, + const char* value) { + error->category = GRAM_ARG_INVALID_NUMERIC_VALUE; + error->option = option; + error->value = value; + return 0; +} + +static int gram_set_grammar(GramArgs* a, GramArgError* error, + const char* value) { + if (a->grammar) { + error->category = GRAM_ARG_DUPLICATE_GRAMMAR; + error->previous = a->grammar; + error->value = value; + return 0; + } + a->grammar = value; + return 1; +} + +static int parse_args(int argc, char** argv, GramArgs* a, + GramArgError* error) { + int options = 1; memset(a, 0, sizeof *a); + memset(error, 0, sizeof *error); for (int i = 1; i < argc; i++) { const char* arg = argv[i]; const char* v; - int err = 0; - if (driver_streq(arg, "--dump-sexpr")) { + if (options && driver_streq(arg, "--")) { + options = 0; + } else if (!options) { + if (!gram_set_grammar(a, error, arg)) return 0; + } else if (driver_streq(arg, "--dump-sexpr")) { a->dump_sexpr = 1; } else if (driver_streq(arg, "--multiline")) { a->multiline = 1; @@ -198,63 +239,120 @@ static int parse_args(int argc, char** argv, GramArgs* a) { } else if (driver_streq(arg, "--sample-tokens")) { a->sample_tokens = 1; } else if ((v = opt_val(argc, argv, &i, "--sample-traces", - "--sample-traces=", 16, &err))) { + "--sample-traces=", 16, error))) { a->sample_traces = v; - } else if (err) { + } else if (error->category) { return 0; } else if ((v = opt_val(argc, argv, &i, "--stop-prob", "--stop-prob=", 12, - &err))) { - if (!gram_parse_double(v, &a->stop_prob)) return 0; + error))) { + if (!gram_parse_double(v, &a->stop_prob)) + return gram_invalid_numeric(error, "--stop-prob", v); a->have_stop_prob = 1; - } else if (err) { + } else if (error->category) { return 0; } else if ((v = opt_val(argc, argv, &i, "--samples", "--samples=", 10, - &err))) { - if (!gram_parse_size(v, &a->samples)) return 0; - } else if (err) { + error))) { + if (!gram_parse_size(v, &a->samples)) + return gram_invalid_numeric(error, "--samples", v); + } else if (error->category) { return 0; - } else if ((v = opt_val(argc, argv, &i, "--seed", "--seed=", 7, &err))) { - if (driver_parse_u64(v, &a->seed) != 0) return 0; - } else if (err) { + } else if ((v = opt_val(argc, argv, &i, "--seed", "--seed=", 7, error))) { + if (driver_parse_u64(v, &a->seed) != 0) + return gram_invalid_numeric(error, "--seed", v); + } else if (error->category) { return 0; } else if ((v = opt_val(argc, argv, &i, "--max-depth", "--max-depth=", 12, - &err))) { - if (!gram_parse_size(v, &a->max_depth)) return 0; - } else if (err) { + error))) { + if (!gram_parse_size(v, &a->max_depth)) + return gram_invalid_numeric(error, "--max-depth", v); + } else if (error->category) { return 0; } else if ((v = opt_val(argc, argv, &i, "--max-repeat", "--max-repeat=", 13, - &err))) { - if (!gram_parse_size(v, &a->max_repeat)) return 0; - } else if (err) { + error))) { + if (!gram_parse_size(v, &a->max_repeat)) + return gram_invalid_numeric(error, "--max-repeat", v); + } else if (error->category) { return 0; } else if ((v = opt_val(argc, argv, &i, "--max-tokens", "--max-tokens=", 13, - &err))) { - if (!gram_parse_size(v, &a->max_tokens)) return 0; - } else if (err) { + error))) { + if (!gram_parse_size(v, &a->max_tokens)) + return gram_invalid_numeric(error, "--max-tokens", v); + } else if (error->category) { return 0; - } else if ((v = opt_val(argc, argv, &i, "-o", "-o", 2, &err))) { + } else if ((v = opt_val(argc, argv, &i, "-o", "-o", 2, error))) { a->out_c = v; - } else if (err) { + } else if (error->category) { return 0; - } else if ((v = opt_val(argc, argv, &i, "--header", "--header=", 9, &err))) { + } else if ((v = opt_val(argc, argv, &i, "--header", "--header=", 9, + error))) { a->out_h = v; - } else if (err) { + } else if (error->category) { return 0; - } else if ((v = opt_val(argc, argv, &i, "--prefix", "--prefix=", 9, &err))) { + } else if ((v = opt_val(argc, argv, &i, "--prefix", "--prefix=", 9, + error))) { a->prefix = v; - } else if (err) { + } else if (error->category) { return 0; } else if (driver_streq(arg, "-")) { - if (a->grammar) return 0; - a->grammar = arg; + if (!gram_set_grammar(a, error, arg)) return 0; } else if (arg[0] == '-' && arg[1]) { + error->category = GRAM_ARG_UNKNOWN_OPTION; + error->option = arg; return 0; } else { - if (a->grammar) return 0; - a->grammar = arg; + if (!gram_set_grammar(a, error, arg)) return 0; + } + } + if (!a->grammar) { + error->category = GRAM_ARG_MISSING_GRAMMAR; + return 0; + } + return 1; +} + +static void gram_print_arg_error(const GramArgError* error) { + switch (error->category) { + case GRAM_ARG_UNKNOWN_OPTION: { + const char* const options[] = { + "--dump-sexpr", "--multiline", "--lexer-standalone", + "--fold-keywords", "--position-lazy", "--parser-codegen", + "--parser-recover", "--sample-tokens", "--sample-traces", + "--stop-prob", "--samples", "--seed", + "--max-depth", "--max-repeat", "--max-tokens", + "-o", "--header", "--prefix", + "-h", "--help", "--version", + }; + DriverSuggestion suggestions[3]; + size_t n = driver_suggest_values( + error->option, options, sizeof options / sizeof options[0], + suggestions, 3); + if (n) + driver_errf(GRAM_TOOL, "unknown option: %s; did you mean '%s'?", + error->option, suggestions[0].value); + else + driver_errf(GRAM_TOOL, "unknown option: %s", error->option); + break; } + case GRAM_ARG_MISSING_OPTION_VALUE: + driver_errf(GRAM_TOOL, "option requires an argument: %s", error->option); + break; + case GRAM_ARG_INVALID_NUMERIC_VALUE: + driver_errf(GRAM_TOOL, "invalid numeric value '%s' for %s", error->value, + error->option); + break; + case GRAM_ARG_DUPLICATE_GRAMMAR: + driver_errf(GRAM_TOOL, "multiple grammar inputs: %s and %s", + error->previous, error->value); + break; + case GRAM_ARG_MISSING_GRAMMAR: + driver_errf(GRAM_TOOL, "missing grammar input"); + break; + case GRAM_ARG_OK: + driver_errf(GRAM_TOOL, "invalid command line"); + break; } - return a->grammar != NULL; + driver_errf(GRAM_TOOL, + "usage: kit gram [options] grammar.ebnf (see --help)"); } /* ---- owned-string helpers (env heap; freed via gram_free_str) ------------- */ @@ -455,6 +553,7 @@ int driver_gram(int argc, char** argv) { DriverEnv env; KitContext ctx; GramArgs a; + GramArgError arg_error; DriverLoad ld = {0}; uint8_t* sbuf = NULL; size_t sbuf_len = 0; @@ -469,9 +568,8 @@ int driver_gram(int argc, char** argv) { driver_help_gram(); return 0; } - if (!parse_args(argc, argv, &a)) { - driver_errf(GRAM_TOOL, - "usage: kit gram [options] grammar.ebnf (see --help)"); + if (!parse_args(argc, argv, &a, &arg_error)) { + gram_print_arg_error(&arg_error); return 2; } diff --git a/test/gram/run_driver.sh b/test/gram/run_driver.sh @@ -39,6 +39,59 @@ fi # Missing grammar operand is a usage error (exit 2). expect_exit 2 "no grammar => usage error" -- "$KIT" gram --dump-sexpr +if grep -q "missing grammar input" "$DIR/err"; then + ok "missing grammar diagnostic" +else + bad "missing grammar diagnostic" +fi + +# Structured usage errors name the option/value before the synopsis and use +# the shared bounded suggestion helper without touching outputs. +rm -f "$DIR/should-not-exist.c" +expect_exit 2 "unknown option => usage error" -- "$KIT" gram \ + -o "$DIR/should-not-exist.c" --hlep "$SRC/calc.ebnf" +if grep -q "unknown option: --hlep; did you mean '--help'" "$DIR/err"; then + ok "unknown option diagnostic + suggestion" +else + bad "unknown option diagnostic + suggestion" +fi +if [ ! -e "$DIR/should-not-exist.c" ]; then + ok "usage parse completes before output" +else + bad "usage parse completes before output" +fi + +expect_exit 2 "missing -o value => usage error" -- "$KIT" gram -o +if grep -q "option requires an argument: -o" "$DIR/err"; then + ok "missing option argument diagnostic" +else + bad "missing option argument diagnostic" +fi + +expect_exit 2 "invalid numeric => usage error" -- "$KIT" gram \ + --samples nope "$SRC/calc.ebnf" +if grep -q "invalid numeric value 'nope' for --samples" "$DIR/err"; then + ok "invalid numeric diagnostic" +else + bad "invalid numeric diagnostic" +fi + +expect_exit 2 "duplicate grammar => usage error" -- "$KIT" gram \ + "$SRC/calc.ebnf" "$SRC/features.ebnf" +if grep -q "multiple grammar inputs" "$DIR/err"; then + ok "duplicate grammar diagnostic" +else + bad "duplicate grammar diagnostic" +fi + +cp "$SRC/calc.ebnf" "$DIR/-grammar.ebnf" +(cd "$DIR" && "$KIT" gram --dump-sexpr -- -grammar.ebnf) \ + > "$DIR/dash.sexpr" 2> "$DIR/dash.err" +if [ "$?" -eq 0 ] && grep -q '(grammar' "$DIR/dash.sexpr"; then + ok "-- leading-dash grammar" +else + bad "-- leading-dash grammar" +fi # Nonexistent input is an I/O error (exit 1). expect_exit 1 "missing file => error" -- "$KIT" gram /no/such/grammar.ebnf