Leniency and Strict Mode
Lanexio™ Parser is a lenient parser by default. Every grammar entry point
recovers from malformed input instead of throwing, producing LexError nodes
in the tree. Four grammar surfaces also expose a strict parse option that
surfaces spec-level rejections as flags on the tree: CSV/TSV, TOML, and XML via
mode, and the tsql dialect of SQL via clientBatch. This page is the policy
reference; the per-grammar guides show the option in each entry point’s shape.
How the mode option works
Section titled “How the mode option works”The mode option takes "lenient" (the default) or "strict". Since ADR
0044, the { strict: true } alias is accepted as a convenience spelling: it
maps onto mode: "strict" and never replaces it. The two spellings resolve to
the same value through one resolver, so they cannot behave differently. Both
spellings are accepted in two shapes each:
// Direct shape on the grammar entry pointparseCsv(bytes, { mode: 'strict' });parseXml(bytes, { mode: 'strict' });parseToml(bytes, { mode: 'strict' });parseCsv(bytes, { strict: true });parseXml(bytes, { strict: true });parseToml(bytes, { strict: true });
// Unified shape through the registry dispatcherparse(src, { language: 'csv', grammarOptions: { mode: 'strict' } });parse(src, { language: 'xml', grammarOptions: { mode: 'strict' } });parse(src, { language: 'toml', grammarOptions: { mode: 'strict' } });parse(src, { language: 'csv', strict: true });parse(src, { language: 'xml', strict: true });parse(src, { language: 'toml', strict: true });Precedence is mode > strict > default lenient. An explicit mode always
wins, including over a contradictory strict: { mode: "lenient", strict: true } parses lenient and { mode: "strict", strict: false } parses strict.
strict: false is the explicit lenient spelling, so callers who want to be
explicit about leniency can say so. An absent or non-boolean strict falls
back to the default lenient mode.
ParseMode ("lenient" | "strict") is a shared primitive in parser-core
(ADR 0033), and pickParseMode reads the options structurally, so an unknown
mode value is treated exactly like lenient.
Beyond mode and strict, the unified parse() entry point accepts three
more grammar pass-through convenience keys, all resolved by the same merge rule
(ADR 0048): validate (XML DTD validation), clientBatch (the tsql boundary
modes in the table below), and spec (TOML version alias). An explicit
grammarOptions.<key> always wins over the top-level key of the same name.
When grammarOptions is absent, a set top-level key synthesizes
{ grammarOptions: { key } }. When grammarOptions is present, the merged bag
is a shallow copy and never mutates the caller’s options object. The SQL
grammar’s dialect key follows the same rule, and an explicit
grammarOptions.mode still wins over the strict convenience key at
pickParseMode.
Strict mode never throws. A rejected construct is a flag on the tree
(hasError on the node, and on the document root where the grammar documents
it), never an exception. Lenient output is byte-identical to previous
releases.
Per-grammar policy
Section titled “Per-grammar policy”| Grammar | Lenient accepts (recovers) | Strict rejects (flags) |
|---|---|---|
| CSV / TSV | unclosed quotes, uneven column counts | unclosed quotes and records whose field count differs from the reference row (header row when header is true, otherwise the first record) |
| XML | unbound namespace prefixes, DTD validity gaps | unbound element/attribute prefixes (unconditional namespace-prefix well-formedness) and internal-subset DTD violations (VC Root Element Type, element content models, attribute constraints) when an internal subset is present |
| TOML | TOML v1.1.0 constructs (the v1.1 default grammar) | TOML 1.1-only constructs (seconds-less datetimes/times, inline-table newlines and trailing commas, \xHH byte escapes) by defaulting to the 1.0.0 rule set when no explicit version is given; an explicit version always wins |
| SQL (tsql) | a batch-boundary GO and a sqlcmd :directive line, accepted by default | clientBatch: "reject" flags each boundary with an Error node while later statements still materialize; clientBatch: "split" emits first-class SqlKind.BatchBoundary and SqlKind.ClientDirective leaves so a loader can slice one file into batches. The surface is tsql-only: the other dialects ignore clientBatch and strict, and strict: true maps to "reject" when clientBatch is absent |
Grammars without a strict profile (HTML, Markdown, MDX, JSON/JSONC/JSON5, YAML,
CSS, GraphQL) run in lenient mode only: malformed input produces error
leaves, and the parse path never throws. SQL is deliberately absent from that
list: the tsql clientBatch boundary is a strict surface (see the table
above), so SQL is not lenient-only. Duplicate-key strictness for YAML,
external-DTD resolution and content-model/standalone-VC coverage for XML, and
a strict-mode profile for the remaining grammars are roadmap candidates, not
1.0 commitments (ADR 0043).
Why lenient by default
Section titled “Why lenient by default”Leniency is the recovery contract that keeps editors and tools responsive:
malformed input becomes a tree with error nodes instead of a crash or a
partial result. The cost is that some invalid documents parse clean. Strict
mode is the conformance lens: it trades some recovery for spec-level flagging
when a consumer (a linter, a validator, a test harness) needs to reject
marginal input. XML’s conformance contract in particular is the
mode: "strict" profile (ADR 0041).
Related
Section titled “Related”The mode design is recorded in ADR 0033 (per-grammar strict / lenient modes),
the v1.0 niche and roadmap in ADR 0043, the { strict: true } alias in
ADR 0044, and the top-level convenience-key merge convention in ADR 0048, all
under docs/decisions/.