Skip to main content

concinnity_core/behavior/
compile.rs

1// Name resolution: the authored body walked once, every name replaced by the
2// slot that holds its value at tick time.
3//
4// Compilation is total. The world crate's checker has already rejected unknown
5// names and mistyped expressions, so anything unresolvable here becomes
6// `CExpr::Never` or `COp::Never`, which evaluates to nothing and skips its node
7// rather than panicking.
8
9use alloc::boxed::Box;
10use alloc::string::{String, ToString};
11use alloc::vec::Vec;
12
13use crate::behavior::program::{CExpr, CNode, COp, Program, VarTable};
14use crate::behavior::value::{Arith, Cmp, Val};
15use crate::components::{Behavior, BehaviorExpr, BehaviorNode, BehaviorSource};
16use crate::ecs::{ComponentTag, TracePath, TraceStep};
17
18// Compile-time name scope, mirroring the world crate's checker.
19struct Names<'a> {
20    locals: &'a [String],
21    queries: &'a [String],
22    bindings: Vec<String>,
23    // High-water mark of concurrently live bindings, which sizes the frame.
24    peak: usize,
25}
26
27impl Names<'_> {
28    fn bind(&mut self, name: &str) -> u16 {
29        self.bindings.push(name.to_string());
30        self.peak = self.peak.max(self.bindings.len());
31        (self.bindings.len() - 1) as u16
32    }
33
34    fn binding(&self, name: &str) -> Option<u16> {
35        self.bindings
36            .iter()
37            .rposition(|n| n == name)
38            .map(|i| i as u16)
39    }
40
41    fn local(&self, name: &str) -> Option<u16> {
42        self.locals.iter().position(|n| n == name).map(|i| i as u16)
43    }
44
45    fn query(&self, name: &str) -> Option<u16> {
46        self.queries
47            .iter()
48            .position(|n| n == name)
49            .map(|i| i as u16)
50    }
51}
52
53// Resolve an authored component name to the tag that still holds its entities
54// at tick time. A load-time pass drains some columns during `World::start`;
55// `surviving_tag` maps those to the runtime component that replaces them
56// (`Prop` -> `PropInstance`) and drops the ones nothing replaces, which the
57// world checker rejects before a build gets here.
58fn surviving_tag(name: &str) -> Option<u8> {
59    ComponentTag::parse(name)
60        .and_then(ComponentTag::surviving_tag)
61        .map(|t| t as u8)
62}
63
64/// Compile one authored behavior against the world's shared variable table,
65/// interning any variable name it mentions that no `Variables` asset declared.
66pub fn compile(def: Behavior, vars: &mut VarTable) -> Program {
67    let scope: Vec<u8> = def.scope.iter().filter_map(|c| surviving_tag(c)).collect();
68    let local_names: Vec<String> = def.locals.iter().map(|l| l.name.clone()).collect();
69    let local_inits: Vec<Val> = def
70        .locals
71        .iter()
72        .map(|l| Val::from_literal(&l.value))
73        .collect();
74    let query_names: Vec<String> = def.queries.iter().map(|q| q.name.clone()).collect();
75    let queries: Vec<Vec<u8>> = def
76        .queries
77        .iter()
78        .map(|q| q.has.iter().filter_map(|c| surviving_tag(c)).collect())
79        .collect();
80
81    // A variable source names a variable that must have a slot even if no node
82    // ever writes it.
83    if let BehaviorSource::Variable(name) = &def.on {
84        vars.intern(name);
85    }
86
87    let mut names = Names {
88        locals: &local_names,
89        queries: &query_names,
90        bindings: Vec::new(),
91        peak: 0,
92    };
93    let mut paths = Vec::new();
94    let body = compile_nodes(
95        &def.body,
96        &mut names,
97        vars,
98        &[TraceStep::Field("do")],
99        &mut paths,
100    );
101    let bindings = names.peak;
102
103    Program {
104        def,
105        scope,
106        local_inits,
107        queries,
108        body,
109        paths,
110        bindings,
111    }
112}
113
114// A branch list's path base: the parent node's path plus the verb and branch
115// keys the authored JSON nests it under (matching the world checker's fault
116// paths, so a traced node lands on the same row / card a fault would).
117fn branch(path: &[TraceStep], verb: &'static str, list: &'static str) -> Vec<TraceStep> {
118    let mut base = path.to_vec();
119    base.push(TraceStep::Field(verb));
120    base.push(TraceStep::Field(list));
121    base
122}
123
124fn compile_nodes(
125    nodes: &[BehaviorNode],
126    names: &mut Names<'_>,
127    vars: &mut VarTable,
128    base: &[TraceStep],
129    paths: &mut Vec<TracePath>,
130) -> Vec<CNode> {
131    let depth = names.bindings.len();
132    let out = nodes
133        .iter()
134        .enumerate()
135        .map(|(i, n)| {
136            let mut path = base.to_vec();
137            path.push(TraceStep::Index(i as u32));
138            // Pre-order: the node claims its id (and path slot) before its
139            // branches compile, so ids read top-down.
140            let id = paths.len() as u32;
141            paths.push(path.clone());
142            let op = compile_node(n, names, vars, &path, paths);
143            CNode { id, op }
144        })
145        .collect();
146    names.bindings.truncate(depth);
147    out
148}
149
150fn compile_node(
151    node: &BehaviorNode,
152    names: &mut Names<'_>,
153    vars: &mut VarTable,
154    path: &[TraceStep],
155    paths: &mut Vec<TracePath>,
156) -> COp {
157    match node {
158        BehaviorNode::If {
159            cond,
160            then,
161            otherwise,
162        } => COp::If {
163            cond: compile_expr(cond, names, vars),
164            then: compile_nodes(then, names, vars, &branch(path, "if", "then"), paths),
165            otherwise: compile_nodes(otherwise, names, vars, &branch(path, "if", "else"), paths),
166        },
167        BehaviorNode::ForEach { query, bind, body } => {
168            let Some(query) = names.query(query) else {
169                return COp::Never;
170            };
171            let depth = names.bindings.len();
172            let bind = names.bind(bind);
173            let body = compile_nodes(body, names, vars, &branch(path, "for_each", "do"), paths);
174            names.bindings.truncate(depth);
175            COp::ForEach { query, bind, body }
176        }
177        BehaviorNode::Let { name, value } => {
178            let value = compile_expr(value, names, vars);
179            COp::Let {
180                bind: names.bind(name),
181                value,
182            }
183        }
184        BehaviorNode::Set { var, value, add } => COp::SetVar {
185            slot: vars.intern(var),
186            value: compile_expr(value, names, vars),
187            add: *add,
188        },
189        BehaviorNode::SetLocal { local, value, add } => match names.local(local) {
190            Some(slot) => COp::SetLocal {
191                slot,
192                value: compile_expr(value, names, vars),
193                add: *add,
194            },
195            None => COp::Never,
196        },
197        BehaviorNode::SetTransform {
198            entity,
199            position,
200            rotation_deg,
201            scale,
202        } => COp::SetTransform {
203            entity: compile_expr(entity, names, vars),
204            position: position.as_ref().map(|e| compile_expr(e, names, vars)),
205            rotation_deg: rotation_deg.as_ref().map(|e| compile_expr(e, names, vars)),
206            scale: scale.as_ref().map(|e| compile_expr(e, names, vars)),
207        },
208        BehaviorNode::Spawn {
209            template,
210            position,
211            rotation_deg,
212            scale,
213            lifetime,
214            bind,
215        } => match template {
216            Some(template) => COp::Spawn {
217                template: *template,
218                position: *position,
219                rotation_deg: *rotation_deg,
220                // A zero scale would make the copy invisible; treat it as unit
221                // scale, like the spawn request path.
222                scale: if *scale == [0.0; 3] { [1.0; 3] } else { *scale },
223                lifetime: *lifetime,
224                bind: bind.as_ref().map(|b| names.bind(b)),
225            },
226            None => COp::Never,
227        },
228        BehaviorNode::Despawn { target } => COp::Despawn(compile_expr(target, names, vars)),
229        BehaviorNode::Reparent { child, parent } => COp::Reparent {
230            child: compile_expr(child, names, vars),
231            parent: parent.as_ref().map(|e| compile_expr(e, names, vars)),
232        },
233        BehaviorNode::Show { target } => COp::Visible(compile_expr(target, names, vars), true),
234        BehaviorNode::Hide { target } => COp::Visible(compile_expr(target, names, vars), false),
235        BehaviorNode::Sound { clip, kind, volume } => match clip {
236            Some(clip) => COp::Sound {
237                clip: *clip,
238                kind: *kind,
239                volume: *volume,
240            },
241            None => COp::Never,
242        },
243        BehaviorNode::Scene { scene, transition } => match scene {
244            Some(scene) => COp::Scene {
245                scene: *scene,
246                transition: transition.clone(),
247            },
248            None => COp::Never,
249        },
250        BehaviorNode::Screen { screen } => match screen {
251            Some(screen) => COp::Screen(*screen),
252            None => COp::Never,
253        },
254        BehaviorNode::Story(playback) => COp::Story(*playback),
255        BehaviorNode::Save => COp::Save,
256    }
257}
258
259fn compile_expr(expr: &BehaviorExpr, names: &mut Names<'_>, vars: &mut VarTable) -> CExpr {
260    let binary =
261        |a: &BehaviorExpr, b: &BehaviorExpr, names: &mut Names<'_>, vars: &mut VarTable| {
262            (
263                Box::new(compile_expr(a, names, vars)),
264                Box::new(compile_expr(b, names, vars)),
265            )
266        };
267    match expr {
268        BehaviorExpr::Bool(b) => CExpr::Lit(Val::Bool(*b)),
269        BehaviorExpr::Int(i) => CExpr::Lit(Val::Int(*i)),
270        BehaviorExpr::Float(f) => CExpr::Lit(Val::Float(*f)),
271        BehaviorExpr::Vec3(v) => CExpr::Lit(Val::Vec3(*v)),
272        BehaviorExpr::Var(name) => CExpr::Var(vars.intern(name)),
273        BehaviorExpr::Local(name) => names.local(name).map_or(CExpr::Never, CExpr::Local),
274        BehaviorExpr::Bind(name) => names.binding(name).map_or(CExpr::Never, CExpr::Bind),
275        BehaviorExpr::Named(id) => id.map_or(CExpr::Never, CExpr::Named),
276        BehaviorExpr::SelfEntity => CExpr::SelfEntity,
277        BehaviorExpr::Dt => CExpr::Dt,
278        BehaviorExpr::Elapsed => CExpr::Elapsed,
279        BehaviorExpr::Position(e) => CExpr::Position(Box::new(compile_expr(e, names, vars))),
280        BehaviorExpr::Alive(e) => CExpr::Alive(Box::new(compile_expr(e, names, vars))),
281        BehaviorExpr::Normalize(e) => CExpr::Normalize(Box::new(compile_expr(e, names, vars))),
282        BehaviorExpr::Not(e) => CExpr::Not(Box::new(compile_expr(e, names, vars))),
283        BehaviorExpr::Distance(a, b) => {
284            let (a, b) = binary(a, b, names, vars);
285            CExpr::Distance(a, b)
286        }
287        BehaviorExpr::First(q) => names.query(q).map_or(CExpr::Never, CExpr::First),
288        BehaviorExpr::Count(q) => names.query(q).map_or(CExpr::Never, CExpr::Count),
289        BehaviorExpr::Add(a, b) => {
290            let (a, b) = binary(a, b, names, vars);
291            CExpr::Arith(Arith::Add, a, b)
292        }
293        BehaviorExpr::Sub(a, b) => {
294            let (a, b) = binary(a, b, names, vars);
295            CExpr::Arith(Arith::Sub, a, b)
296        }
297        BehaviorExpr::Mul(a, b) => {
298            let (a, b) = binary(a, b, names, vars);
299            CExpr::Arith(Arith::Mul, a, b)
300        }
301        BehaviorExpr::Div(a, b) => {
302            let (a, b) = binary(a, b, names, vars);
303            CExpr::Arith(Arith::Div, a, b)
304        }
305        BehaviorExpr::Eq(a, b) => {
306            let (a, b) = binary(a, b, names, vars);
307            CExpr::Compare(Cmp::Eq, a, b)
308        }
309        BehaviorExpr::Ne(a, b) => {
310            let (a, b) = binary(a, b, names, vars);
311            CExpr::Compare(Cmp::Ne, a, b)
312        }
313        BehaviorExpr::Lt(a, b) => {
314            let (a, b) = binary(a, b, names, vars);
315            CExpr::Compare(Cmp::Lt, a, b)
316        }
317        BehaviorExpr::Le(a, b) => {
318            let (a, b) = binary(a, b, names, vars);
319            CExpr::Compare(Cmp::Le, a, b)
320        }
321        BehaviorExpr::Gt(a, b) => {
322            let (a, b) = binary(a, b, names, vars);
323            CExpr::Compare(Cmp::Gt, a, b)
324        }
325        BehaviorExpr::Ge(a, b) => {
326            let (a, b) = binary(a, b, names, vars);
327            CExpr::Compare(Cmp::Ge, a, b)
328        }
329        BehaviorExpr::All(items) => {
330            CExpr::All(items.iter().map(|e| compile_expr(e, names, vars)).collect())
331        }
332        BehaviorExpr::Any(items) => {
333            CExpr::Any(items.iter().map(|e| compile_expr(e, names, vars)).collect())
334        }
335    }
336}
337
338#[cfg(test)]
339mod tests {
340    use super::*;
341    use crate::components::{BehaviorLiteral, BehaviorLocal, BehaviorQuery, CueKind};
342    use crate::ecs::AudioClipHandle;
343    use crate::ecs::asset_id::AssetId;
344    use alloc::vec;
345
346    // A behavior with one local, one query over props, and the given body, so
347    // a test states only the nodes it is about.
348    fn behavior(body: Vec<BehaviorNode>) -> Behavior {
349        Behavior {
350            locals: vec![BehaviorLocal {
351                name: String::from("hp"),
352                value: BehaviorLiteral::Int(3),
353            }],
354            queries: vec![BehaviorQuery {
355                name: String::from("props"),
356                has: vec![String::from("Prop")],
357            }],
358            body,
359            ..Behavior::default()
360        }
361    }
362
363    fn compiled(body: Vec<BehaviorNode>) -> Program {
364        compile(behavior(body), &mut VarTable::default())
365    }
366
367    // The single compiled op a one-node body produces.
368    fn op(node: BehaviorNode) -> COp {
369        let mut body = compiled(vec![node]).body;
370        assert_eq!(body.len(), 1, "expected exactly one compiled node");
371        body.remove(0).op
372    }
373
374    fn expr(e: BehaviorExpr) -> CExpr {
375        match op(BehaviorNode::Despawn { target: e }) {
376            COp::Despawn(e) => e,
377            other => panic!("expected a despawn, got {other:?}"),
378        }
379    }
380
381    fn int(i: i32) -> Box<BehaviorExpr> {
382        Box::new(BehaviorExpr::Int(i))
383    }
384
385    // Compilation is total: the world checker has already rejected these, so a
386    // name that does not resolve becomes a node that does nothing rather than
387    // a panic.
388    #[test]
389    fn an_unresolvable_name_compiles_to_a_node_that_does_nothing() {
390        assert!(matches!(
391            op(BehaviorNode::ForEach {
392                query: String::from("undeclared"),
393                bind: String::from("it"),
394                body: Vec::new(),
395            }),
396            COp::Never
397        ));
398        assert!(matches!(
399            op(BehaviorNode::SetLocal {
400                local: String::from("undeclared"),
401                value: BehaviorExpr::Int(1),
402                add: false,
403            }),
404            COp::Never
405        ));
406        assert!(matches!(
407            op(BehaviorNode::Spawn {
408                template: None,
409                position: [0.0; 3],
410                rotation_deg: [0.0; 3],
411                scale: [1.0; 3],
412                lifetime: 0.0,
413                bind: None,
414            }),
415            COp::Never
416        ));
417        assert!(matches!(
418            op(BehaviorNode::Sound {
419                clip: None,
420                kind: CueKind::Sound,
421                volume: 1.0,
422            }),
423            COp::Never
424        ));
425        assert!(matches!(
426            op(BehaviorNode::Scene {
427                scene: None,
428                transition: String::from("Cut"),
429            }),
430            COp::Never
431        ));
432        assert!(matches!(
433            op(BehaviorNode::Screen { screen: None }),
434            COp::Never
435        ));
436    }
437
438    #[test]
439    fn a_resolvable_name_compiles_to_the_node_it_denotes() {
440        assert!(matches!(
441            op(BehaviorNode::Sound {
442                clip: Some(AudioClipHandle(2)),
443                kind: CueKind::Music,
444                volume: 0.5,
445            }),
446            COp::Sound {
447                clip: AudioClipHandle(2),
448                kind: CueKind::Music,
449                volume: 0.5,
450            }
451        ));
452        assert!(matches!(
453            op(BehaviorNode::Scene {
454                scene: Some(AssetId(4)),
455                transition: String::from("FadeBlack"),
456            }),
457            COp::Scene {
458                scene: AssetId(4),
459                ..
460            }
461        ));
462        assert!(matches!(
463            op(BehaviorNode::Screen {
464                screen: Some(AssetId(5)),
465            }),
466            COp::Screen(AssetId(5))
467        ));
468    }
469
470    // A zero scale would make the copy invisible, so it reads as the
471    // template's own size instead.
472    #[test]
473    fn a_zero_scaled_spawn_compiles_to_unit_scale() {
474        let spawn = |scale| {
475            op(BehaviorNode::Spawn {
476                template: Some(AssetId(1)),
477                position: [1.0, 2.0, 3.0],
478                rotation_deg: [0.0; 3],
479                scale,
480                lifetime: 2.0,
481                bind: Some(String::from("copy")),
482            })
483        };
484        let COp::Spawn { scale, bind, .. } = spawn([0.0; 3]) else {
485            panic!("expected a spawn");
486        };
487        assert_eq!(scale, [1.0; 3]);
488        assert_eq!(bind, Some(0));
489
490        let COp::Spawn { scale, .. } = spawn([2.0; 3]) else {
491            panic!("expected a spawn");
492        };
493        assert_eq!(scale, [2.0; 3]);
494    }
495
496    #[test]
497    fn show_and_hide_compile_to_the_same_node_with_opposite_visibility() {
498        let target = || BehaviorExpr::Named(Some(AssetId(1)));
499        assert!(matches!(
500            op(BehaviorNode::Show { target: target() }),
501            COp::Visible(_, true)
502        ));
503        assert!(matches!(
504            op(BehaviorNode::Hide { target: target() }),
505            COp::Visible(_, false)
506        ));
507    }
508
509    #[test]
510    fn a_reparent_compiles_both_its_parent_forms() {
511        assert!(matches!(
512            op(BehaviorNode::Reparent {
513                child: BehaviorExpr::SelfEntity,
514                parent: Some(BehaviorExpr::Named(Some(AssetId(1)))),
515            }),
516            COp::Reparent {
517                parent: Some(_),
518                ..
519            }
520        ));
521        assert!(matches!(
522            op(BehaviorNode::Reparent {
523                child: BehaviorExpr::SelfEntity,
524                parent: None,
525            }),
526            COp::Reparent { parent: None, .. }
527        ));
528    }
529
530    #[test]
531    fn a_local_and_the_tick_readings_compile_to_their_slots() {
532        assert!(matches!(
533            expr(BehaviorExpr::Local(String::from("hp"))),
534            CExpr::Local(0)
535        ));
536        assert!(matches!(
537            expr(BehaviorExpr::Local(String::from("undeclared"))),
538            CExpr::Never
539        ));
540        assert!(matches!(expr(BehaviorExpr::Dt), CExpr::Dt));
541        assert!(matches!(expr(BehaviorExpr::Elapsed), CExpr::Elapsed));
542    }
543
544    #[test]
545    fn the_unary_expressions_compile_to_their_operators() {
546        assert!(matches!(
547            expr(BehaviorExpr::Normalize(int(1))),
548            CExpr::Normalize(_)
549        ));
550        assert!(matches!(expr(BehaviorExpr::Not(int(1))), CExpr::Not(_)));
551    }
552
553    #[test]
554    fn every_arithmetic_operator_compiles_to_its_own_kind() {
555        let arith = |e| match expr(e) {
556            CExpr::Arith(op, _, _) => op,
557            other => panic!("expected arithmetic, got {other:?}"),
558        };
559        assert!(matches!(
560            arith(BehaviorExpr::Add(int(1), int(2))),
561            Arith::Add
562        ));
563        assert!(matches!(
564            arith(BehaviorExpr::Sub(int(1), int(2))),
565            Arith::Sub
566        ));
567        assert!(matches!(
568            arith(BehaviorExpr::Mul(int(1), int(2))),
569            Arith::Mul
570        ));
571        assert!(matches!(
572            arith(BehaviorExpr::Div(int(1), int(2))),
573            Arith::Div
574        ));
575    }
576
577    #[test]
578    fn every_comparison_operator_compiles_to_its_own_kind() {
579        let cmp = |e| match expr(e) {
580            CExpr::Compare(op, _, _) => op,
581            other => panic!("expected a comparison, got {other:?}"),
582        };
583        assert!(matches!(cmp(BehaviorExpr::Eq(int(1), int(2))), Cmp::Eq));
584        assert!(matches!(cmp(BehaviorExpr::Ne(int(1), int(2))), Cmp::Ne));
585        assert!(matches!(cmp(BehaviorExpr::Lt(int(1), int(2))), Cmp::Lt));
586        assert!(matches!(cmp(BehaviorExpr::Le(int(1), int(2))), Cmp::Le));
587        assert!(matches!(cmp(BehaviorExpr::Gt(int(1), int(2))), Cmp::Gt));
588        assert!(matches!(cmp(BehaviorExpr::Ge(int(1), int(2))), Cmp::Ge));
589    }
590
591    #[test]
592    fn the_variadic_expressions_compile_each_operand() {
593        let items = || vec![BehaviorExpr::Bool(true), BehaviorExpr::Dt];
594        let CExpr::All(all) = expr(BehaviorExpr::All(items())) else {
595            panic!("expected an all");
596        };
597        assert_eq!(all.len(), 2);
598        let CExpr::Any(any) = expr(BehaviorExpr::Any(items())) else {
599            panic!("expected an any");
600        };
601        assert_eq!(any.len(), 2);
602    }
603}