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

  1. ActorDirective captures the complete attachment (alphabet, scale, sounds, transport, eval)
  2. Symbol.actor: null = implicit, "sitar" = explicit — qualifiers are not fields of the Symbol; they are carried by suffixQualifiers (both [] and () are suffixes)
  3. Split qualifiers: Qualifier ([], engine) and RuntimeQualifier ((), runtime — bare pairs, scope inferred from position)
  4. Control: BP3 control in the stream; SymbolWithTriggerIn: symbol waiting for <!
  5. CVInstance: CV objects declared at the top of the scene (ADSR, LFO, ramp) — objectType, positional args, 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


Back to index