S12) The EBNF of BPscript
Formal Grammar of the Language
Key Productions
A language in active development has divergences between the spec and the code. This article honestly documents them — what is implemented, what is planned, what diverges.
Where does this article fit in?
Technical reference. For EBNF notation, see L3. For BP3’s EBNF, see B10. Full specification: BPSCRIPT_EBNF.md (v0.6).
The Root
scene = { directive | actor_directive | declaration | cv_instance
| macro | backtick_orphan | comment }
, subgrammar+ , [ template_section ] ;
Directives
directive = "@" , directive_body ;
actor_directive = "@actor" , IDENT , actor_props+ ;
actor_props = IDENT , ":" , actor_value ;
actor_value = IDENT [ "(" , param_pairs , ")" ] ;
directive_body = IDENT (* @core *)
| "controls" (* @controls *)
| "mode" , ":" , MODE (* @mode:random *)
| IDENT , ":" , value (* @tempo:120 *)
;
value = INT | FLOAT | neg_number | IDENT | ratio ;
neg_number = "-" , ( INT | FLOAT ) ;
ratio = INT , "/" , INT ;
The @actor directive is the central concept (S3):
@actor sitar alphabet:sargam scale:sargam_22shruti transport:webaudio
Known properties of an actor: alphabet (required), scale (range/degrees → pitch via temperament,
if the actor has pitches), sounds (definitions by terminal: timbre, percussions, samples), transport (required),eval (backtick runtime; if omitted, the actor’s backticks are not evaluated — default null).
Subgrammars and Rules
subgrammar = rule+ , [ separator ] ;
separator = "-----" , { "-" } ;
mode_directive = "@mode:" , MODE , [ "(" , mode_modifier , { "," , mode_modifier } , ")" ] ;
MODE = "ord" | "random" | "lin" | "sub1" | "sub" | "tem" | "poslong" ;
rule = [ guard ] , { context } , lhs , arrow , rhs
, [ runtime_qualifier ] , { qualifier } ;
guard = "[" , flag_expr , "]" ;
flag_expr = IDENT | IDENT , comparator , value ;
arrow = "->" | "<-" | "<>" ;
The mode is a block directive @mode:X that applies to the following subgrammar, up to the
next ----- separator. It is not an inline qualifier of the rule.
Qualifiers — [] engine vs () runtime
engine_qualifier = "[" , engine_pair , { "," , engine_pair } , "]"
| "[" , tempo_op , "]" ;
engine_pair = ENGINE_KEY , ":" , raw_value | ENGINE_KEY ;
ENGINE_KEY = "mode" | "scan" | "speed" | "weight" | "on_fail"
| "tempo" | "meter" | "scale" | "retro" | "rotate"
| "keyxpand" | "repeat" | "failed" | "stop" | "goto"
| "striated" | "smooth" ;
runtime_qualifier = "(" , runtime_pair , { "," , runtime_pair } , ")" ;
runtime_pair = RUNTIME_KEY , ":" , value ;
[]= BP3 engine, strictly reserved keys (ENGINE_KEY)()= runtime/dispatcher (downstream, outside repository), keys defined bycontrols.json[]like()are suffix on a RHS element — no prefix qualifier ([X]Ais not supported; to position a flag between elements, use the instantaneous form![X])
The RHS
rhs_element = symbol_call | rest | prolongation | undetermined_rest
| polymetric | backtick_inline | flag_mutation
| simultaneous | trigger_in | symbol_with_trigger_in
| capture | homomorphism_var
| context_positive | context_negative
| template_master | template_slave | nil_string ;
Simultaneity — Infix Syntax
simultaneous = symbol_call , { "!" , sim_target } ;
sim_target = symbol | symbol_call ;
Sa!dha!spotlight → SimultaneousGroup (primary + secondary). The ! is infix, not standalone,
and exclusively temporal: a secondary is always a symbol (never a flag mutation).
Flags go into the rule’s [] qualifiers.
Incoming Trigger — Standalone and Attached Form
trigger_in = "<!" , IDENT ;
symbol_with_trigger_in = symbol_call , "<!" , IDENT ;
Sa<!sync → SymbolWithTriggerIn: a symbol that waits for an incoming trigger before activating.
Lexemes — Pitfalls
- Hyphen
-in IDENT: allowed in non-terminals (LHS) only. Pre-scan. #in IDENT: alterations (C#4). Be careful with the flat alphabet._in IDENT: EBNF allows it but BP3 rejects_in the alphabet. Known blocking point.
Known Divergences
| Topic | Spec | Parser | Status |
|---|---|---|---|
@+ |
Absent | Accepted (legacy @controls) | Legacy |
| Negative INT | FLOAT alone has - |
Parser detects - before INT |
Spec to correct |
@mode → subgrammar |
No formal link | parseSubgrammars(initialMode) |
Spec to correct |
Sa!dha |
! standalone |
Infix SimultaneousGroup |
Spec to correct |
on_fail, hooks, timeout |
In the spec | Not implemented | Planned |
| Cross-rule braces | Absent | annotateUnbalancedBraces |
To document |
Two old divergences are now resolved in the spec: Sa<!sync (attached formSymbolWithTriggerIn) and cv_instance (env1(...) = lib.type(...)) are now described
by the EBNF.
Key Takeaways
@actoris the central directive — links alphabet, scale, sounds, transport[]= engine (strict keys),()= runtime (free keys via controls.json)!is infix (Sa!dha), not standalone, and exclusively temporal (no flag mutation)@mode:Xis a block directive, not an inline qualifier- Documented divergences between spec and parser — the language is actively evolving
Glossary
- EBNF: Extended Backus-Naur Form — standard notation for syntax (ISO 14977)
- SimultaneousGroup: AST node for
Sa!dha!spotlight(primary + secondary, temporal) - SymbolWithTriggerIn: AST node for
Sa<!sync(symbol + attached incoming trigger) - ENGINE_KEY: Reserved key for engine qualifiers
[] - raw_value: Raw value (any text up to
,or]) for engine keys
Links in the Series
- L3 — EBNF Notation
- B10 — BP3’s EBNF
- S2 — The Tokens
- S3 — The @actor Directive
- S10 — The Compiler that Implements this Grammar
- S13 (upcoming) — The AST produced by the parser
Prerequisites: S2, L3
Reading time: 10 min
Tags: #BPscript #EBNF #formal-grammar #specification
Next article: S13 (coming soon) — BPscript’s AST