S13) L’AST de BPscript

Anatomie du code intermédiaire

ActorDirective, Symbol.actor, CVInstance

Le parser produit un arbre. L’encoder le traduit en grammaire BP3. Entre les deux, l’AST capture tout : acteurs, symboles, qualificateurs, CV.

Où se situe cet article ?

Référence technique. Pour l’AST en général, voir L4. Pour la grammaire que le parser implémente, voir S12. Spécification complète : BPSCRIPT_AST.md (v0.7).


Le nœud racine : Scene

 

Scene {
  type: "Scene"
  directives: Directive[]
  actors: ActorDirective[]         // @actor directives
  scenes: SceneDirective[]         // @scene directives (scènes filles)
  exposes: ExposeDirective[]       // @expose (flags visibles au parent)
  maps: MapDirective[]             // @map (correspondances E/S CC/OSC ↔ flags/triggers)
  aliases: AliasDirective[]        // @alias (points d'E/S nommés)
  labels: LabelDirective[]         // @label (déclarations de label)
  declarations: Declaration[]
  macros: Macro[]
  cvInstances: CVInstance[]
  backticks: BacktickOrphan[]
  subgrammars: Subgrammar[]
  templates: TemplateEntry[] | null  // section @templates (optionnelle)
}

 

La racine collecte tout le niveau supérieur de la scène : les rattachements (actors), l’orchestration multi-scènes (scenes, exposes, maps, aliases, labels), les déclarations, les macros, les objets CV, et les sous-grammaires qui contiennent les règles.


ActorDirective — le nœud central

 

ActorDirective {
  type: "ActorDirective"
  name: string                     // "sitar", "tabla", "lights"
  properties: {
    alphabet: string               // référence vers alphabets.json ("sargam")
    scale: string | null           // gamme/degrés → hauteur via tempérament (null = pas de hauteur)
    sounds: string | null          // définitions par terminal: timbre, percussions, échantillons
    transport: TransportRef        // destination de rendu
    eval: string | null            // clé d'eval pour backticks (null = aucun REPL)
  }
  line: number
}

TransportRef {
  type: "TransportRef"
  key: string                      // "webaudio", "midi", "osc", "dmx"
  params: { [key: string]: any }   // { ch: 10 }, { port: 57110 }, {}
}

 

Exemple :

@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 } }

 

La propriété scale porte la gamme (degrés → hauteur via le tempérament) ; sounds porte les
définitions par terminal (timbre, percussions, échantillons) pour les acteurs sans hauteur ou à timbre
spécifique. Un acteur sans scale n’a pas de hauteur résolue (ex: un acteur percussif).


Symbol — avec acteur

 

Symbol {
  type: "Symbol"
  name: string                     // "Sa", "C4", "tin"
  actor: string | null             // "sitar" (explicite) ou null
  line: number
}

 

  • actor: null → résolution implicite (le compilateur cherche l’unique acteur qui contient ce symbole), ou non-terminal (qui n’a pas d’acteur)
  • actor: "sitar" → résolution explicite via notation pointée (sitar.Sa)

Le Symbol ne porte pas ses qualificateurs comme champs propres. Tout élément RHS (donc
le Symbol) peut porter des qualificateurs via deux champs communs (voir ci-dessous).


Qualificateurs portés par les éléments RHS

Les qualificateurs ne sont pas des champs spécifiques au Symbol : tout RhsElement peut porter
des qualificateurs moteur [] et/ou runtime (), toujours en suffixe (collés à droite de
l’élément, sans espace), via un seul champ commun :

 

RhsElement {
  ...                                          // propriétés spécifiques au type
  suffixQualifiers: (Qualifier | RuntimeQualifier)[] | null  // [] ou () collés à droite : A[weight:50], A(vel:80)
}

 

Le tokenizer marque chaque token avec spaceBefore : un [ ou ( sans espace avant
s’attache comme suffixe à l’élément précédent. Le parser ne produit pas de qualificateur
préfixe ; pour positionner un flag entre deux éléments, on utilise la forme instantanée ![X].

 

Qualifier {
  type: "Qualifier"
  pairs: QualPair[]
  tempoOp: TempoOp | null          // [/2], [*3]... — mutuellement exclusif avec pairs
}

QualPair {
  type: "QualPair"
  key: string                      // ENGINE_KEY : mode, scan, weight, tempo, scale...
  value: string | number | boolean // "random", 50, "1/2", "inf", true (clé nue)
  decrement: number | null         // pour weight:50-12
}

RuntimeQualifier {
  type: "RuntimeQualifier"
  pairs: { key: string, value: string | number | boolean }[]   // [{key:"vel", value:120}]
}

 

Sa(vel:120)[speed:2] produit, sur le nœud Symbol("Sa") :

  • `suffixQualifiers: [{ type: »RuntimeQualifier », pairs:[{key: »vel », value:120}] },

{ type: »Qualifier », pairs:[{key: »speed », value:2}] }]`

Les pairs runtime sont des objets nus { key, value } — pas de champ type (contrairement aux
QualPair du Qualifier moteur). La portée (symbole / règle / groupe / instantané) n’est pas
stockée sur le nœud : elle est déduite de la position dans l’AST par l’encodeur. Les RuntimeQualifiers
compilent en _script(CT n) dans la control table.


Control — contrôle BP3 dans le flux

 

Control {
  type: "Control"
  name: string        // vel, tempo, goto, striated, smooth, destru, stop...
  args: string[]      // ["120"], ["2","1"] ; [] pour un contrôle sans argument
}

 

Forme parsée d’un contrôle BP3 écrit directement dans le flux : vel(120){ name:"vel", args:["120"] },
goto(2,1){ name:"goto", args:["2","1"] }, striated{ name:"striated", args:[] }. Distinct
de l’InstantControl (!(...)) et du RuntimeQualifier ((...) suffixe d’un symbole).


SymbolWithTriggerIn — symbole en attente de trigger

 

SymbolWithTriggerIn {
  type: "SymbolWithTriggerIn"
  symbol: Symbol           // le symbole porteur
  triggers: TriggerIn[]    // un ou plusieurs trigger-in attachés
}

 

Émis pour Sa<!sync1 : un symbole qui attend un trigger entrant (<!) avant de se déclencher.


CVInstance

 

CVInstance {
  type: "CVInstance"
  name: string                      // "env1", "lfo1"
  target: string                    // paramètre ciblé ("filter", "pan", "gain")
  transport: string                 // runtime cible ("sc", "webaudio")
  lib: string | null                // lib source ("filter", null pour backtick)
  objectType: string                // type d'objet ("adsr", "lfo", "ramp", "backtick")
  args: (number | string)[]         // arguments positionnels
  namedArgs: { [key: string]: any } // arguments nommés (attack:10, rate:4)
  code: string | null               // code backtick (si objectType == "backtick")
  line: number
}

 

Exemples :

  • 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 et Rule

 

Subgrammar {
  type: "Subgrammar"
  rules: Rule[]
  index: number
  mode: string | null              // "random", "ord"... (de @mode:X)
  modifiers: ModeModifier[] | null // directives de sous-grammaire (@mode:X(destru, mm:60))
}

Rule {
  type: "Rule"
  guard: Guard | Guard[] | null    // un ou plusieurs guards (AND)
  contexts: Context[]
  lhs: LhsElement[]
  arrow: "->" | "<-" | "<>"
  rhs: RhsElement[]
  flags: FlagExpr[]                // mutations collectées, émises en fin de règle : /phase=2/ /Atrans/
  qualifiers: Qualifier[]          // [mode:random, scan:left] en fin de règle (moteur [])
  runtimeQualifier: RuntimeQualifier | null  // suffixe () sur la règle : S -> C4 D4 (vel:80)
  line: number
}

Guard {
  type: "Guard"
  flag: string
  operator: "==" | "!=" | ">" | "<" | ">=" | "<=" | "+" | "-" | null  // null = bare flag
  value: number | string | null    // null pour bare flag [Ideas]
  mutates: boolean
}

 


Traduction vers BP3

Nœud AST BP3 compilé
ActorDirective (pas de BP3 — produit le terminalActorMap)
Symbol("Sa", actor:"sitar") bolSa (nom opaque)
RuntimeQualifier({vel:120}) _script(CT 0) + controlTable[CT0]={vel:120}
Qualifier("speed", 2) {2, ...} ou /2
Control("vel", ["120"]) contrôle BP3 dans le flux
Guard("phase", "==", 1) /phase=1/
FlagExpr("phase", "=", 2) /phase=2/
Subgrammar(mode:"random") RND en mode_line
Polymetric([v1, v2]) {v1, v2}

Les terminaux sont des noms simples préfixés bol. Le resolver en aval (hors dépôt) fait
correspondre noms → fréquences via les acteurs.


Ce qu’il faut retenir

  1. ActorDirective capture le rattachement complet (alphabet, scale, sounds, transport, eval)
  2. Symbol.actor : null = implicite, "sitar" = explicite — les qualifiers ne sont pas des champs du Symbol, ils sont portés par suffixQualifiers ([] comme () sont suffixe)
  3. Qualificateurs scindés : Qualifier ([], moteur) et RuntimeQualifier ((), runtime — pairs nues, portée déduite de la position)
  4. Control : contrôle BP3 dans le flux ; SymbolWithTriggerIn : symbole en attente de <!
  5. CVInstance : objets CV déclarés en tête de scène (ADSR, LFO, ramp) — objectType, args positionnels, namedArgs

Glossaire

  • AST : Abstract Syntax Tree — représentation arborescente du code après parsing
  • ActorDirective : Nœud déclarant un acteur avec ses propriétés (alphabet, scale, sounds, transport…)
  • Qualifier : Qualificateur [] pour le moteur BP3 (speed, weight, tempo…)
  • RuntimeQualifier : Qualificateur () pour le runtime (vel, pan, wave…) — compilé en _script(CTn)
  • Control : Contrôle BP3 écrit directement dans le flux (vel(120), goto(2,1))
  • SymbolWithTriggerIn : Symbole attendant un trigger entrant <! avant de se déclencher
  • CVInstance : Déclaration d’un objet CV en tête de scène (enveloppe, LFO, ramp)
  • terminalActorMap : Dictionnaire {terminal BP3 → acteur} émis par l’encoder

Liens dans la série

  • L4 — Qu’est-ce qu’un AST
  • S12 — L’EBNF que le parser implémente
  • S10 — Le compilateur (tokenizer → parser → encoder → cet AST)
  • S3 — Les acteurs

Prérequis : S12, L4
Temps de lecture : 10 min
Tags : #BPscript #AST #compilateur #code-intermédiaire


Prochain article : S14 (à venir) — Sons, specs et effets


Retour à l’index