BNF, EBNF and ABNF mapping
How markup-bnf-writer maps the Grammar model to BNF-family source text.
Unlike the other writers, this one serves three notations from one model. They are the same format punctuated differently, so the notation is a BnfFlavor parameter on the four output forms rather than three separate writers:
grammar.toBnf() // EBNF — the default
grammar.toBnf(BnfFlavor.BNF)
grammar.toBnf(BnfFlavor.ABNF)
EBNF is the default because it is the only flavor that maps 1:1 to the model: it desugars nothing, so one rule in the model is one rule in the output.
Notation punctuation
| Element | BNF | EBNF (ISO 14977) | ABNF (RFC 5234) |
|---|---|---|---|
defines |
|
|
|
alternation |
|
|
|
concatenation |
space |
|
space |
rule terminator |
none |
|
none |
rule name |
|
|
|
comment |
none — dropped |
|
|
Model mapping
| Model element | Produced source | Notes |
|---|---|---|
|
(* Arith *) |
a leading comment line, in the flavors that have comments. Dropped entirely in BNF. |
|
(* the entry point *) |
a comment line directly above its rule. Dropped in BNF. |
|
|
see Terminal quoting. |
|
|
emitted verbatim; a reference to an undeclared rule is written as-is, not validated. |
|
|
the empty string (ε). |
|
|
native in ABNF only; see Ranges. |
|
|
joined by the flavor’s concatenation punctuation. |
|
|
joined by the flavor’s alternation punctuation. |
|
|
native in EBNF and ABNF; becomes a synthetic rule in BNF. |
|
|
see Repetition. |
Grouping and precedence
The model has no grouping node — grouping is a printing concern, not a semantic one. The writer parenthesizes by precedence instead: a Choice binds looser than a Seq, which binds looser than an atom. Parentheses appear only where they are needed:
grammar {
rule("a") {
ref("head")
choice { +"x"; +"y" }
}
}
a = head, ("x" | "y"); (* EBNF — the choice needs parentheses inside a concatenation *)
a = x | y; (* a top-level choice does not *)
Classic BNF has no parentheses at all, which is why it desugars instead.
Repetition
EBNF and ABNF map Repeat natively, but with different reach. ABNF has a true min/max form; ISO EBNF has only {x} (zero or more) and n * x (exactly n), so bounds are spelled out:
Repeat |
EBNF | ABNF |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Classic BNF desugaring
Classic BNF has no optional, repetition or grouping syntax. Every Optional, every Repeat, and every Choice nested inside a Seq therefore becomes a synthetic rule, emitted directly after the rule that induced it. BNF is the only flavor whose emitted rule count differs from the model’s.
| Model | Synthetic rule |
|---|---|
|
|
|
|
|
|
|
a choice over each permitted count, then desugared — linear in |
|
|
Synthetic names derive from the enclosing rule (expr becomes <expr-opt>, <expr-rep>, <expr-alt>), with a counter appended if that name is already taken by a declared or previously generated rule. Naming is deterministic, so output is stable across runs.
grammar {
rule("expr") {
ref("term")
zeroOrMore {
choice { +"+"; +"-" }
ref("term")
}
}
}
expr = term, {("+" | "-"), term}; (* EBNF — one rule *)
<expr> ::= <term> <expr-rep>
<expr-rep> ::= <expr-alt> <term> <expr-rep> | ""
<expr-alt> ::= "+" | "-"
Terminal quoting
No notation in this family has a string escape, so a terminal is written by choosing a quote rather than escaping one:
Terminal text |
Written as (BNF/EBNF) |
|---|---|
|
|
|
|
|
|
|
|
A terminal that splits is a concatenation, and is parenthesized like one when it appears where an atom is required.
ABNF terminals
ABNF adds two constraints. Its quoted strings admit only %x20-21 / %x23-7E, so anything else — a quote, a newline, any non-ASCII character — leaves the string form for a %xNN concatenation. And RFC 5234 quoted strings are case-insensitive, which the model’s terminals are not: a terminal containing an ASCII letter is therefore emitted as an RFC 7405 case-sensitive string, %s"…". This is the one place the writer reaches past RFC 5234, and it is deliberate — plain "abc" would mean something the model does not.
Terminal text |
Written as (ABNF) |
|---|---|
|
|
|
|
|
|
|
|
Ranges
Range is native in ABNF (%x41-5A) and has no syntax in BNF or EBNF, where it expands into a choice of single-character terminals:
digit = %x30-33 ; ABNF
digit = "0" | "1" | "2" | "3"; (* EBNF *)
The expansion is capped at 256 code points. A wider range throws IllegalArgumentException rather than emitting an unusably large grammar — so range(0, 0x10FFFF) writes fine as ABNF but is rejected by the other two flavors. This is the only case where a valid model succeeds in one flavor and fails in another, and the only writer in this library that throws.
Rule names
Rule names are emitted verbatim. Because none of these notations can escape a name, a name carrying punctuation that would break the output structurally is rejected with IllegalArgumentException rather than transformed — silently rewriting a name could collapse two distinct rules into one and produce a wrong grammar.
| Flavor | Rejected |
|---|---|
BNF |
empty names, and names containing |
EBNF |
empty names, and names containing any of |
ABNF |
names not matching |
ISO 14977 strictly permits only letters, digits and spaces in a meta identifier, so if-stmt is technically non-conforming EBNF. The writer emits it anyway: every EBNF tool in practice accepts hyphens, and rejecting them would make the writer useless.
Flow chunking
bnfFlow(flavor) emits chunks in this order (concatenation equals toBnf(flavor) exactly; boundaries are an implementation detail):
-
the grammar-name comment, when the grammar has a name and the flavor has comments — one chunk
-
one chunk per emitted rule, each including its comment line
In BnfFlavor.BNF the second step includes synthetic rules, so the same model yields more chunks in BNF than in EBNF or ABNF.
See also
-
Writer API contract — the four output forms and writer options
-
Modules and coordinates —
markup-grammarandmarkup-bnf-writer