Check a hand-written parser's grammar with a Lark LALR(1) copy and one shared sample file
When to use#
A language has a hand-written recursive-descent parser. Such a parser never reports an ambiguity: when two readings are possible it takes the branch it tries first, so users get a surprising parse and no error.
Steps#
Restate the grammar as a Lark grammar (
scripts/<lang>.lark) and build it withLark(grammar, parser="lalr", lexer="contextual", debug=True), collecting every shift/reduce and reduce/reduce conflict. Fail on any conflict.Keep the samples in one file both parsers read, for example
tests/<lang>/grammar_samples.<ext>: samples separated by a line---, and a line### invalidthat starts the samples that must not parse.The Lark script checks: no conflicts, every valid sample parses, every invalid sample fails. The native test checks the same file, plus the round trip parse, print, parse and tree equality.
Run the Lark script without installing anything:
uv run --no-project --with lark python scripts/check_<lang>_grammar.py --samples tests/<lang>/grammar_samples.<ext>.Register it as a CTest that is skipped, not failed, when
uvis missing:find_program(UV_EXECUTABLE uv) if(UV_EXECUTABLE) add_test(NAME lang_grammar COMMAND ${UV_EXECUTABLE} run --no-project --with lark python ${CMAKE_SOURCE_DIR}/scripts/check_lang_grammar.py --samples ${CMAKE_CURRENT_SOURCE_DIR}/grammar_samples.lang) else() add_test(NAME lang_grammar COMMAND ${CMAKE_COMMAND} -E echo "uv not found; grammar check skipped") set_tests_properties(lang_grammar PROPERTIES SKIP_REGULAR_EXPRESSION "skipped") endif()When the native parser gains a form, add a sample; the check then fails until the Lark copy accepts the same language, so the two cannot drift silently.
Evidence#
- dftracer-utils duql stage 12a1 (2026-09-27): the first Lark run found four real ambiguities in a grammar that had been designed and audited: sort
-(direction or unary minus, reduce/reduce),pivot k in [..]versus theinoperator, a legacy"text" in fieldform versusx in [..], and optional,separators between block members. Later, a sample fortags.0found that the Lark copy lacked numeric path keys. - Final state: 55 valid and 20 invalid samples, no conflicts,
duql_grammarCTest.