Skip to main content

concinnity_core/behavior/
run.rs

1// Evaluating a compiled body: expressions read the world through a per-tick
2// view, nodes append to an effect buffer the caller drains onto the runtime's
3// request queues.
4//
5// Nothing here mutates the world. An expression that cannot produce a value
6// (an empty query's `first`, an unresolved name, a despawned entity) yields
7// None, and the node holding it is skipped rather than guessing.
8
9use alloc::string::String;
10use alloc::vec::Vec;
11
12use crate::behavior::program::{CExpr, CNode, COp};
13use crate::behavior::value::{Arith, Cmp, Val};
14use crate::components::{PlayCue, StoryPlayback, Transform};
15use crate::ecs::{Entity, asset_id::AssetId};
16use crate::math::sqrt;
17
18/// What a behavior may read this tick.
19pub struct View<'a> {
20    /// Seconds this tick advances.
21    pub dt: f32,
22    /// Seconds of simulated time so far.
23    pub elapsed: f32,
24    /// The world's variables, in slot order.
25    pub vars: &'a [Val],
26    /// This instance's locals, in slot order.
27    pub locals: &'a [Val],
28    /// Exactly as wide as the body's binding high-water mark, so a slot is
29    /// always in range and the frame never grows mid-run.
30    pub bindings: &'a mut [Option<Val>],
31    /// The entities each declared query selected this tick, in slot order.
32    pub queries: &'a [Vec<Entity>],
33    /// Resolves a name to the entity carrying it.
34    pub by_name: &'a dyn Fn(AssetId) -> Option<Entity>,
35    /// Reads an entity's transform.
36    pub transforms: &'a dyn Fn(Entity) -> Option<Transform>,
37    /// Whether an entity still exists.
38    pub alive: &'a dyn Fn(Entity) -> bool,
39    /// The entity this run is scoped to, if any.
40    pub self_entity: Option<Entity>,
41    /// Node ids executed this run, recorded only while tracing is requested
42    /// (`None` costs one branch per node).
43    pub trace: &'a mut Option<Vec<u32>>,
44}
45
46/// One world change a behavior asked for, in body order.
47#[derive(Debug, Clone)]
48pub enum Effect {
49    /// Write a world variable.
50    SetVar {
51        /// The variable's slot.
52        slot: u16,
53        /// The value produced.
54        value: Val,
55        /// Add to the current value rather than replacing it.
56        add: bool,
57    },
58    /// Write one of the instance's locals.
59    SetLocal {
60        /// The local's slot.
61        slot: u16,
62        /// The value produced.
63        value: Val,
64        /// Add to the current value rather than replacing it.
65        add: bool,
66    },
67    /// Replace an entity's transform.
68    SetTransform {
69        /// The entity to move.
70        entity: Entity,
71        /// Its new transform.
72        transform: Transform,
73    },
74    /// Copy a template into the world.
75    Spawn(SpawnEffect),
76    /// Remove an entity.
77    Despawn(Entity),
78    /// Move an entity under another parent, or to the root.
79    Reparent {
80        /// The entity to move.
81        child: Entity,
82        /// Its new parent, or `None` for the root.
83        parent: Option<Entity>,
84    },
85    /// Show or hide an entity.
86    Visible(Entity, bool),
87    /// Play an audio cue.
88    Sound(PlayCue),
89    /// Load a scene.
90    Scene {
91        /// The scene to load.
92        scene: AssetId,
93        /// The transition to play.
94        transition: String,
95    },
96    /// Show a screen.
97    Screen(AssetId),
98    /// Drive story playback.
99    Story(StoryPlayback),
100    /// Persist the world's behavior state.
101    Save,
102}
103
104/// A requested copy of a template.
105#[derive(Debug, Clone)]
106pub struct SpawnEffect {
107    /// The template to copy.
108    pub template: AssetId,
109    /// Where the copy starts.
110    pub transform: Transform,
111    /// Seconds the copy lives, or `None` for no limit.
112    pub lifetime: Option<f32>,
113}
114
115fn eval(expr: &CExpr, view: &View<'_>) -> Option<Val> {
116    match expr {
117        CExpr::Lit(v) => Some(*v),
118        CExpr::Var(slot) => view.vars.get(*slot as usize).copied(),
119        CExpr::Local(slot) => view.locals.get(*slot as usize).copied(),
120        CExpr::Bind(slot) => view.bindings.get(*slot as usize).copied().flatten(),
121        CExpr::Named(id) => (view.by_name)(*id).map(Val::Entity),
122        CExpr::SelfEntity => view.self_entity.map(Val::Entity),
123        CExpr::Dt => Some(Val::Float(view.dt)),
124        CExpr::Elapsed => Some(Val::Float(view.elapsed)),
125        CExpr::Position(e) => {
126            let entity = eval(e, view)?.as_entity()?;
127            Some(Val::Vec3((view.transforms)(entity)?.position))
128        }
129        CExpr::Alive(e) => {
130            // An expression that yields no entity at all is not alive.
131            let alive = eval(e, view)
132                .and_then(Val::as_entity)
133                .is_some_and(view.alive);
134            Some(Val::Bool(alive))
135        }
136        CExpr::Distance(a, b) => {
137            let a = (view.transforms)(eval(a, view)?.as_entity()?)?.position;
138            let b = (view.transforms)(eval(b, view)?.as_entity()?)?.position;
139            let d = [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
140            Some(Val::Float(sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2])))
141        }
142        CExpr::First(slot) => view
143            .queries
144            .get(*slot as usize)?
145            .first()
146            .copied()
147            .map(Val::Entity),
148        CExpr::Count(slot) => Some(Val::Int(view.queries.get(*slot as usize)?.len() as i32)),
149        CExpr::Normalize(e) => {
150            let v = eval(e, view)?.as_vec3()?;
151            let len = sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
152            Some(Val::Vec3(if len > f32::EPSILON {
153                [v[0] / len, v[1] / len, v[2] / len]
154            } else {
155                [0.0; 3]
156            }))
157        }
158        CExpr::Arith(op, a, b) => arith(*op, eval(a, view)?, eval(b, view)?),
159        CExpr::Compare(op, a, b) => compare(*op, eval(a, view)?, eval(b, view)?),
160        CExpr::Not(e) => Some(Val::Bool(!eval(e, view)?.as_bool()?)),
161        CExpr::All(items) => {
162            for item in items {
163                if !eval(item, view)?.as_bool()? {
164                    return Some(Val::Bool(false));
165                }
166            }
167            Some(Val::Bool(true))
168        }
169        CExpr::Any(items) => {
170            for item in items {
171                if eval(item, view)?.as_bool()? {
172                    return Some(Val::Bool(true));
173                }
174            }
175            Some(Val::Bool(false))
176        }
177        CExpr::Never => None,
178    }
179}
180
181fn arith(op: Arith, a: Val, b: Val) -> Option<Val> {
182    let scalar = |x: f32, y: f32| match op {
183        Arith::Add => x + y,
184        Arith::Sub => x - y,
185        Arith::Mul => x * y,
186        // Division by zero yields zero rather than an infinity that would
187        // silently poison every downstream transform.
188        Arith::Div => {
189            if y.abs() > f32::EPSILON {
190                x / y
191            } else {
192                0.0
193            }
194        }
195    };
196    match (a, b) {
197        (Val::Int(x), Val::Int(y)) => Some(Val::Int(scalar(x as f32, y as f32) as i32)),
198        (Val::Vec3(x), Val::Vec3(y)) => Some(Val::Vec3([
199            scalar(x[0], y[0]),
200            scalar(x[1], y[1]),
201            scalar(x[2], y[2]),
202        ])),
203        (Val::Vec3(v), other) => {
204            let s = other.as_f32()?;
205            Some(Val::Vec3([
206                scalar(v[0], s),
207                scalar(v[1], s),
208                scalar(v[2], s),
209            ]))
210        }
211        (other, Val::Vec3(v)) => {
212            let s = other.as_f32()?;
213            Some(Val::Vec3([
214                scalar(s, v[0]),
215                scalar(s, v[1]),
216                scalar(s, v[2]),
217            ]))
218        }
219        (x, y) => Some(Val::Float(scalar(x.as_f32()?, y.as_f32()?))),
220    }
221}
222
223fn compare(op: Cmp, a: Val, b: Val) -> Option<Val> {
224    let equal = match (a, b) {
225        (Val::Entity(x), Val::Entity(y)) => x == y,
226        (Val::Bool(x), Val::Bool(y)) => x == y,
227        (x, y) => x.as_f32()? == y.as_f32()?,
228    };
229    Some(Val::Bool(match op {
230        Cmp::Eq => equal,
231        Cmp::Ne => !equal,
232        Cmp::Lt => a.as_f32()? < b.as_f32()?,
233        Cmp::Le => a.as_f32()? <= b.as_f32()?,
234        Cmp::Gt => a.as_f32()? > b.as_f32()?,
235        Cmp::Ge => a.as_f32()? >= b.as_f32()?,
236    }))
237}
238
239/// Run a compiled body against `view`, appending what it asked for to `out`.
240pub fn exec(nodes: &[CNode], view: &mut View<'_>, out: &mut Vec<Effect>) {
241    for node in nodes {
242        exec_node(node, view, out);
243    }
244}
245
246fn exec_node(node: &CNode, view: &mut View<'_>, out: &mut Vec<Effect>) {
247    if let Some(t) = view.trace.as_mut() {
248        t.push(node.id);
249    }
250    match &node.op {
251        COp::If {
252            cond,
253            then,
254            otherwise,
255        } => {
256            let Some(Val::Bool(pass)) = eval(cond, view) else {
257                return;
258            };
259            exec(if pass { then } else { otherwise }, view, out);
260        }
261        COp::ForEach { query, bind, body } => {
262            let Some(entities) = view.queries.get(*query as usize) else {
263                return;
264            };
265            // Cloned so the body may read other queries (and mutate bindings)
266            // while this one is iterated.
267            for entity in entities.clone() {
268                set_binding(view, *bind, Some(Val::Entity(entity)));
269                exec(body, view, out);
270            }
271        }
272        COp::Let { bind, value } => {
273            let value = eval(value, view);
274            set_binding(view, *bind, value);
275        }
276        COp::SetVar { slot, value, add } => {
277            let Some(value) = eval(value, view) else {
278                return;
279            };
280            out.push(Effect::SetVar {
281                slot: *slot,
282                value,
283                add: *add,
284            });
285        }
286        COp::SetLocal { slot, value, add } => {
287            let Some(value) = eval(value, view) else {
288                return;
289            };
290            out.push(Effect::SetLocal {
291                slot: *slot,
292                value,
293                add: *add,
294            });
295        }
296        COp::SetTransform {
297            entity,
298            position,
299            rotation_deg,
300            scale,
301        } => {
302            let Some(entity) = eval(entity, view).and_then(Val::as_entity) else {
303                return;
304            };
305            let Some(mut transform) = (view.transforms)(entity) else {
306                return;
307            };
308            let field = |expr: &Option<CExpr>, into: &mut [f32; 3]| {
309                if let Some(expr) = expr
310                    && let Some(v) = eval(expr, view).and_then(Val::as_vec3)
311                {
312                    *into = v;
313                }
314            };
315            field(position, &mut transform.position);
316            field(rotation_deg, &mut transform.rotation_deg);
317            field(scale, &mut transform.scale);
318            out.push(Effect::SetTransform { entity, transform });
319        }
320        COp::Spawn {
321            template,
322            position,
323            rotation_deg,
324            scale,
325            lifetime,
326            bind,
327        } => {
328            out.push(Effect::Spawn(SpawnEffect {
329                template: *template,
330                transform: Transform {
331                    position: *position,
332                    rotation_deg: *rotation_deg,
333                    scale: *scale,
334                },
335                lifetime: (*lifetime > 0.0).then_some(*lifetime),
336            }));
337            // The entity does not exist until SpawnSystem applies the request,
338            // so the binding holds nothing this tick and reads skip their node.
339            if let Some(bind) = bind {
340                set_binding(view, *bind, None);
341            }
342        }
343        COp::Despawn(target) => {
344            if let Some(entity) = eval(target, view).and_then(Val::as_entity) {
345                out.push(Effect::Despawn(entity));
346            }
347        }
348        COp::Reparent { child, parent } => {
349            let Some(child) = eval(child, view).and_then(Val::as_entity) else {
350                return;
351            };
352            // A named-but-unresolvable parent skips, so a stale reference never
353            // silently detaches the child to a root.
354            let parent = match parent {
355                Some(expr) => match eval(expr, view).and_then(Val::as_entity) {
356                    Some(entity) => Some(entity),
357                    None => return,
358                },
359                None => None,
360            };
361            out.push(Effect::Reparent { child, parent });
362        }
363        COp::Visible(target, visible) => {
364            if let Some(entity) = eval(target, view).and_then(Val::as_entity) {
365                out.push(Effect::Visible(entity, *visible));
366            }
367        }
368        COp::Sound { clip, kind, volume } => out.push(Effect::Sound(PlayCue {
369            clip: *clip,
370            kind: *kind,
371            volume: *volume,
372            priority: 0,
373        })),
374        COp::Scene { scene, transition } => out.push(Effect::Scene {
375            scene: *scene,
376            transition: transition.clone(),
377        }),
378        COp::Screen(screen) => out.push(Effect::Screen(*screen)),
379        COp::Story(playback) => out.push(Effect::Story(*playback)),
380        COp::Save => out.push(Effect::Save),
381        COp::Never => {}
382    }
383}
384
385fn set_binding(view: &mut View<'_>, slot: u16, value: Option<Val>) {
386    if let Some(slot) = view.bindings.get_mut(slot as usize) {
387        *slot = value;
388    }
389}
390
391#[cfg(test)]
392mod tests {
393    use super::*;
394    use crate::components::CueKind;
395    use crate::ecs::AudioClipHandle;
396    use alloc::boxed::Box;
397    use alloc::vec;
398    use core::num::NonZeroU32;
399
400    fn entity(index: u32) -> Entity {
401        Entity::new(index, NonZeroU32::MIN)
402    }
403
404    fn moved(position: [f32; 3]) -> Transform {
405        Transform {
406            position,
407            ..Transform::default()
408        }
409    }
410
411    // What one evaluated run reads, so a test states only the parts it is
412    // about. Every list defaults empty: an entity absent from `entities` is
413    // dead, one absent from `transforms` has none, and a name absent from
414    // `names` resolves to nothing.
415    #[derive(Default)]
416    struct Run {
417        dt: f32,
418        elapsed: f32,
419        vars: Vec<Val>,
420        locals: Vec<Val>,
421        queries: Vec<Vec<Entity>>,
422        entities: Vec<Entity>,
423        transforms: Vec<(Entity, Transform)>,
424        names: Vec<(AssetId, Entity)>,
425        self_entity: Option<Entity>,
426        bindings: usize,
427    }
428
429    impl Run {
430        fn with_view<R>(&self, body: impl FnOnce(&mut View<'_>) -> R) -> R {
431            let mut bindings = vec![None; self.bindings];
432            let mut trace = None;
433            let mut view = View {
434                dt: self.dt,
435                elapsed: self.elapsed,
436                vars: &self.vars,
437                locals: &self.locals,
438                bindings: &mut bindings,
439                queries: &self.queries,
440                by_name: &|id| self.names.iter().find(|(n, _)| *n == id).map(|(_, e)| *e),
441                transforms: &|e| {
442                    self.transforms
443                        .iter()
444                        .find(|(t, _)| *t == e)
445                        .map(|(_, t)| *t)
446                },
447                alive: &|e| self.entities.contains(&e),
448                self_entity: self.self_entity,
449                trace: &mut trace,
450            };
451            body(&mut view)
452        }
453
454        fn eval(&self, expr: &CExpr) -> Option<Val> {
455            self.with_view(|view| eval(expr, view))
456        }
457
458        fn exec(&self, nodes: &[CNode]) -> Vec<Effect> {
459            let mut out = Vec::new();
460            self.with_view(|view| exec(nodes, view, &mut out));
461            out
462        }
463    }
464
465    fn node(op: COp) -> CNode {
466        CNode { id: 0, op }
467    }
468
469    fn lit(v: Val) -> Box<CExpr> {
470        Box::new(CExpr::Lit(v))
471    }
472
473    #[test]
474    fn a_local_reads_its_slot_and_an_out_of_range_one_yields_nothing() {
475        let run = Run {
476            locals: vec![Val::Int(4), Val::Float(1.5)],
477            ..Run::default()
478        };
479        assert_eq!(run.eval(&CExpr::Local(1)), Some(Val::Float(1.5)));
480        assert_eq!(run.eval(&CExpr::Local(9)), None);
481    }
482
483    #[test]
484    fn dt_and_elapsed_read_the_tick() {
485        let run = Run {
486            dt: 0.25,
487            elapsed: 12.0,
488            ..Run::default()
489        };
490        assert_eq!(run.eval(&CExpr::Dt), Some(Val::Float(0.25)));
491        assert_eq!(run.eval(&CExpr::Elapsed), Some(Val::Float(12.0)));
492    }
493
494    #[test]
495    fn normalize_scales_to_unit_length() {
496        let run = Run::default();
497        let v = run
498            .eval(&CExpr::Normalize(lit(Val::Vec3([0.0, 3.0, 4.0]))))
499            .expect("a vector normalizes");
500        assert_eq!(v, Val::Vec3([0.0, 0.6, 0.8]));
501    }
502
503    // A zero-length vector has no direction, so it normalizes to zero rather
504    // than to the infinities a division by its length would produce.
505    #[test]
506    fn normalize_of_a_zero_vector_is_zero() {
507        let run = Run::default();
508        assert_eq!(
509            run.eval(&CExpr::Normalize(lit(Val::Vec3([0.0; 3])))),
510            Some(Val::Vec3([0.0; 3]))
511        );
512    }
513
514    #[test]
515    fn normalize_of_a_non_vector_yields_nothing() {
516        let run = Run::default();
517        assert_eq!(run.eval(&CExpr::Normalize(lit(Val::Int(3)))), None);
518    }
519
520    #[test]
521    fn not_inverts_a_bool_and_rejects_anything_else() {
522        let run = Run::default();
523        assert_eq!(
524            run.eval(&CExpr::Not(lit(Val::Bool(false)))),
525            Some(Val::Bool(true))
526        );
527        assert_eq!(run.eval(&CExpr::Not(lit(Val::Int(1)))), None);
528    }
529
530    #[test]
531    fn all_holds_only_when_every_operand_does() {
532        let run = Run::default();
533        let all = |items: Vec<CExpr>| run.eval(&CExpr::All(items));
534        assert_eq!(all(Vec::new()), Some(Val::Bool(true)));
535        assert_eq!(
536            all(vec![
537                CExpr::Lit(Val::Bool(true)),
538                CExpr::Lit(Val::Bool(true))
539            ]),
540            Some(Val::Bool(true))
541        );
542        assert_eq!(
543            all(vec![
544                CExpr::Lit(Val::Bool(true)),
545                CExpr::Lit(Val::Bool(false))
546            ]),
547            Some(Val::Bool(false))
548        );
549        assert_eq!(all(vec![CExpr::Lit(Val::Int(1))]), None);
550    }
551
552    #[test]
553    fn any_holds_as_soon_as_one_operand_does() {
554        let run = Run::default();
555        let any = |items: Vec<CExpr>| run.eval(&CExpr::Any(items));
556        assert_eq!(any(Vec::new()), Some(Val::Bool(false)));
557        assert_eq!(
558            any(vec![
559                CExpr::Lit(Val::Bool(false)),
560                CExpr::Lit(Val::Bool(true))
561            ]),
562            Some(Val::Bool(true))
563        );
564        assert_eq!(
565            any(vec![
566                CExpr::Lit(Val::Bool(false)),
567                CExpr::Lit(Val::Bool(false))
568            ]),
569            Some(Val::Bool(false))
570        );
571        assert_eq!(any(vec![CExpr::Lit(Val::Int(1))]), None);
572    }
573
574    #[test]
575    fn a_never_expression_yields_nothing() {
576        assert_eq!(Run::default().eval(&CExpr::Never), None);
577    }
578
579    #[test]
580    fn arithmetic_on_two_ints_stays_an_int() {
581        let run = Run::default();
582        let op = |op| run.eval(&CExpr::Arith(op, lit(Val::Int(7)), lit(Val::Int(2))));
583        assert_eq!(op(Arith::Add), Some(Val::Int(9)));
584        assert_eq!(op(Arith::Sub), Some(Val::Int(5)));
585        assert_eq!(op(Arith::Mul), Some(Val::Int(14)));
586        assert_eq!(op(Arith::Div), Some(Val::Int(3)));
587    }
588
589    // A quotient by zero yields zero rather than the infinity that would
590    // silently poison every transform downstream of it.
591    #[test]
592    fn division_by_zero_yields_zero() {
593        let run = Run::default();
594        assert_eq!(
595            run.eval(&CExpr::Arith(
596                Arith::Div,
597                lit(Val::Int(7)),
598                lit(Val::Int(0))
599            )),
600            Some(Val::Int(0))
601        );
602        assert_eq!(
603            run.eval(&CExpr::Arith(
604                Arith::Div,
605                lit(Val::Vec3([1.0, 2.0, 3.0])),
606                lit(Val::Float(0.0)),
607            )),
608            Some(Val::Vec3([0.0; 3]))
609        );
610    }
611
612    #[test]
613    fn arithmetic_on_two_vectors_is_component_wise() {
614        let run = Run::default();
615        assert_eq!(
616            run.eval(&CExpr::Arith(
617                Arith::Add,
618                lit(Val::Vec3([1.0, 2.0, 3.0])),
619                lit(Val::Vec3([10.0, 20.0, 30.0])),
620            )),
621            Some(Val::Vec3([11.0, 22.0, 33.0]))
622        );
623    }
624
625    #[test]
626    fn a_vector_and_a_scalar_combine_component_wise_either_way_round() {
627        let run = Run::default();
628        assert_eq!(
629            run.eval(&CExpr::Arith(
630                Arith::Mul,
631                lit(Val::Vec3([1.0, 2.0, 3.0])),
632                lit(Val::Float(2.0)),
633            )),
634            Some(Val::Vec3([2.0, 4.0, 6.0]))
635        );
636        // Order matters for the non-commutative operators: the scalar is the
637        // left operand of every component.
638        assert_eq!(
639            run.eval(&CExpr::Arith(
640                Arith::Sub,
641                lit(Val::Int(10)),
642                lit(Val::Vec3([1.0, 2.0, 3.0])),
643            )),
644            Some(Val::Vec3([9.0, 8.0, 7.0]))
645        );
646    }
647
648    #[test]
649    fn a_vector_against_a_non_numeric_yields_nothing() {
650        let run = Run::default();
651        assert_eq!(
652            run.eval(&CExpr::Arith(
653                Arith::Add,
654                lit(Val::Vec3([1.0; 3])),
655                lit(Val::Bool(true)),
656            )),
657            None
658        );
659        assert_eq!(
660            run.eval(&CExpr::Arith(
661                Arith::Add,
662                lit(Val::Bool(true)),
663                lit(Val::Vec3([1.0; 3])),
664            )),
665            None
666        );
667    }
668
669    #[test]
670    fn a_mixed_int_and_float_widens_to_a_float() {
671        let run = Run::default();
672        assert_eq!(
673            run.eval(&CExpr::Arith(
674                Arith::Add,
675                lit(Val::Int(1)),
676                lit(Val::Float(0.5)),
677            )),
678            Some(Val::Float(1.5))
679        );
680        assert_eq!(
681            run.eval(&CExpr::Arith(
682                Arith::Add,
683                lit(Val::Bool(true)),
684                lit(Val::Int(1)),
685            )),
686            None
687        );
688    }
689
690    #[test]
691    fn entities_and_bools_compare_by_identity_rather_than_as_numbers() {
692        let run = Run::default();
693        let cmp = |op, a, b| run.eval(&CExpr::Compare(op, lit(a), lit(b)));
694        let (a, b) = (Val::Entity(entity(1)), Val::Entity(entity(2)));
695        assert_eq!(cmp(Cmp::Eq, a, a), Some(Val::Bool(true)));
696        assert_eq!(cmp(Cmp::Eq, a, b), Some(Val::Bool(false)));
697        assert_eq!(cmp(Cmp::Ne, a, b), Some(Val::Bool(true)));
698        assert_eq!(
699            cmp(Cmp::Eq, Val::Bool(true), Val::Bool(true)),
700            Some(Val::Bool(true))
701        );
702        // Neither side reads as a number, so an ordering comparison of them
703        // has no answer.
704        assert_eq!(cmp(Cmp::Lt, a, b), None);
705    }
706
707    #[test]
708    fn numbers_compare_in_every_ordering() {
709        let run = Run::default();
710        let cmp = |op| {
711            run.eval(&CExpr::Compare(op, lit(Val::Int(1)), lit(Val::Float(2.0))))
712                .and_then(Val::as_bool)
713        };
714        assert_eq!(cmp(Cmp::Eq), Some(false));
715        assert_eq!(cmp(Cmp::Ne), Some(true));
716        assert_eq!(cmp(Cmp::Lt), Some(true));
717        assert_eq!(cmp(Cmp::Le), Some(true));
718        assert_eq!(cmp(Cmp::Gt), Some(false));
719        assert_eq!(cmp(Cmp::Ge), Some(false));
720    }
721
722    #[test]
723    fn an_unevaluable_comparison_yields_nothing() {
724        let run = Run::default();
725        assert_eq!(
726            run.eval(&CExpr::Compare(
727                Cmp::Eq,
728                lit(Val::Bool(true)),
729                lit(Val::Int(1)),
730            )),
731            None
732        );
733    }
734
735    #[test]
736    fn a_condition_that_is_not_a_bool_runs_neither_branch() {
737        let run = Run::default();
738        let effects = run.exec(&[node(COp::If {
739            cond: CExpr::Lit(Val::Int(1)),
740            then: vec![node(COp::Save)],
741            otherwise: vec![node(COp::Save)],
742        })]);
743        assert!(effects.is_empty(), "{effects:?}");
744    }
745
746    #[test]
747    fn a_for_each_over_an_undeclared_query_runs_nothing() {
748        let run = Run {
749            bindings: 1,
750            ..Run::default()
751        };
752        let effects = run.exec(&[node(COp::ForEach {
753            query: 3,
754            bind: 0,
755            body: vec![node(COp::Save)],
756        })]);
757        assert!(effects.is_empty(), "{effects:?}");
758    }
759
760    #[test]
761    fn setting_a_local_from_an_unevaluable_value_asks_for_nothing() {
762        let run = Run::default();
763        let effects = run.exec(&[node(COp::SetLocal {
764            slot: 0,
765            value: CExpr::Never,
766            add: false,
767        })]);
768        assert!(effects.is_empty(), "{effects:?}");
769    }
770
771    #[test]
772    fn a_transform_write_skips_an_entity_it_cannot_resolve() {
773        let ghost = entity(7);
774        // Resolves to no entity at all.
775        let run = Run::default();
776        let effects = run.exec(&[node(COp::SetTransform {
777            entity: CExpr::Never,
778            position: None,
779            rotation_deg: None,
780            scale: None,
781        })]);
782        assert!(effects.is_empty(), "{effects:?}");
783
784        // Resolves to an entity that carries no transform to overwrite.
785        let effects = run.exec(&[node(COp::SetTransform {
786            entity: CExpr::Lit(Val::Entity(ghost)),
787            position: None,
788            rotation_deg: None,
789            scale: None,
790        })]);
791        assert!(effects.is_empty(), "{effects:?}");
792    }
793
794    #[test]
795    fn a_transform_write_keeps_the_fields_it_was_not_given() {
796        let e = entity(1);
797        let run = Run {
798            transforms: vec![(e, moved([1.0, 2.0, 3.0]))],
799            ..Run::default()
800        };
801        let effects = run.exec(&[node(COp::SetTransform {
802            entity: CExpr::Lit(Val::Entity(e)),
803            position: Some(CExpr::Lit(Val::Vec3([9.0; 3]))),
804            // Neither an absent field nor one that evaluates to a non-vector
805            // overwrites what the entity already carries.
806            rotation_deg: None,
807            scale: Some(CExpr::Lit(Val::Int(2))),
808        })]);
809        let [Effect::SetTransform { entity, transform }] = effects.as_slice() else {
810            panic!("expected one transform write, got {effects:?}");
811        };
812        assert_eq!(*entity, e);
813        assert_eq!(transform.position, [9.0; 3]);
814        assert_eq!(transform.scale, Transform::default().scale);
815    }
816
817    #[test]
818    fn a_reparent_skips_a_child_it_cannot_resolve() {
819        let run = Run::default();
820        let effects = run.exec(&[node(COp::Reparent {
821            child: CExpr::Never,
822            parent: None,
823        })]);
824        assert!(effects.is_empty(), "{effects:?}");
825    }
826
827    // A stale parent reference skips the whole node rather than detaching the
828    // child to the root, which is what a `None` parent means.
829    #[test]
830    fn a_reparent_onto_an_unresolvable_parent_skips_rather_than_detaching() {
831        let run = Run::default();
832        let effects = run.exec(&[node(COp::Reparent {
833            child: CExpr::Lit(Val::Entity(entity(1))),
834            parent: Some(CExpr::Never),
835        })]);
836        assert!(effects.is_empty(), "{effects:?}");
837    }
838
839    #[test]
840    fn a_reparent_moves_the_child_under_a_parent_or_to_the_root() {
841        let (child, parent) = (entity(1), entity(2));
842        let run = Run::default();
843        let effects = run.exec(&[
844            node(COp::Reparent {
845                child: CExpr::Lit(Val::Entity(child)),
846                parent: Some(CExpr::Lit(Val::Entity(parent))),
847            }),
848            node(COp::Reparent {
849                child: CExpr::Lit(Val::Entity(child)),
850                parent: None,
851            }),
852        ]);
853        let [
854            Effect::Reparent {
855                child: a,
856                parent: Some(p),
857            },
858            Effect::Reparent {
859                child: b,
860                parent: None,
861            },
862        ] = effects.as_slice()
863        else {
864            panic!("expected two reparents, got {effects:?}");
865        };
866        assert_eq!((*a, *p, *b), (child, parent, child));
867    }
868
869    #[test]
870    fn the_request_only_nodes_each_push_what_they_name() {
871        let run = Run::default();
872        let effects = run.exec(&[
873            node(COp::Sound {
874                clip: AudioClipHandle(3),
875                kind: CueKind::Music,
876                volume: 0.5,
877            }),
878            node(COp::Scene {
879                scene: AssetId(1),
880                transition: String::from("fade"),
881            }),
882            node(COp::Screen(AssetId(2))),
883            // A node the checker should have rejected asks for nothing rather
884            // than guessing at what it meant.
885            node(COp::Never),
886        ]);
887        let [
888            Effect::Sound(cue),
889            Effect::Scene { scene, transition },
890            Effect::Screen(screen),
891        ] = effects.as_slice()
892        else {
893            panic!("expected three requests, got {effects:?}");
894        };
895        assert_eq!(
896            (cue.clip, cue.kind, cue.volume),
897            (AudioClipHandle(3), CueKind::Music, 0.5)
898        );
899        assert_eq!(*scene, AssetId(1));
900        assert_eq!(transition, "fade");
901        assert_eq!(*screen, AssetId(2));
902    }
903}