Skip to main content

mittens_engine/scripting/
object.rs

1use std::collections::HashMap;
2use std::hash::{Hash, Hasher};
3use std::sync::{Arc, Mutex, Weak};
4
5use crate::engine::ecs::ComponentId;
6use crate::scripting::ast::BlockStatement;
7use crate::scripting::block_effect_analyzer::BlockEffectAnalysis;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum BuiltinTableKind {
11    Math,
12    MusicNote,
13}
14
15// ---------------------------------------------------------------------------
16// Materialized component expression
17// ---------------------------------------------------------------------------
18
19/// A fully-evaluated component expression: all values are concrete `Value`s,
20/// all control flow has been expanded, all constructor args evaluated.
21///
22/// Produced by the evaluator when a `ComponentExpression` AST node is
23/// evaluated. Passed to `spawn_tree` in the registry — no MMS expression
24/// evaluation needed on the main thread.
25#[derive(Debug, Clone, PartialEq)]
26pub struct RuntimeClosure {
27    pub body: BlockStatement,
28    pub captured_env: Arc<HashMap<String, Value>>,
29    pub heap: HeapHandle,
30    pub analysis: Option<BlockEffectAnalysis>,
31}
32
33#[derive(Debug, Clone, PartialEq)]
34pub struct MaterializedCE {
35    /// Component type name (short or full, e.g. `"T"` / `"Transform"`).
36    pub component_type: String,
37    /// When true, `name = expr` inside the CE body is captured as a named
38    /// component property instead of a lexical reassignment.
39    pub component_property_assignment_only: bool,
40    /// First constructor call method, e.g. `"position"` from `T.position(...)`.
41    pub ctor_method: Option<String>,
42    /// First constructor call args, evaluated.
43    pub ctor_args: Vec<Value>,
44    /// Remaining chained constructor calls + body builder calls, in source order.
45    /// e.g. `.scale(...)` after `.position(...)`, plus `fps_rotation()` in the body.
46    pub calls: Vec<(String, Vec<Value>)>,
47    /// Named property assignments from the body, e.g. `intensity = 0.9`.
48    pub named: Vec<(String, Value)>,
49    /// String-type positional content (e.g. `Text { "hello " + name }`).
50    pub positionals: Vec<Value>,
51    /// Deferred executable block payload for components that own imperative
52    /// runtime bodies, such as `Keyframe.at(...) { ... }`.
53    pub deferred_block: Option<RuntimeClosure>,
54    /// Child component trees, in source order. Each entry is either a CE to
55    /// spawn fresh, or a pre-Registered `ComponentId` to splice in.
56    pub children: Vec<CeChild>,
57}
58
59/// A child slot inside a `MaterializedCE`.
60///
61/// `Spawn` is the normal case (the body produced a fresh CE). `Attach` is used
62/// when the body referenced a `Value::ComponentObject` — a previously
63/// `Register`ed component — that should be attached as a child rather than
64/// re-created.
65#[derive(Debug, Clone, PartialEq)]
66pub enum CeChild {
67    Spawn(MaterializedCE),
68    Attach(ComponentId),
69}
70
71// ---------------------------------------------------------------------------
72// Runtime values
73// ---------------------------------------------------------------------------
74
75/// Runtime value representation for Meow Meow evaluation.
76#[derive(Debug, Clone, PartialEq)]
77pub enum Value {
78    Null,
79    Bool(bool),
80    Number(f64),
81    /// Numeric value tagged with a source-level unit suffix (e.g. `50%`,
82    /// `20gu`, `30deg`). Produced by `Expression::Dimension`. Consumers
83    /// such as the Style setters use this to disambiguate `Percent` vs
84    /// `GlyphUnits` at the boundary between MMS values and engine types.
85    Dimension {
86        value: f64,
87        unit: crate::scripting::token::Unit,
88    },
89    String(String),
90    Array(Vec<Value>),
91    Map(HashMap<String, Value>),
92
93    /// A live engine component (already spawned). Holds the engine-side
94    /// `ComponentId` and the MMS component type name (e.g. `"Anim"`, `"T"`).
95    /// Produced when `let x = CE` is evaluated with a live reply channel
96    /// (`eval_with_world`). The `component_type` drives method dispatch.
97    ComponentObject {
98        id: ComponentId,
99        component_type: String,
100    },
101
102    /// Heap-allocated MMS object (map / record / instance).
103    Object(ObjectId),
104
105    /// Symbolic identifier value (e.g. enum-like flags passed to constructors).
106    Identifier(String),
107
108    /// Host-provided built-in namespace/table.
109    BuiltinTable(BuiltinTableKind),
110
111    /// A fully-evaluated component expression ready to spawn.
112    /// Produced whenever a `ComponentExpression` AST node is evaluated.
113    ComponentExpr(Box<MaterializedCE>),
114
115    /// A closure: params + body AST + captured environment snapshot.
116    Function {
117        params: Vec<String>,
118        body: crate::scripting::ast::BlockStatement,
119        captured_env: Arc<HashMap<String, Value>>,
120        heap: HeapHandle,
121    },
122
123    /// A loaded module: named exports + ordered sequence of root CE emits.
124    Module {
125        named: HashMap<String, Value>,
126        sequence: Vec<MaterializedCE>,
127        heap: HeapHandle,
128    },
129}
130
131// ---------------------------------------------------------------------------
132// MMS heap objects
133// ---------------------------------------------------------------------------
134
135#[derive(Debug, Clone)]
136pub struct ObjectId {
137    slot: u32,
138    heap: Weak<Mutex<Heap>>,
139}
140
141impl ObjectId {
142    pub fn as_u32(&self) -> u32 {
143        self.slot
144    }
145
146    pub fn get(&self) -> Option<Object> {
147        let heap = self.heap.upgrade()?;
148        heap.lock().ok()?.get(self.clone()).cloned()
149    }
150
151    pub fn with_map<R>(&self, f: impl FnOnce(&HashMap<String, Value>) -> R) -> Option<R> {
152        let heap = self.heap.upgrade()?;
153        let heap = heap.lock().ok()?;
154        let Object::Map(map) = heap.get(self.clone())?;
155        Some(f(map))
156    }
157
158    pub fn with_map_mut<R>(&self, f: impl FnOnce(&mut HashMap<String, Value>) -> R) -> Option<R> {
159        let heap = self.heap.upgrade()?;
160        let mut heap = heap.lock().ok()?;
161        let Object::Map(map) = heap.get_mut(self.clone())?;
162        Some(f(map))
163    }
164}
165
166impl PartialEq for ObjectId {
167    fn eq(&self, other: &Self) -> bool {
168        self.slot == other.slot && Weak::as_ptr(&self.heap) == Weak::as_ptr(&other.heap)
169    }
170}
171
172impl Eq for ObjectId {}
173
174impl Hash for ObjectId {
175    fn hash<H: Hasher>(&self, state: &mut H) {
176        self.slot.hash(state);
177        Weak::as_ptr(&self.heap).hash(state);
178    }
179}
180
181#[derive(Debug, Clone, PartialEq)]
182pub enum Object {
183    /// Simple string-keyed map.
184    Map(HashMap<String, Value>),
185}
186
187#[derive(Debug, Default)]
188pub struct Heap {
189    objects: Vec<Object>,
190}
191
192impl Heap {
193    pub fn new() -> Self {
194        Self::default()
195    }
196
197    fn alloc_in(handle: &HeapHandle, object: Object) -> ObjectId {
198        let slot = {
199            let mut heap = handle.0.lock().expect("heap lock poisoned");
200            let slot = heap
201                .objects
202                .len()
203                .try_into()
204                .expect("too many heap objects");
205            heap.objects.push(object);
206            slot
207        };
208        ObjectId {
209            slot,
210            heap: Arc::downgrade(&handle.0),
211        }
212    }
213
214    pub fn get(&self, id: ObjectId) -> Option<&Object> {
215        self.objects.get(id.slot as usize)
216    }
217
218    pub fn get_mut(&mut self, id: ObjectId) -> Option<&mut Object> {
219        self.objects.get_mut(id.slot as usize)
220    }
221
222    pub fn len(&self) -> usize {
223        self.objects.len()
224    }
225
226    pub fn is_empty(&self) -> bool {
227        self.objects.is_empty()
228    }
229}
230
231#[derive(Debug, Clone)]
232pub struct HeapHandle(Arc<Mutex<Heap>>);
233
234impl HeapHandle {
235    pub fn new() -> Self {
236        Self(Arc::new(Mutex::new(Heap::new())))
237    }
238
239    pub fn alloc(&self, object: Object) -> ObjectId {
240        Heap::alloc_in(self, object)
241    }
242}
243
244impl Default for HeapHandle {
245    fn default() -> Self {
246        Self::new()
247    }
248}
249
250impl PartialEq for HeapHandle {
251    fn eq(&self, other: &Self) -> bool {
252        Arc::ptr_eq(&self.0, &other.0)
253    }
254}
255
256// ---------------------------------------------------------------------------
257// ObjectWorld — the MMS worker thread's evaluated object layer
258// ---------------------------------------------------------------------------
259
260/// What a scope frame can do at its boundary.
261///
262/// - `Block` — fully transparent: read & reassign walk past it. Used for plain
263///   blocks, loops, if-bodies, and CE bodies (CE bodies are not write-barriered;
264///   children can mutate parent CE locals if they choose to).
265/// - `Function` — hard barrier: read & reassign both stop. Seeded with a closure's
266///   `captured_env`; the function body cannot see the caller's locals.
267#[derive(Debug, Clone, Copy, PartialEq, Eq)]
268pub enum FrameKind {
269    Block,
270    Function,
271}
272
273#[derive(Debug, Default)]
274struct Frame {
275    kind_or_root: Option<FrameKind>,
276    bindings: HashMap<String, Value>,
277    captured_bindings: Option<Arc<HashMap<String, Value>>>,
278}
279
280impl Frame {
281    fn new(kind: FrameKind) -> Self {
282        Self {
283            kind_or_root: Some(kind),
284            bindings: HashMap::new(),
285            captured_bindings: None,
286        }
287    }
288    fn root() -> Self {
289        Self {
290            kind_or_root: None,
291            bindings: HashMap::new(),
292            captured_bindings: None,
293        }
294    }
295    fn is_function_barrier(&self) -> bool {
296        matches!(self.kind_or_root, Some(FrameKind::Function))
297    }
298}
299
300/// The scripting-side runtime container. Lives on the MMS worker thread.
301///
302/// Holds the lexical scope chain (a stack of frames) and the MMS-side heap.
303/// Communication with the engine goes through intent channels owned by the
304/// evaluator — `ObjectWorld` itself never sends intents directly.
305///
306/// See `docs/meow_meow/spec/env-heap-object-world.md` for the full design and
307/// `docs/meow_meow/task/frame-stack-object-world.md` for the migration plan.
308#[derive(Debug)]
309pub struct ObjectWorld {
310    frames: Vec<Frame>,
311    heap: HeapHandle,
312    reset_requested: bool,
313}
314
315impl Default for ObjectWorld {
316    fn default() -> Self {
317        Self {
318            frames: vec![Frame::root()],
319            heap: HeapHandle::new(),
320            reset_requested: false,
321        }
322    }
323}
324
325impl ObjectWorld {
326    /// New `ObjectWorld` with one root frame already pushed.
327    pub fn new() -> Self {
328        Self::default()
329    }
330
331    pub fn with_heap(heap: HeapHandle) -> Self {
332        Self {
333            frames: vec![Frame::root()],
334            heap,
335            reset_requested: false,
336        }
337    }
338
339    // --- Frame management ---
340
341    pub fn push_frame(&mut self, kind: FrameKind) {
342        self.frames.push(Frame::new(kind));
343    }
344
345    /// Push a function frame seeded with the closure's captured environment.
346    /// This is a hard barrier: lookups inside the function will not see past it.
347    pub fn push_function_frame(&mut self, captured: Arc<HashMap<String, Value>>) {
348        self.frames.push(Frame {
349            kind_or_root: Some(FrameKind::Function),
350            bindings: HashMap::new(),
351            captured_bindings: Some(captured),
352        });
353    }
354
355    pub fn pop_frame(&mut self) {
356        // Never pop the root frame.
357        if self.frames.len() > 1 {
358            self.frames.pop();
359        }
360    }
361
362    pub fn frame_depth(&self) -> usize {
363        self.frames.len()
364    }
365
366    pub fn request_reset(&mut self) {
367        self.reset_requested = true;
368    }
369
370    pub fn take_reset_requested(&mut self) -> bool {
371        std::mem::take(&mut self.reset_requested)
372    }
373
374    // --- Variable environment ---
375
376    /// Bind a name in the top (innermost) frame. Shadows outer bindings.
377    pub fn bind(&mut self, name: impl Into<String>, value: Value) {
378        let top = self.frames.last_mut().expect("ObjectWorld: no frames");
379        top.bindings.insert(name.into(), value);
380    }
381
382    /// Look up a name. Walks frames from innermost outward; stops at a
383    /// `Function` barrier (function bodies cannot see the caller's locals).
384    pub fn lookup(&self, name: &str) -> Option<&Value> {
385        for frame in self.frames.iter().rev() {
386            if let Some(v) = frame.bindings.get(name) {
387                return Some(v);
388            }
389            if let Some(v) = frame
390                .captured_bindings
391                .as_ref()
392                .and_then(|captured| captured.get(name))
393            {
394                return Some(v);
395            }
396            if frame.is_function_barrier() {
397                return None;
398            }
399        }
400        None
401    }
402
403    /// Whether `name` is reachable from the current scope (same walk as `lookup`).
404    pub fn has(&self, name: &str) -> bool {
405        self.lookup(name).is_some()
406    }
407
408    /// Reassign an existing binding. Walks frames inward-to-outward looking for
409    /// the declaring frame; stops at a `Function` barrier.
410    ///
411    /// Errors:
412    /// - name not declared anywhere reachable
413    /// - name declared only beyond the function barrier (caller's locals)
414    pub fn reassign(&mut self, name: &str, value: Value) -> Result<(), String> {
415        for frame in self.frames.iter_mut().rev() {
416            if frame.bindings.contains_key(name) {
417                frame.bindings.insert(name.to_string(), value);
418                return Ok(());
419            }
420            if frame
421                .captured_bindings
422                .as_ref()
423                .is_some_and(|captured| captured.contains_key(name))
424            {
425                frame.bindings.insert(name.to_string(), value);
426                return Ok(());
427            }
428            if matches!(frame.kind_or_root, Some(FrameKind::Function)) {
429                return Err(format!(
430                    "cannot reassign '{}' from inside function (only its captured snapshot is visible)",
431                    name
432                ));
433            }
434        }
435        Err(format!("reassignment: '{}' is not defined", name))
436    }
437
438    /// Flatten all frames visible from the current point into a single map for
439    /// closure capture. Walks innermost outward, **including** the first
440    /// `Function` barrier's frame (so a closure created inside a function sees
441    /// that function's captured snapshot), then stops. Inner names shadow outer.
442    pub fn snapshot_visible(&self) -> HashMap<String, Value> {
443        let mut out: HashMap<String, Value> = HashMap::new();
444        for frame in self.frames.iter().rev() {
445            for (k, v) in &frame.bindings {
446                out.entry(k.clone()).or_insert_with(|| v.clone());
447            }
448            if let Some(captured) = &frame.captured_bindings {
449                for (k, v) in captured.iter() {
450                    out.entry(k.clone()).or_insert_with(|| v.clone());
451                }
452            }
453            if frame.is_function_barrier() {
454                break;
455            }
456        }
457        out
458    }
459
460    // --- Heap access ---
461
462    pub fn heap(&self) -> &HeapHandle {
463        &self.heap
464    }
465
466    pub fn alloc_object(&self, object: Object) -> ObjectId {
467        self.heap.alloc(object)
468    }
469}
470
471// ---------------------------------------------------------------------------
472// Tests
473// ---------------------------------------------------------------------------
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478    use std::sync::Arc;
479
480    fn n(x: f64) -> Value {
481        Value::Number(x)
482    }
483
484    #[test]
485    fn root_frame_bind_and_lookup() {
486        let mut ow = ObjectWorld::new();
487        ow.bind("x", n(1.0));
488        assert_eq!(ow.lookup("x"), Some(&n(1.0)));
489        assert!(ow.has("x"));
490        assert!(!ow.has("y"));
491    }
492
493    #[test]
494    fn block_frame_is_transparent_for_read_and_write() {
495        let mut ow = ObjectWorld::new();
496        ow.bind("x", n(1.0));
497        ow.push_frame(FrameKind::Block);
498        // Read sees outer
499        assert_eq!(ow.lookup("x"), Some(&n(1.0)));
500        // Reassign writes through to outer
501        ow.reassign("x", n(2.0)).unwrap();
502        ow.pop_frame();
503        assert_eq!(ow.lookup("x"), Some(&n(2.0)));
504    }
505
506    #[test]
507    fn block_frame_local_let_does_not_leak() {
508        let mut ow = ObjectWorld::new();
509        ow.push_frame(FrameKind::Block);
510        ow.bind("local", n(42.0));
511        assert_eq!(ow.lookup("local"), Some(&n(42.0)));
512        ow.pop_frame();
513        assert_eq!(ow.lookup("local"), None);
514    }
515
516    #[test]
517    fn function_frame_blocks_read_of_caller_locals() {
518        let mut ow = ObjectWorld::new();
519        ow.bind("caller_var", n(1.0));
520        let mut captured = HashMap::new();
521        captured.insert("captured_var".to_string(), n(99.0));
522        ow.push_function_frame(Arc::new(captured));
523        assert_eq!(ow.lookup("captured_var"), Some(&n(99.0)));
524        assert_eq!(ow.lookup("caller_var"), None);
525    }
526
527    #[test]
528    fn function_frame_blocks_reassign_of_caller_locals() {
529        let mut ow = ObjectWorld::new();
530        ow.bind("caller_var", n(1.0));
531        ow.push_function_frame(Arc::new(HashMap::new()));
532        let err = ow.reassign("caller_var", n(2.0)).unwrap_err();
533        assert!(err.contains("inside function"), "got: {}", err);
534    }
535
536    #[test]
537    fn reassign_undefined_errors() {
538        let mut ow = ObjectWorld::new();
539        let err = ow.reassign("nope", n(1.0)).unwrap_err();
540        assert!(err.contains("not defined"), "got: {}", err);
541    }
542
543    #[test]
544    fn nested_block_reassign_walks_to_declaring_frame() {
545        let mut ow = ObjectWorld::new();
546        ow.bind("sum", n(0.0));
547        ow.push_frame(FrameKind::Block);
548        ow.push_frame(FrameKind::Block);
549        ow.reassign("sum", n(6.0)).unwrap();
550        ow.pop_frame();
551        ow.pop_frame();
552        assert_eq!(ow.lookup("sum"), Some(&n(6.0)));
553    }
554
555    #[test]
556    fn snapshot_visible_flattens_with_inner_shadowing() {
557        let mut ow = ObjectWorld::new();
558        ow.bind("a", n(1.0));
559        ow.bind("b", n(2.0));
560        ow.push_frame(FrameKind::Block);
561        ow.bind("b", n(20.0)); // shadows
562        ow.bind("c", n(3.0));
563        let snap = ow.snapshot_visible();
564        assert_eq!(snap.get("a"), Some(&n(1.0)));
565        assert_eq!(snap.get("b"), Some(&n(20.0))); // inner wins
566        assert_eq!(snap.get("c"), Some(&n(3.0)));
567    }
568
569    #[test]
570    fn snapshot_visible_stops_at_function_barrier() {
571        let mut ow = ObjectWorld::new();
572        ow.bind("caller", n(1.0));
573        let mut captured = HashMap::new();
574        captured.insert("cap".to_string(), n(2.0));
575        ow.push_function_frame(Arc::new(captured));
576        ow.bind("inner", n(3.0));
577        let snap = ow.snapshot_visible();
578        assert_eq!(snap.get("inner"), Some(&n(3.0)));
579        assert_eq!(snap.get("cap"), Some(&n(2.0)));
580        assert_eq!(snap.get("caller"), None);
581    }
582
583    #[test]
584    fn reassign_captured_binding_shadows_shared_snapshot_in_current_call() {
585        let mut ow = ObjectWorld::new();
586        let mut captured = HashMap::new();
587        captured.insert("cap".to_string(), n(2.0));
588        ow.push_function_frame(Arc::new(captured));
589        ow.reassign("cap", n(5.0)).unwrap();
590        assert_eq!(ow.lookup("cap"), Some(&n(5.0)));
591    }
592
593    #[test]
594    fn pop_frame_does_not_pop_root() {
595        let mut ow = ObjectWorld::new();
596        let depth0 = ow.frame_depth();
597        ow.pop_frame();
598        ow.pop_frame();
599        assert_eq!(ow.frame_depth(), depth0);
600    }
601
602    #[test]
603    fn bind_in_inner_frame_shadows_outer_for_lookup() {
604        let mut ow = ObjectWorld::new();
605        ow.bind("x", n(1.0));
606        ow.push_frame(FrameKind::Block);
607        ow.bind("x", n(2.0));
608        assert_eq!(ow.lookup("x"), Some(&n(2.0)));
609        ow.pop_frame();
610        assert_eq!(ow.lookup("x"), Some(&n(1.0)));
611    }
612}