Documentation
¶
Overview ¶
Package bnf is the Go port of @tabnas/bnf: the notation-neutral compiler shared by the BNF-family grammar front-ends (abnf, gbnf, ebnf). It compiles a grammar IR into a tabnas GrammarSpec and parses no syntax itself.
PORT STATUS: not yet implemented. The TypeScript implementation is canonical and lands first by design; this package currently exposes only VERSION so the module builds and the release tooling has something to check. The compiler itself — desugaring, left-recursion elimination, tail repeats, probe dispatch, literal lifting, token allocation, first sets and chain emission — is ported in a later change, mirroring ts/src/compiler.ts.
Until then, use github.com/tabnas/abnf/go, which still carries its own copy of that pipeline.
Index ¶
- Constants
- func AttachActionSlots(spec *tabnas.GrammarSpec, refNames []string) error
- func AttachActions(spec *tabnas.GrammarSpec, actions ActionsMap) error
- func BuiltinTokens() map[string]string
- func EmitGrammarSpec(grammar *Grammar, opts *ConvertOptions) (*tabnas.GrammarSpec, error)
- func EscapeRegexp(s string) string
- func IsEffectivelyCaseSensitive(el *Element) bool
- func IsProseName(name string) bool
- func MarkListing(spec *tabnas.GrammarSpec) string
- func RefsIn(alt Sequence, out map[string]bool)
- func SpecToData(spec *tabnas.GrammarSpec) map[string]any
- func SpecToDataErr(spec *tabnas.GrammarSpec) (map[string]any, error)
- func SpecToJSON(spec *tabnas.GrammarSpec, indent int) string
- func SpecToJSONErr(spec *tabnas.GrammarSpec, indent int) (string, error)
- func TermKey(el *Element) string
- func ToJsonic(value any, strict bool, indent int) string
- func ToPureSpec(spec *tabnas.GrammarSpec) (map[string]any, error)
- func ToRecognitionSpec(spec *tabnas.GrammarSpec) (map[string]any, error)
- type ActionError
- type ActionFn
- type ActionsMap
- type AmbiguityReport
- type CompileError
- type CompileOptions
- type ConvertOptions
- type ElemKind
- type Element
- type EmitError
- type Grammar
- type ParseError
- type ProbeDispatchSpec
- type ProbeHelperSpec
- type Production
- type Sequence
- type SrcSpan
- type TailRepeatSpec
Constants ¶
const MaxInfinity = 1 << 30
MaxInfinity stands in for the TS `Infinity` upper bound on repetition.
const VERSION = "0.1.9"
VERSION is this module's version. It MUST equal ts/package.json "version": the release orchestrator rewrites both, and the version test fails the build if they drift.
Variables ¶
This section is empty.
Functions ¶
func AttachActionSlots ¶
func AttachActionSlots(spec *tabnas.GrammarSpec, refNames []string) error
AttachActionSlots declares user-action slots by name on a pure-data spec, without supplying functions.
func AttachActions ¶
func AttachActions(spec *tabnas.GrammarSpec, actions ActionsMap) error
AttachActions attaches user semantic actions to a spec in place.
func BuiltinTokens ¶
BuiltinTokens maps a bareword name to the engine's lexer token.
func EmitGrammarSpec ¶
func EmitGrammarSpec( grammar *Grammar, opts *ConvertOptions) (*tabnas.GrammarSpec, error)
EmitGrammarSpec compiles a grammar IR into a tabnas GrammarSpec. This is the whole public surface a front-end needs: parse your notation into a *Grammar, then call this.
Opts.Tag should be the notation's own name ("abnf", "gbnf", "ebnf"), so emitted alts stay attributable and diagnostics name the syntax the user actually wrote.
func EscapeRegexp ¶
EscapeRegexp quotes the regex metacharacters in a literal.
func IsEffectivelyCaseSensitive ¶
IsEffectivelyCaseSensitive reports whether a literal's case actually matters — a literal with no ASCII letter is insensitive either way.
func IsProseName ¶
IsProseName reports whether a prose text is a compiler directive.
func MarkListing ¶
func MarkListing(spec *tabnas.GrammarSpec) string
MarkListing returns a human-readable listing of compiler-assigned marks.
func SpecToData ¶
func SpecToData(spec *tabnas.GrammarSpec) map[string]any
SpecToData converts a *GrammarSpec into a plain data tree (map/slice/scalar/regexHolder). Action/condition refs are emitted as their `@`-name strings. Closures in spec.Ref are dropped — like the TS CLI's JSON output, which serialises actions as FuncRef strings. Used by the CLI's default (spec-dump) mode.
func SpecToDataErr ¶ added in v0.1.5
func SpecToDataErr(spec *tabnas.GrammarSpec) (map[string]any, error)
SpecToDataErr is SpecToData with the failure surfaced.
specToData can now refuse a spec outright — an option holding a function cannot be emitted as data — and a dump API that swallowed that returned an EMPTY spec while reporting success, which is the silent-wrong-grammar failure this package exists to avoid. It also panicked outright on `data["ref"]` when the nil map met a spec with refs.
The signatures of SpecToData / SpecToJSON are kept so callers do not break; on failure they now yield nil / "" rather than a plausible lie, and this function is how a caller learns why.
func SpecToJSON ¶
func SpecToJSON(spec *tabnas.GrammarSpec, indent int) string
SpecToJSON renders a spec as JSON text (the CLI default output). Returns "" for a spec that cannot be represented; use SpecToJSONErr to learn why rather than emitting an empty grammar as if it were one.
func SpecToJSONErr ¶ added in v0.1.5
func SpecToJSONErr(spec *tabnas.GrammarSpec, indent int) (string, error)
SpecToJSONErr is SpecToJSON with the failure surfaced.
func ToPureSpec ¶
func ToPureSpec(spec *tabnas.GrammarSpec) (map[string]any, error)
ToPureSpec reduces a spec to a pure-data, function-free grammar that *keeps* the AST-building `$`-builtins (so the reloaded grammar still builds the full {rule, src, kids} tree), with `v` set to the engine's BUILTIN_SCHEMA_VERSION. It is the Go counterpart of the TS `toPureSpec` export (ts/src/compile.ts); where TS returns a GrammarSpec object, Go returns the generic pure-data tree (map[string]any / []any / scalars — see SpecToData / ToJsonic).
Requires a `Builtins: true` conversion: if any closures remain in spec.Ref it returns a *CompileError, matching the TS throw.
func ToRecognitionSpec ¶
func ToRecognitionSpec(spec *tabnas.GrammarSpec) (map[string]any, error)
ToRecognitionSpec strips a converted spec down to a function-free recognition grammar: AST-building hooks (`a`/`bo`/`bc` refs into spec.Ref and the tree `$`-builtins, plus their `k.node$`/`k.capture$` config) are dropped, and the result carries `v` set to the engine's BUILTIN_SCHEMA_VERSION. It is the Go counterpart of the TS `toRecognitionSpec` export (ts/src/compile.ts); where TS returns a GrammarSpec object, Go returns the generic pure-data tree (map[string]any / []any / scalars — see SpecToData / ToJsonic).
Grammars whose control logic is still closures (a probe dispatcher converted without `Builtins: true`) cannot be emitted as pure recognition data: those return a *CompileError listing the offending rules, matching the TS function's throw.
Types ¶
type ActionError ¶
type ActionError struct{ Message string }
ActionError is raised for a malformed or unresolvable action ref.
func (*ActionError) Error ¶
func (e *ActionError) Error() string
type ActionsMap ¶
ActionsMap maps action refs to a function or list of functions.
type AmbiguityReport ¶
type CompileError ¶
CompileError is raised when a grammar can't be compiled to a pure-data spec.
func (*CompileError) Error ¶
func (e *CompileError) Error() string
type CompileOptions ¶
type CompileOptions struct {
Start string
Tag string
Strict bool
Indent int
Recognition *bool // default true
}
CompileOptions controls compilation. Mirrors CompileOptions.
type ConvertOptions ¶
type ConvertOptions struct {
Start string
Tag string
Builtins bool
Marks bool
// WordKeywords makes a literal ending in a word character match only as a
// whole word: it is emitted as an anchored regex with a trailing `\b`
// guard so e.g. `option` does not match inside `optional`. Mirrors the TS
// `wordKeywords` option (which uses a `(?![A-Za-z0-9_])` lookahead; the Go
// engine's RE2 has no lookahead, so `\b` — equivalent here — is used).
WordKeywords bool
// Provenance emits `Meta["provenance"]` — the map from each generated
// rule name back to the author-written production it came from (see
// `Production.Origin`). DEFAULT TRUE, hence the pointer: the names are
// otherwise unattributable, and every tool that shows a rule name to a
// human needs it. Point it at false to keep an embedded grammar as
// small as possible. Mirrors the TS `provenance?: boolean`, which is
// likewise on unless explicitly `false`.
Provenance *bool
}
ConvertOptions controls emission. Each front-end passes its own Tag so emitted alts stay attributable to the notation they came from.
type ElemKind ¶
type ElemKind string
ElemKind is the discriminator for a Element.
const ( KindTerm ElemKind = "term" KindRef ElemKind = "ref" KindRegex ElemKind = "regex" KindOpt ElemKind = "opt" KindStar ElemKind = "star" KindPlus ElemKind = "plus" KindRep ElemKind = "rep" KindGroup ElemKind = "group" // KindToken is an engine builtin lexer token (e.g. #TX/#NR/#ST/#VL), // produced by normalizeBuiltinTokens. Its token name is held in Name and // is emitted verbatim into a rule's token sequence (no allocation, unlike // a literal term). KindToken ElemKind = "token" // KindProse is an RFC 5234 prose-val (`<free text>`). Prose is // informational: it describes a terminal in English rather than defining // one. The converter accepts it only as the entire body of a production // naming a builtin lexer token (`NR = <number>`), where it documents the // token the lexer already provides; resolveProseTerminals then drops the // production so refs resolve to that builtin. Anywhere else there is // nothing to compile, and it is an error. Text holds the prose body. KindProse ElemKind = "prose" )
type Element ¶
type Element struct {
Kind ElemKind
// Sp is where this element came from in the grammar source
// (front-end populated, nil when unrecorded). Rewrite passes share
// element objects by reference — cloneGrammar copies productions and
// alt slices but not the elements themselves — so a span recorded at
// parse time survives all the way to the emitter. Elements the
// compiler synthesises for itself (a group wrapper around
// left-recursion seeds, say) carry none, which is correct: the author
// wrote no such group. Mirrors the TS `Element.sp`.
Sp *SrcSpan
// term
Literal string
CaseSensitive bool // explicit %s flag (ABNF strings are insensitive by default)
HasCaseSens bool // whether CaseSensitive was set explicitly (TS optional flag)
// TokenName is the preferred lexer token name, set by liftLiteralTokens
// when this terminal came from a production that names it (`PL = "+"` ->
// `#PL`). Without it the emitter derives a name from the literal text,
// which for punctuation degrades to `#T`, `#T1`, …
TokenName string
// prose
Text string
// NumErr carries a deferred diagnostic from parseNumericValue: an
// alt-action has no error return, and panicking is no good either
// because the engine turns a panic into its own `tabnas/internal`
// wrapper. So the element is built anyway and parseAbnf reports this
// message once the parse is structurally complete. Unexported: it is an
// internal signal, not part of the AST.
NumErr string
// regex
Pattern string
Flags string
// ref
Name string
// Debt holds the suffix-debt counter mutations to emit on the alt that
// pushes this reference (`n: {<counter>: 1|0}`). Written by
// resolveSuffixDebts; see that pass for what the counter means. Nil on
// every reference in a grammar with no contested tail loop, which is all
// of them until one is detected. Mirrors the TS `debt` field.
Debt map[string]int
// opt / star / plus / rep
Inner *Element
Min int
Max int // MaxInfinity for unbounded
// DebtGuard names the suffix-debt counter guarding a star, set by
// eliminateDirectLeftRec on the tail loop it generates. desugar carries
// it onto the helper production the star becomes; resolveSuffixDebts
// then confirms or drops it. Mirrors the TS `debtGuard` field.
DebtGuard string
// group
Alts []Sequence
}
Element is one element of an ABNF sequence (a term, ref, regex, or EBNF sugar). Mirrors the TS AbnfElement union.
type EmitError ¶ added in v0.1.6
type EmitError struct {
Message string
// Rule is the rule being compiled when the failure was raised.
Rule string
// Sp is where in the grammar source, when the IR knew. Nil when the
// front-end recorded no span for the offending node.
Sp *SrcSpan
Cause error
}
EmitError is a compile failure that can say WHERE. Every diagnostic this compiler raises used to be a bare message whose only structure was the `diagName():` prefix, so a caller wanting to underline the offending text had nothing to read and had to parse the message.
Sp is populated only when the offending IR node carries a span, which means only when the front-end recorded one — so this is a strict improvement on every path and a change of behaviour on none. It implements `error` and the message text is unchanged, so existing error handling and message assertions keep working.
It sits alongside ParseError rather than replacing it, mirroring TypeScript: there, five author-facing throw sites became `EmitError` and the other eleven stayed a plain `Error`. The same five sites raise this here, and the rest still raise *ParseError or a bare `fmt.Errorf`.
NOTE one of those five (`eliminateDirectLeftRec`'s purely-left- recursive rule) PANICS in Go where TypeScript throws — inherited behaviour the ABNF front-end's suite pins. It panics with a *EmitError VALUE rather than a string precisely so the span survives the panic: a `recover()` that type-asserts gets the span, where one that only stringifies gets what it always got.
type Grammar ¶
type Grammar struct {
Productions []*Production
Ambiguities []AmbiguityReport
// `<remove>` directives. Remove names rules/tokens to drop; ClearAll is
// `<all> = <remove>`, which wipes the instance first. Mirrors the TS
// Grammar `remove` / `clearAll` fields.
Remove []string
ClearAll bool
}
func EliminateLeftRecursion ¶
EliminateLeftRecursion runs that pass alone, for a front-end that wants to inspect or test the rewritten IR.
type ParseError ¶
ParseError is raised by the shared compiler for a grammar the IR cannot express. Front-ends wrap or restamp it as they see fit.
func (*ParseError) Error ¶
func (e *ParseError) Error() string
func (*ParseError) Unwrap ¶
func (e *ParseError) Unwrap() error
type ProbeDispatchSpec ¶
type ProbeDispatchSpec struct {
ProbeRule string
Disambiguator *Element
WithBranch string
NoBranch string
}
ProbeDispatchSpec configures a synthesised dispatcher production for an ambiguous `[X D] Y` subsequence.
type ProbeHelperSpec ¶
type ProbeHelperSpec struct {
VocabElements []*Element
}
ProbeHelperSpec carries the vocabulary for a synthesised probe helper.
type Production ¶
type Production struct {
Name string
Alts []Sequence
Incremental bool
ProbeDisp *ProbeDispatchSpec
ProbeHelper *ProbeHelperSpec
// TailRepeat is set by rewriteTailRepeats on a production of the
// shape `X = prefix [ sep X ]` (all-terminal prefix and separator,
// self-ref last). The opt is removed from Alts (leaving just the
// prefix) and the separator elements are stashed here; the emitter
// compiles the production to a same-depth close-phase repeat
// (`r: X`) instead of the opt→group→push helper chain. Mirrors the
// TS `tailRepeat` flag.
TailRepeat *TailRepeatSpec
// DebtGuard is set by desugar on the star helper generated for a
// left-recursion tail loop whose greediness contests a suffix of the
// rule it was derived from, and confirmed by resolveSuffixDebts. Names
// the counter whose value must be zero for the loop to keep going.
// Mirrors the TS `debtGuard` production flag.
DebtGuard string
// DebtOwed lists the loop's own FIRST tokens that an enclosing suffix
// can actually compete for, set by resolveSuffixDebts alongside
// DebtGuard. Only the branches that could eat one of these are guarded:
// a multi-tail loop (`A = A "y" / A "w" / "x" A "y" / "z"`) owes a `"y"`
// and nothing else, so blocking its `"w"` branch as well would reject
// `xzwy`. Mirrors the TS `debtOwed` production flag.
DebtOwed []string
NodeKind string // "", "user", "core", "helper"
// RepeatHelper marks a synthetic production standing in for a
// repetition (`opt`/`star` and the tails of `plus`/`rep`), and the
// nullable tail helpers left factoring creates. Their terminating
// alternative is EMPTY, so it names no token — and the engine only
// offers a matcher at a position where the active rule names it.
// The emitter therefore guards that alternative with a FOLLOW-set
// peek, without which a repetition followed by a character class
// cannot terminate. See computeFollowSets.
RepeatHelper bool
// Origin is the author-written production this one descends from. Set
// by every pass that SYNTHESISES a production (desugar's sugar
// helpers, left factoring's `$fact` tails, the probe rewriter's
// dispatch branches) to the origin of the production being rewritten.
// EMPTY means the production is itself author-written — so the source
// rule is always `Origin or Name`, which is what originOf returns.
//
// A compiled grammar carries an order of magnitude more rules than the
// author wrote (a 12-production ABNF grammar emits 118), and every one
// of the extra names surfaces in rule stacks, hover and completion.
// Carrying the origin is what lets emitGrammarSpec export the map back
// out (`spec.Meta["provenance"]`) so a tool can name the user's rule
// instead of the machinery's. Mirrors the TS `Production.origin`.
Origin string
// Sp is where the author wrote this production, when the front-end
// records it. Element spans locate a term or a reference; this locates
// the rule as a whole, which is what an outline entry or a
// go-to-definition on a rule name needs. Synthesised productions carry
// none — Origin is how they are located, by naming the rule they
// descend from.
//
// A pointer, not a value: see SrcSpan. Nil means "not recorded", which
// a zero-valued span cannot mean.
//
// CAUTION: every pass that REBUILDS a production field by field has to
// carry this across, exactly as it carries Origin — a `&Production{…}`
// that forgets it silently drops the span. The passes that copy with
// `cp := *p` get it for free. Mirrors the TS `Production.sp`.
Sp *SrcSpan
}
type SrcSpan ¶ added in v0.1.6
SrcSpan is where an IR node came from in the front-end's grammar text.
S start offset, inclusive E end offset, exclusive R row of the start, 1-based (optional) C column of the start, 1-based (optional)
Offsets and row/column are in the SAME UNITS the front-end's own engine tokens use, so a front-end copies `sI`/`rI`/`cI` straight across with no arithmetic — the step where an off-by-one would otherwise creep in. That does mean the units are runtime-native and not identical across ports: Go offsets count BYTES and TypeScript's count UTF-16 code units, the same divergence the engine already records for token positions. A consumer that needs LSP positions converts at the LSP boundary, where the document's encoding is known; nothing here can do that conversion correctly, because the IR does not hold the source text.
R and C are 1-based, so a zero in either means "not recorded" — there is no row 0. The span ITSELF is optional a level up: `Element.Sp` and `Production.Sp` are POINTERS, because `SrcSpan{S: 0, E: 0}` is a legitimate empty span at the very start of a file and must not read as "no span".
Spans are optional everywhere. A front-end that records them gets ranged compile errors (see `EmitError.Sp`); one that does not compiles to exactly the same grammar. Mirrors the TS `SrcSpan`.
type TailRepeatSpec ¶
type TailRepeatSpec struct {
Sep Sequence
}