ADR 0055 — Directive integer arguments: positive-integer bounds
- Status: accepted
- Date: 2026-05-22
- Spec target: XTL 0.1
- Affects: grammar.ebnf, language.md § "Top" / "Repeat Right", ADR-0027
Context
grammar.ebnf defines:
top_directive = "@top" , integer ;
repeat_directive = "@repeat" , "right" , [ integer ] ;
integer = digit_seq ;
digit_seq = digit , { digit } ;
The integer production therefore allows 0. The grammar has no
explicit upper bound (which is correct — it is a parsing surface,
not a semantic constraint). But it also has no statement that 0
is semantically invalid.
language.md § "Top" says "Keeps the first N rows after filters and
sorts." For N = 0, the meaning is ambiguous: "the first zero rows"
could mean "produce zero rendered rows" (one reasonable
interpretation) or "the default is no truncation" (another
reasonable interpretation given 0 often means "unset" in CLI
conventions).
language.md § "Repeat Right" says "the column span per repeated
record; when omitted, the column span is 1." A value of 0 is
similarly ambiguous.
The reference impl (src/directive-parser.ts:185-198) rejects both
via n <= 0 and falls through to the generic
xl3/directive/invalid-syntax:
function parseTop(body: string): Directive | null {
const n = parseInt(body, 10);
if (isNaN(n) || n <= 0) return null;
...
}
function parseRepeat(body: string): Directive | null {
const match = body.match(/^right(?:\s+(\d+))?$/i);
if (!match) return null;
const colSpan = match[1] ? parseInt(match[1], 10) : 1;
if (colSpan <= 0) return null;
return { kind: 'repeat', direction: 'right', colSpan };
}
The impl rejects 0, but the spec does not. The grammar
silently allows 0. The error message is the generic
xl3/directive/invalid-syntax rather than a precise
"integer must be ≥ 1" diagnostic.
This is a small surface, but it leaves three rough edges:
@top 0is grammar-legal but impl-rejected — no spec sentence says which is canonical.- The
digit_seqproduction trivially admits00,007, etc. — leading zeros are unstated. - Negative integers are excluded by
digit_seq(no-prefix in integer position), but no normative sentence says so.
Considered Options
A. Grammar-level: replace integer in directive positions with a
new positive_integer production. Pro: parser-level rejection;
no semantic ambiguity. Con: grammar surface grows by one production.
B. Prose-level: keep integer in grammar, add a "MUST be ≥ 1"
sentence to language.md. Pro: minimal grammar change. Con:
"semantics rejects what grammar accepts" is the same shape that
ADR-0027 audited as user-hostile.
C. Status quo (impl checks, spec silent). Worst — silent divergence between grammar and impl.
Decision
Adopt A.
Grammar changes
Add a new production:
positive_integer
(* A non-empty sequence of decimal digits whose parsed value is
* ≥ 1. Leading zeros are NOT permitted (`05` is a parse error).
* Used in directive arguments where 0 / negative makes no sense. *)
= non_zero_digit , { digit } ;
non_zero_digit
= "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
Update directive productions to use it:
top_directive = "@top" , positive_integer ;
repeat_directive = "@repeat" , "right" , [ positive_integer ] ;
integer continues to exist (used by number_literal's digit
sequences); positive_integer is additive.
Normative prose (added to language.md)
- § "Top": "
@top Nkeeps the first N rows after filters and sorts. N MUST be a positive integer (≥ 1).@top 0,@top -5, and@top 05are parse errors raisingxl3/directive/invalid-syntax." - § "Repeat Right": "
@repeat right N(and the default-1form@repeat right) requires N to be a positive integer (≥ 1).@repeat right 0and@repeat right -3are parse errors raisingxl3/directive/invalid-syntax."
Why ≥ 1 and not ≥ 0
A @top 0 block produces zero rendered rows, but the rest of the
sheet still renders. Authors who want to test the "no data" path
typically use an empty source workbook (already a tested shape per
ADR-0021 § "Empty source data"); @top 0 would be a strictly
weaker tool that adds a second code path for the same outcome.
Reject the redundancy.
A @repeat right 0 block has no semantic meaning at all (zero-
width per-record span). Reject.
Diagnostic message
When parseTop / parseRepeat rejects on the bound:
@topinteger must be ≥ 1; got0@repeat rightcolumn span must be ≥ 1; got0
The error code remains xl3/directive/invalid-syntax per ADR-0027
(single-code-for-directive-parse-failures policy). The message
substring is stable for fixtures.
Implementation note — leading-zero detection
The reference impl previously used parseInt(body, 10) which
accepts leading zeros (parseInt("05", 10) === 5). The new
positive_integer production REQUIRES that leading-zero forms be
rejected, so an impl MUST add an explicit pre-check before
delegating to parseInt:
if (!/^[1-9][0-9]*$/.test(body.trim())) {
throw xtlError('xl3/directive/invalid-syntax',
`@top integer must be ≥ 1 and have no leading zeros; got "${body}"`);
}
A pure parseInt-based check is insufficient. Ports MUST add the
shape pre-check to match the grammar.
Consequences
- Templates with
@top 0or@repeat right 0(none observed in the production corpus) now error at parse with a precise diagnostic. grammar.ebnfgains thepositive_integerproduction; the new directive productions reference it.- Conformance fixture additions:
153-top-zero-error—@top 0raisesxl3/directive/invalid-syntaxwith the "must be ≥ 1" substring.154-repeat-right-zero-error—@repeat right 0raises the same.155-top-leading-zero-error—@top 05raises the same.
- No new error code. Diagnostic substring is the contract.
References
- ADR-0021 — Implementation-defined boundaries (the "spec-explicit vs. impl-implicit" pattern this ADR closes)
- ADR-0027 — Reserved column names + directive validation (the silent-fallthrough audit-pass theme)
- grammar.ebnf §
top_directive,repeat_directive,integer - language.md § "Top", "Repeat Right"
src/directive-parser.ts§parseTop,parseRepeat