S13) BPscript’s AST
Anatomy of the intermediate code
ActorDirective, Symbol.actor, CVInstance
The parser produces a tree. The encoder translates it into BP3 grammar. Between the two, the AST captures everything: actors, symbols, qualifiers, CVs.
Where does this article fit in?
Technical reference. For the AST in general, see L4. For the grammar implemented by the parser, see S12. Full specification: BPSCRIPT_AST.md (v0.7).
The root node: Scene
Scene {
type: "Scene"
directives: Directive[]
actors: ActorDirective[] // @actor directives
scenes: SceneDirective[] // @scene directives (child scenes)
exposes: ExposeDirective[] // @expose (flags visible to the parent)
maps: MapDirective[] // @map (I/O mappings CC/OSC ↔ flags/triggers)
aliases: AliasDirective[] // @alias (named I/O points)
labels: LabelDirective[] // @label (label declarations)
declarations: Declaration[]
macros: Macro[]
cvInstances: CVInstance[]
backticks: BacktickOrphan[]
subgrammars: Subgrammar[]
templates: TemplateEntry[] | null // @templates section (optional)
}
The root collects everything at the scene’s top level: attachments (actors), multi-scene orchestration (scenes, exposes, maps, aliases, labels), declarations, macros, CV objects, and subgrammars containing the rules.
ActorDirective — the central node
ActorDirective {
type: "ActorDirective"
name: string // "sitar", "tabla", "lights"
properties: {
alphabet: string // reference to alphabets.json ("sargam")
scale: string | null // scale/degrees → pitch via temperament (null = no pitch)
sounds: string | null // definitions per terminal: timbre, percussion, samples
transport: TransportRef // rendering destination
eval: string | null // eval key for backticks (null = no REPL)
}
line: number
}
TransportRef {
type: "TransportRef"
key: string // "webaudio", "midi", "osc", "dmx"
params: { [key: string]: any } // { ch: 10 }, { port: 57110 }, {}
}
Example:
@actor sitar alphabet:sargam scale:sargam_22shruti transport:webaudio
→ { name:"sitar", properties: { alphabet:"sargam", scale:"sargam_22shruti", sounds:null,
transport:{key:"webaudio", params:{}}, eval:null } }
The scale property carries the scale (degrees → pitch via temperament); sounds carries definitions per terminal (timbre, percussion, samples) for actors without pitch or with a specific timbre. An actor without a scale has no resolved pitch (e.g., a percussive actor).
Symbol — with actor
Symbol {
type: "Symbol"
name: string // "Sa", "C4", "tin"
actor: string | null // "sitar" (explicit) or null
line: number
}
actor: null→ implicit resolution (the compiler looks for the unique actor containing this symbol), or non-terminal (which has no actor)actor: "sitar"→ explicit resolution via dot notation (sitar.Sa)
The Symbol does not carry its qualifiers as its own fields. Any RHS element (and thus the Symbol) can carry qualifiers via two common fields (see below).
Qualifiers carried by RHS elements
Qualifiers are not fields specific to the Symbol: any RhsElement can carry engine qualifiers [] and/or runtime qualifiers (), always as a suffix (attached to the right of the element, without a space), via a single common field:
RhsElement {
... // type-specific properties
suffixQualifiers: (Qualifier | RuntimeQualifier)[] | null // [] or () attached to the right: A[weight:50], A(vel:80)
}
The tokenizer marks each token with spaceBefore: a [ or ( without a preceding space attaches as a suffix to the previous element. The parser does not produce a prefix qualifier; to position a flag between two elements, the instantaneous form ![X] is used.
Qualifier {
type: "Qualifier"
pairs: QualPair[]
tempoOp: TempoOp | null // [/2], [*3]... — mutually exclusive with pairs
}
QualPair {
type: "QualPair"
key: string // ENGINE_KEY : mode, scan, weight, tempo, scale...
value: string | number | boolean // "random", 50, "1/2", "inf", true (bare key)
decrement: number | null // for weight:50-12
}
RuntimeQualifier {
type: "RuntimeQualifier"
pairs: { key: string, value: string | number | boolean }[] // [{key:"vel", value:120}]
}
Sa(vel:120)[speed:2] produces, on the Symbol("Sa") node:
- `suffixQualifiers: [{ type:”RuntimeQualifier”, pairs:[{key:”vel”, value:120}] },
{ type:”Qualifier”, pairs:[{key:”speed”, value:2}] }]`
Runtime pairs are bare objects { key, value } — no type field (unlike the engine Qualifier‘s QualPair). The scope (symbol / rule / group / instantaneous) is not stored on the node: it is inferred from the position in the AST by the encoder. RuntimeQualifiers compile into _script(CT n) in the control table.
Control — BP3 control in the stream
Control {
type: "Control"
name: string // vel, tempo, goto, striated, smooth, destru, stop...
args: string[] // ["120"], ["2","1"] ; [] for a control without arguments
}
Parsed form of a BP3 control written directly in the stream: vel(120) → { name:"vel", args:["120"] }, goto(2,1) → { name:"goto", args:["2","1"] }, striated → { name:"striated", args:[] }. Distinct from InstantControl (!(...)) and RuntimeQualifier ((...) suffix of a symbol).
SymbolWithTriggerIn — symbol waiting for a trigger
SymbolWithTriggerIn {
type: "SymbolWithTriggerIn"
symbol: Symbol // the carrying symbol
triggers: TriggerIn[] // one or more attached trigger-ins
}
Emitted for Sa<!sync1: a symbol waiting for an incoming trigger (<!) before triggering.
CVInstance
CVInstance {
type: "CVInstance"
name: string // "env1", "lfo1"
target: string // targeted parameter ("filter", "pan", "gain")
transport: string // target runtime ("sc", "webaudio")
lib: string | null // source lib ("filter", null for backtick)
objectType: string // object type ("adsr", "lfo", "ramp", "backtick")
args: (number | string)[] // positional arguments
namedArgs: { [key: string]: any } // named arguments (attack:10, rate:4)
code: string | null // backtick code (if objectType == "backtick")
line: number
}
Examples:
env1(filter, sc) = filter.adsr(10, 100, 0.7, 200)
→ { name:"env1", target:"filter", transport:"sc", lib:"filter", objectType:"adsr", args:[10,100,0.7,200], namedArgs:{} }
lfo1(pan, webaudio) = filter.lfo(rate:4, depth:50)
→ { name:"lfo1", target:"pan", transport:"webaudio", lib:"filter", objectType:"lfo", args:[], namedArgs:{rate:4, depth:50} }
Subgrammar and Rule
Subgrammar {
type: "Subgrammar"
rules: Rule[]
index: number
mode: string | null // "random", "ord"... (from @mode:X)
modifiers: ModeModifier[] | null // subgrammar directives (@mode:X(destru, mm:60))
}
Rule {
type: "Rule"
guard: Guard | Guard[] | null // one or more guards (AND)
contexts: Context[]
lhs: LhsElement[]
arrow: "->" | "<-" | "<>"
rhs: RhsElement[]
flags: FlagExpr[] // collected mutations, emitted at the end of the rule: /phase=2/ /Atrans/
qualifiers: Qualifier[] // [mode:random, scan:left] at the end of the rule (engine [])
runtimeQualifier: RuntimeQualifier | null // () suffix on the rule: S -> C4 D4 (vel:80)
line: number
}
Guard {
type: "Guard"
flag: string
operator: "==" | "!=" | ">" | "<" | ">=" | "<=" | "+" | "-" | null // null = bare flag
value: number | string | null // null for bare flag [Ideas]
mutates: boolean
}
Translation to BP3
| AST Node | Compiled BP3 |
|---|---|
ActorDirective |
(no BP3 — produces the terminalActorMap) |
Symbol("Sa", actor:"sitar") |
bolSa (opaque name) |
RuntimeQualifier({vel:120}) |
_script(CT 0) + controlTable[CT0]={vel:120} |
Qualifier("speed", 2) |
{2, ...} or /2 |
Control("vel", ["120"]) |
BP3 control in the stream |
Guard("phase", "==", 1) |
/phase=1/ |
FlagExpr("phase", "=", 2) |
/phase=2/ |
Subgrammar(mode:"random") |
RND in mode_line |
Polymetric([v1, v2]) |
{v1, v2} |
Terminals are simple names prefixed with bol. The downstream resolver (outside the repository) maps names → frequencies via the actors.
Key takeaways
ActorDirectivecaptures the complete attachment (alphabet,scale,sounds,transport,eval)Symbol.actor:null= implicit,"sitar"= explicit — qualifiers are not fields of the Symbol; they are carried bysuffixQualifiers(both[]and()are suffixes)- Split qualifiers:
Qualifier([], engine) andRuntimeQualifier((), runtime — bare pairs, scope inferred from position) Control: BP3 control in the stream;SymbolWithTriggerIn: symbol waiting for<!CVInstance: CV objects declared at the top of the scene (ADSR, LFO, ramp) —objectType, positionalargs,namedArgs
Glossary
- AST: Abstract Syntax Tree — tree representation of the code after parsing
- ActorDirective: Node declaring an actor with its properties (alphabet, scale, sounds, transport…)
- Qualifier: Qualifier
[]for the BP3 engine (speed, weight, tempo…) - RuntimeQualifier: Qualifier
()for the runtime (vel, pan, wave…) — compiled into_script(CTn) - Control: BP3 control written directly in the stream (
vel(120),goto(2,1)) - SymbolWithTriggerIn: Symbol waiting for an incoming trigger
<!before triggering - CVInstance: Declaration of a CV object at the top of the scene (envelope, LFO, ramp)
- terminalActorMap: Dictionary
{BP3 terminal → actor}emitted by the encoder
Links in the series
- L4 — What is an AST
- S12 — The EBNF implemented by the parser
- S10 — The compiler (tokenizer → parser → encoder → this AST)
- S3 — Actors
Prerequisites: S12, L4
Reading time: 10 min
Tags: #BPscript #AST #compiler #intermediate-code
Next article: S14 (coming soon) — Sounds, specs, and effects