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:
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