Skip to main content

mittens_engine/scripting/
world_evaluator.rs

1use std::cell::RefCell;
2use std::collections::HashMap;
3use std::sync::Arc;
4use std::thread::{self, JoinHandle};
5
6use rtrb::{Consumer, Producer, RingBuffer};
7
8use crate::engine::ecs::ComponentId;
9use crate::engine::ecs::IntentValue;
10use crate::engine::ecs::SignalEmitter;
11use crate::engine::ecs::SignalKind;
12use crate::engine::ecs::World;
13use crate::engine::ecs::component::{AnimationState, ComponentRef, MusicNote};
14use crate::scripting::ast::{
15    BinOpKind, CallExpression, ComponentExpression, ElseBranch, Expression, IfStatement,
16    ImportItem, Statement, UnaryOpKind,
17};
18use crate::scripting::block_effect_analyzer::BlockEffectAnalyzer;
19use crate::scripting::component_method_registry::{
20    invoke_component_method, supports_component_method,
21};
22use crate::scripting::component_registry::{
23    component_expr_uses_property_assignment_only, is_universal_component_named_prop,
24};
25use crate::scripting::object::{
26    BuiltinTableKind, CeChild, FrameKind, MaterializedCE, Object, ObjectWorld, RuntimeClosure,
27    Value,
28};
29use crate::scripting::parser::{MeowMeowParser, ParseError};
30use crate::scripting::token::TokenizeError;
31use crate::scripting::tokenizer::MeowMeowTokenizer;
32use crate::scripting::transform::{EmitLiftTransform, QueryDesugarTransform};
33
34// ---------------------------------------------------------------------------
35// Thread protocol
36// ---------------------------------------------------------------------------
37
38#[derive(Debug, Clone)]
39pub enum EvalRequest {
40    /// Parse and evaluate a script. Emitted `SpawnComponentTree` intents come back
41    /// as `EvalResponse::Intent` messages.
42    EvalScript {
43        source: String,
44        source_path: Option<String>,
45    },
46    /// Evaluate one interactive snippet while preserving the session's bindings and heap.
47    EvalSnippet {
48        source: String,
49        /// Dynamic REPL cursor. This is deliberately not installed in the
50        /// lexical environment, so closures cannot capture it.
51        cwd: Option<Value>,
52    },
53    /// Evaluate exactly one expression for `cd`. Unlike an ordinary snippet,
54    /// this never auto-emits or registers a component expression.
55    EvalNavigation {
56        source: String,
57        cwd: Option<Value>,
58    },
59    /// Parse only — returns a debug AST string (used in tests / tooling).
60    ParseScript {
61        source: String,
62    },
63    /// Reply to a pending `EvalResponse::HostCall`. The `id` must match the
64    /// correlation id from the HostCall that is being answered.
65    HostCallResult {
66        id: u32,
67        value: HostValue,
68    },
69    Shutdown,
70}
71
72#[derive(Debug, Clone)]
73pub enum EvalResponse {
74    /// A `SpawnComponentTree` (or other) intent ready to be pushed into the engine.
75    Intent(IntentValue),
76    /// Parse-only debug output (from `ParseScript`).
77    ParsedOk {
78        debug_ast: String,
79    },
80    Error {
81        message: String,
82    },
83    /// Always emitted once for an `EvalSnippet`, including parse/evaluation failures.
84    SnippetComplete {
85        result: Result<Option<Value>, String>,
86    },
87    NavigationComplete {
88        result: Result<Value, String>,
89    },
90    /// Tells the engine-side REPL to reset its independently-owned cursor.
91    ReplReset,
92    ShutdownAck,
93    /// The evaluator needs the host to perform a side-effecting operation and
94    /// return a result before evaluation can continue. The host must push a
95    /// matching `EvalRequest::HostCallResult { id, value }` to unblock the
96    /// evaluator thread.
97    HostCall {
98        id: u32,
99        kind: HostCallKind,
100    },
101}
102
103/// Operations the evaluator can request from the host.
104#[derive(Debug, Clone)]
105pub enum HostCallKind {
106    /// Spawn a component tree and return its root `ComponentId`.
107    /// Used for fire-and-forget root emissions (currently unused by the
108    /// evaluator — top-level CEs are still pushed as `IntentValue` for now).
109    Spawn(MaterializedCE),
110    /// Create the component tree in the world but do **not** attach it to a
111    /// parent and do **not** run init. Returns the root `ComponentId`. The
112    /// caller (typically `let x = CE`) holds the id as a `ComponentObject`
113    /// and decides where/when to splice the subtree in.
114    Register(MaterializedCE),
115    /// Attach a previously `Register`ed (or `Spawn`ed) detached subtree to a
116    /// parent and run the deferred init walk. With `parent: None` the subtree
117    /// is initialised in place as a world root.
118    Attach {
119        parent: Option<ComponentId>,
120        child: ComponentId,
121    },
122    /// Register an MMS function as a scoped signal handler.
123    /// The host installs the closure and replies with `HostValue::Null`.
124    RegisterHandler {
125        scope: ComponentId,
126        signal_kind: SignalKind,
127        name: Option<String>,
128        handler: Value,
129    },
130    /// Query the live ECS world. `scope = None` means search from the world's
131    /// canonical roots; `scope = Some(id)` restricts the search to the
132    /// subtree rooted at `id`. `multiple` selects between `query_all`
133    /// (`true`, returns `ComponentList`) and `query` (`false`, returns the
134    /// first match as `Component` or `Null` if none).
135    Query {
136        selector: String,
137        scope: Option<ComponentId>,
138        multiple: bool,
139    },
140    ReplTree {
141        value: Value,
142        max_depth: Option<usize>,
143    },
144    ReplDump {
145        value: Value,
146    },
147    ReplHelp,
148    ReplClear,
149    /// Create a new `AudioClipComponent` that shares `source`'s decoded
150    /// asset but gets its own playhead (RT instance). Returns the new
151    /// component's id, detached — mirrors `Register` semantics so the
152    /// caller can splice it via the usual CE-body bare-reference path.
153    /// See docs/draft/audio-clip-instance-cloning.md §3.
154    AudioClipInstance {
155        source: ComponentId,
156        start_beat: Option<f64>,
157        stop_beat: Option<f64>,
158    },
159    InvokeComponentMethod {
160        id: ComponentId,
161        component_type: String,
162        method: String,
163        args: Vec<Value>,
164    },
165}
166
167/// Values the host can return in response to a `HostCall`.
168#[derive(Debug, Clone)]
169pub enum HostValue {
170    ComponentId(ComponentId),
171    Component {
172        id: ComponentId,
173        component_type: String,
174    },
175    ComponentList(Vec<(ComponentId, String)>),
176    Null,
177}
178
179// ---------------------------------------------------------------------------
180// Handle
181// ---------------------------------------------------------------------------
182
183#[derive(Debug)]
184pub struct MeowMeowEvaluatorHandle {
185    pub requests: Producer<EvalRequest>,
186    pub responses: Consumer<EvalResponse>,
187    join: Option<JoinHandle<()>>,
188}
189
190impl MeowMeowEvaluatorHandle {
191    pub fn shutdown_and_join(mut self) {
192        let _ = self.requests.push(EvalRequest::Shutdown);
193        if let Some(j) = self.join.take() {
194            let _ = j.join();
195        }
196    }
197}
198
199pub struct MeowMeowEvaluator;
200
201#[derive(Debug, Clone, Copy, PartialEq)]
202pub enum RuntimeClosureExecMode {
203    Full,
204    KeyframeAudioOnly { beat_context: f64 },
205    KeyframeVisualOnly,
206}
207
208impl MeowMeowEvaluator {
209    pub fn spawn(queue_capacity: usize) -> MeowMeowEvaluatorHandle {
210        let (req_prod, req_cons) = RingBuffer::<EvalRequest>::new(queue_capacity);
211        let (res_prod, res_cons) = RingBuffer::<EvalResponse>::new(queue_capacity);
212        let join = thread::spawn(move || evaluator_thread(req_cons, res_prod));
213        MeowMeowEvaluatorHandle {
214            requests: req_prod,
215            responses: res_cons,
216            join: Some(join),
217        }
218    }
219}
220
221// ---------------------------------------------------------------------------
222// Worker thread
223// ---------------------------------------------------------------------------
224
225fn evaluator_thread(requests: Consumer<EvalRequest>, responses: Producer<EvalResponse>) {
226    let mut ch = EvalChannels::new(requests, responses);
227    let mut session_world = ObjectWorld::new();
228    session_world.bind("world", Value::Identifier("__mms_world__".into()));
229    loop {
230        if ch.shutdown_requested {
231            while ch.responses.push(EvalResponse::ShutdownAck).is_err() {
232                std::thread::yield_now();
233            }
234            break;
235        }
236
237        match ch.requests.pop() {
238            Ok(EvalRequest::EvalScript {
239                source,
240                source_path,
241            }) => {
242                eval_script(&source, source_path.as_deref(), &mut ch);
243            }
244            Ok(EvalRequest::EvalSnippet { source, cwd }) => {
245                let result = eval_snippet(&source, cwd, &mut session_world, &mut ch);
246                while ch
247                    .responses
248                    .push(EvalResponse::SnippetComplete {
249                        result: result.clone(),
250                    })
251                    .is_err()
252                {
253                    std::thread::yield_now();
254                }
255            }
256            Ok(EvalRequest::EvalNavigation { source, cwd }) => {
257                let result = eval_navigation(&source, cwd, &mut session_world, &mut ch);
258                while ch
259                    .responses
260                    .push(EvalResponse::NavigationComplete {
261                        result: result.clone(),
262                    })
263                    .is_err()
264                {
265                    std::thread::yield_now();
266                }
267            }
268            Ok(EvalRequest::ParseScript { source }) => {
269                let resp = parse_only(&source)
270                    .map(|dbg| EvalResponse::ParsedOk { debug_ast: dbg })
271                    .unwrap_or_else(|msg| EvalResponse::Error { message: msg });
272                let _ = ch.responses.push(resp);
273            }
274            Ok(EvalRequest::HostCallResult { .. }) => {
275                // HostCallResult arriving outside of a spin-wait means the host
276                // sent a stale reply. Discard silently.
277            }
278            Ok(EvalRequest::Shutdown) => {
279                let _ = ch.responses.push(EvalResponse::ShutdownAck);
280                break;
281            }
282            Err(rtrb::PopError::Empty) => {
283                std::thread::yield_now();
284            }
285        }
286    }
287}
288
289/// Emit a `HostCall` and spin-wait for the matching `HostCallResult`.
290///
291/// Blocks the evaluator thread until the host pushes `HostCallResult { id, value }`.
292/// Any `HostCallResult` with a non-matching id is discarded (stale reply).
293/// Other request kinds (e.g. Shutdown) are processed normally while waiting.
294///
295/// Returns `None` if the host sent `HostValue::Null` or if a Shutdown arrived
296/// before the reply.
297fn host_call(
298    id: u32,
299    kind: HostCallKind,
300    requests: &mut Consumer<EvalRequest>,
301    responses: &mut Producer<EvalResponse>,
302    shutdown_requested: &mut bool,
303) -> Option<HostValue> {
304    while responses
305        .push(EvalResponse::HostCall {
306            id,
307            kind: kind.clone(),
308        })
309        .is_err()
310    {
311        std::thread::yield_now();
312    }
313    loop {
314        match requests.pop() {
315            Ok(EvalRequest::HostCallResult {
316                id: reply_id,
317                value,
318            }) if reply_id == id => {
319                return match value {
320                    HostValue::Null => None,
321                    v => Some(v),
322                };
323            }
324            Ok(EvalRequest::HostCallResult { .. }) => {
325                // Stale reply for a different id — discard.
326            }
327            Ok(EvalRequest::Shutdown) => {
328                *shutdown_requested = true;
329            }
330            Ok(other) => {
331                // Other requests (unlikely mid-eval) — re-queue by yielding;
332                // in practice only HostCallResult and Shutdown arrive here.
333                let _ = other; // consumed, cannot re-push to Consumer
334            }
335            Err(rtrb::PopError::Empty) => {
336                std::thread::yield_now();
337            }
338        }
339    }
340}
341
342// ---------------------------------------------------------------------------
343// Script evaluation
344// ---------------------------------------------------------------------------
345
346/// Shared mutable context threaded through evaluation.
347///
348/// Carries everything that eval functions need beyond the immutable `env`.
349/// Adding future context (module cache, call-depth limit, …) is a one-field
350/// change here rather than a signature change across every eval function.
351struct EvalContext<'a> {
352    /// Accumulates `SpawnComponentTree` intents produced by top-level CE emissions.
353    emits: &'a mut Vec<IntentValue>,
354    /// Filesystem path of the file being evaluated, used to resolve relative
355    /// `import "…"` paths. `None` inside function call bodies and when
356    /// source was provided as a raw string without a path.
357    source_path: Option<&'a str>,
358    /// Channel back to the host for HostCall round-trips (reply channel).
359    /// `None` when running in fire-and-forget mode (no world access available).
360    channels: Option<&'a mut EvalChannels>,
361    /// When evaluating inside a CE body block, captures children, builder calls,
362    /// positionals, and named assignments instead of emitting to the top level.
363    ce_builder: Option<&'a mut CeBuilder>,
364    /// Scripting-side runtime storage: variable scope chain (frames) + heap.
365    /// All variable bindings, lookups, and reassignments go through this.
366    object_world: &'a mut ObjectWorld,
367    /// Inline live-world host access used when MMS runs directly inside a
368    /// registered engine handler rather than through evaluator channels.
369    host_world: Option<*mut World>,
370    /// Live component subtree that owns the currently executing imperative block.
371    exec_scope: Option<ComponentId>,
372    /// Execution policy for deferred runtime closures such as `Keyframe` callbacks.
373    runtime_closure_mode: RuntimeClosureExecMode,
374    /// Prompt-time navigation context. It propagates through functions called
375    /// by the prompt, but is absent from file and later handler evaluation.
376    implicit_cwd: Option<Value>,
377}
378
379thread_local! {
380    static LIVE_SIGNAL_EMITTER: RefCell<Option<*mut dyn SignalEmitter>> = RefCell::new(None);
381    static NAVIGATION_EVAL_ACTIVE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
382}
383
384fn with_live_signal_emitter<R>(emit: Option<*mut dyn SignalEmitter>, f: impl FnOnce() -> R) -> R {
385    LIVE_SIGNAL_EMITTER.with(|slot| {
386        let prev = slot.replace(emit);
387        let result = f();
388        let _ = slot.replace(prev);
389        result
390    })
391}
392
393fn with_navigation_eval<R>(f: impl FnOnce() -> R) -> R {
394    NAVIGATION_EVAL_ACTIVE.with(|active| {
395        let previous = active.replace(true);
396        let result = f();
397        active.set(previous);
398        result
399    })
400}
401
402fn push_eval_intent(ctx: &mut EvalContext<'_>, mut intent: IntentValue) {
403    match ctx.runtime_closure_mode {
404        RuntimeClosureExecMode::Full => {}
405        RuntimeClosureExecMode::KeyframeAudioOnly { beat_context } => match &mut intent {
406            IntentValue::AudioSchedulePlay {
407                beat_context: signal_beat_context,
408                ..
409            }
410            | IntentValue::OscillatorScheduleSetPitch {
411                beat_context: signal_beat_context,
412                ..
413            } => {
414                *signal_beat_context = Some(beat_context);
415            }
416            _ => return,
417        },
418        RuntimeClosureExecMode::KeyframeVisualOnly => match intent {
419            IntentValue::AudioSchedulePlay { .. }
420            | IntentValue::OscillatorScheduleSetPitch { .. } => {
421                return;
422            }
423            _ => {}
424        },
425    }
426
427    ctx.emits.push(intent);
428}
429
430/// Accumulator used while evaluating a component expression body block.
431/// Collects the results that will be materialized into a `MaterializedCE`.
432struct CeBuilder {
433    /// Whether `name = expr` in this CE body should be captured as a named prop.
434    component_property_assignment_only: bool,
435    /// Remaining chained ctor calls + body builder calls (calls to names not in env).
436    calls: Vec<(String, Vec<Value>)>,
437    /// Named property assignments (`name = expr` in body where name was pre-injected).
438    named: Vec<(String, Value)>,
439    /// String-type positional content (e.g. `"hello " + name` in Text body).
440    positionals: Vec<Value>,
441    /// Child entries in source order — either fresh CEs to spawn or
442    /// pre-Registered ComponentIds to splice in.
443    children: Vec<CeChild>,
444}
445
446/// Live request/response channels plus a monotonic correlation-id counter.
447/// Owned by the evaluator thread; passed as `&mut` into eval functions.
448pub struct EvalChannels {
449    pub requests: Consumer<EvalRequest>,
450    pub responses: Producer<EvalResponse>,
451    next_id: u32,
452    shutdown_requested: bool,
453}
454
455impl EvalChannels {
456    pub fn new(requests: Consumer<EvalRequest>, responses: Producer<EvalResponse>) -> Self {
457        Self {
458            requests,
459            responses,
460            next_id: 0,
461            shutdown_requested: false,
462        }
463    }
464
465    /// Emit a `HostCall` and spin-wait for the matching `HostCallResult`.
466    pub fn call(&mut self, kind: HostCallKind) -> Option<HostValue> {
467        let id = self.next_id;
468        self.next_id = self.next_id.wrapping_add(1);
469        host_call(
470            id,
471            kind,
472            &mut self.requests,
473            &mut self.responses,
474            &mut self.shutdown_requested,
475        )
476    }
477}
478
479/// Evaluate a script: parse → AstTransforms → walk statements.
480/// `ch` is the owned channel pair for the evaluator thread; the borrow is
481/// released before intents are flushed so `ch.responses` is free to use again.
482fn eval_script(source: &str, source_path: Option<&str>, ch: &mut EvalChannels) {
483    let mut stmts = match parse_source(source) {
484        Ok(s) => s,
485        Err(msg) => {
486            let _ = ch.responses.push(EvalResponse::Error { message: msg });
487            return;
488        }
489    };
490
491    EmitLiftTransform::apply(&mut stmts);
492    QueryDesugarTransform::apply(&mut stmts);
493
494    let mut emits: Vec<IntentValue> = Vec::new();
495    let mut world = ObjectWorld::new();
496
497    // Borrow `ch` into the context for the duration of eval, then release it.
498    let eval_result = {
499        let mut ctx = EvalContext {
500            emits: &mut emits,
501            source_path,
502            channels: Some(ch),
503            ce_builder: None,
504            object_world: &mut world,
505            host_world: None,
506            exec_scope: None,
507            runtime_closure_mode: RuntimeClosureExecMode::Full,
508            implicit_cwd: None,
509        };
510        eval_block_stmts(&stmts, &mut ctx)
511    }; // ctx (and its borrow of ch) dropped here
512
513    match eval_result {
514        Ok(_) => {}
515        Err(msg) => {
516            let _ = ch.responses.push(EvalResponse::Error { message: msg });
517            return;
518        }
519    }
520
521    for intent in emits {
522        while ch
523            .responses
524            .push(EvalResponse::Intent(intent.clone()))
525            .is_err()
526        {
527            std::thread::yield_now();
528        }
529    }
530}
531
532/// Interactive evaluation differs from file evaluation in two deliberate ways:
533/// its object world survives requests, and a top-level expression is a value to
534/// echo instead of an implicit component attachment.
535fn eval_snippet(
536    source: &str,
537    cwd: Option<Value>,
538    object_world: &mut ObjectWorld,
539    ch: &mut EvalChannels,
540) -> Result<Option<Value>, String> {
541    let result = eval_snippet_inner(source, cwd, object_world, ch);
542    if object_world.take_reset_requested() {
543        *object_world = ObjectWorld::new();
544        object_world.bind("world", Value::Identifier("__mms_world__".into()));
545        while ch.responses.push(EvalResponse::ReplReset).is_err() {
546            std::thread::yield_now();
547        }
548    }
549    result
550}
551
552fn eval_snippet_inner(
553    source: &str,
554    cwd: Option<Value>,
555    object_world: &mut ObjectWorld,
556    ch: &mut EvalChannels,
557) -> Result<Option<Value>, String> {
558    let mut stmts = parse_source(source)?;
559    QueryDesugarTransform::apply(&mut stmts);
560    let mut emits = Vec::new();
561    let mut last = None;
562    let count = stmts.len();
563    for (index, stmt) in stmts.iter().enumerate() {
564        let mut ctx = EvalContext {
565            emits: &mut emits,
566            source_path: None,
567            channels: Some(ch),
568            ce_builder: None,
569            object_world,
570            host_world: None,
571            exec_scope: None,
572            runtime_closure_mode: RuntimeClosureExecMode::Full,
573            implicit_cwd: cwd.clone(),
574        };
575        if let Statement::Expression(expr) = stmt {
576            let value = if let Expression::Call(call) = expr
577                && matches!(call.callee.as_ref(), Expression::Identifier(id) if id.0 == "emit")
578            {
579                if let Some(arg) = call.args.first() {
580                    let value = eval_expr(arg, &mut ctx)?;
581                    push_component_emit(value, &mut ctx);
582                }
583                Value::Null
584            } else {
585                eval_expr(expr, &mut ctx)?
586            };
587            let value = match value {
588                Value::ComponentExpr(ce) => {
589                    let component_type = ce.component_type.clone();
590                    match ctx
591                        .channels
592                        .as_mut()
593                        .and_then(|c| c.call(HostCallKind::Spawn(*ce)))
594                    {
595                        Some(HostValue::ComponentId(id)) => {
596                            Value::ComponentObject { id, component_type }
597                        }
598                        _ => return Err("unable to spawn component expression".into()),
599                    }
600                }
601                other => other,
602            };
603            if index + 1 == count {
604                last = Some(value);
605            }
606        } else {
607            match eval_stmt(stmt, &mut ctx)? {
608                StmtEffect::None | StmtEffect::Exported(_) => {}
609                StmtEffect::Return(_) => return Err("return cannot escape a REPL snippet".into()),
610                StmtEffect::Break | StmtEffect::Continue => {
611                    return Err("break/continue cannot escape a REPL snippet".into());
612                }
613            }
614        }
615        flush_live_statement_emits(&mut ctx);
616    }
617    Ok(last)
618}
619
620fn eval_navigation(
621    source: &str,
622    cwd: Option<Value>,
623    object_world: &mut ObjectWorld,
624    ch: &mut EvalChannels,
625) -> Result<Value, String> {
626    let mut stmts = parse_source(source)?;
627    QueryDesugarTransform::apply(&mut stmts);
628    let [Statement::Expression(expr)] = stmts.as_slice() else {
629        return Err("cd: expected one MMS expression".into());
630    };
631    let mut emits = Vec::new();
632    let mut ctx = EvalContext {
633        emits: &mut emits,
634        source_path: None,
635        channels: Some(ch),
636        ce_builder: None,
637        object_world,
638        host_world: None,
639        exec_scope: None,
640        runtime_closure_mode: RuntimeClosureExecMode::Full,
641        implicit_cwd: cwd,
642    };
643    // Intentionally do not call maybe_register_live_component_value,
644    // push_component_emit, or flush_live_statement_emits here.
645    with_navigation_eval(|| eval_expr(expr, &mut ctx))
646}
647
648fn flush_live_statement_emits(ctx: &mut EvalContext<'_>) {
649    if ctx.channels.is_none() || ctx.ce_builder.is_some() || ctx.emits.is_empty() {
650        return;
651    }
652
653    let mut deferred: Vec<IntentValue> = Vec::new();
654    for intent in std::mem::take(ctx.emits) {
655        match intent {
656            IntentValue::SpawnComponentTree { root, parent: None } => {
657                if let Some(ch) = ctx.channels.as_mut() {
658                    let spawn_result = ch.call(HostCallKind::Spawn((*root).clone()));
659                    if !matches!(spawn_result, Some(HostValue::ComponentId(_))) {
660                        deferred.push(IntentValue::SpawnComponentTree { root, parent: None });
661                    }
662                } else {
663                    deferred.push(IntentValue::SpawnComponentTree { root, parent: None });
664                }
665            }
666            other => deferred.push(other),
667        }
668    }
669    *ctx.emits = deferred;
670}
671
672// ---------------------------------------------------------------------------
673// Statement evaluation
674// ---------------------------------------------------------------------------
675
676/// Effect produced by evaluating a statement (excluding emits, which go to the emits vec).
677///
678/// Bindings, reassignments, and import-bindings are applied directly to
679/// `ctx.object_world` inside `eval_stmt` — they do not flow through this enum.
680/// Only control-flow effects and the `Exported` marker bubble out.
681enum StmtEffect {
682    None,
683    /// `export let X = ...` — the binding has already been written to
684    /// `object_world`; this signals that the module body should also register
685    /// the name in its named-exports map.
686    Exported(String),
687    Return(Value),
688    Break,
689    Continue,
690}
691
692/// Evaluate a block of statements against the current top frame of
693/// `ctx.object_world`. Returns the first control-flow effect encountered
694/// (Return/Break/Continue), `Exported(name)` (only consumed by module body),
695/// or `None`.
696///
697/// Frame management is the *caller*'s responsibility: this function does not
698/// push or pop. Function calls, loops, if-bodies, etc. wrap the call.
699fn eval_block_stmts(stmts: &[Statement], ctx: &mut EvalContext<'_>) -> Result<StmtEffect, String> {
700    for stmt in stmts {
701        let effect = eval_stmt(stmt, ctx)?;
702        flush_live_statement_emits(ctx);
703        match effect {
704            StmtEffect::None => {}
705            // Exported is only meaningful at module-body level; ignored elsewhere.
706            StmtEffect::Exported(_) => {}
707            effect => return Ok(effect),
708        }
709    }
710    Ok(StmtEffect::None)
711}
712
713fn eval_stmt(stmt: &Statement, ctx: &mut EvalContext<'_>) -> Result<StmtEffect, String> {
714    match stmt {
715        Statement::Assignment(a) => {
716            if a.name.0 == "cwd" && ctx.implicit_cwd.is_some() {
717                return Err("cwd is reserved by the interactive REPL".into());
718            }
719            let val = eval_expr(&a.value, ctx)?;
720            let val = maybe_register_live_component_value(val, ctx);
721            ctx.object_world.bind(a.name.0.clone(), val);
722            if a.exported {
723                Ok(StmtEffect::Exported(a.name.0.clone()))
724            } else {
725                Ok(StmtEffect::None)
726            }
727        }
728        Statement::Reassign { target, value } => {
729            if matches!(target, Expression::Identifier(name) if name.0 == "cwd")
730                && ctx.implicit_cwd.is_some()
731            {
732                return Err("cwd is reserved by the interactive REPL".into());
733            }
734            let val = eval_expr(value, ctx)?;
735            if let Some(builder) = ctx.ce_builder.as_mut() {
736                if let Expression::Identifier(name) = target {
737                    if builder.component_property_assignment_only
738                        || is_universal_component_named_prop(&name.0)
739                    {
740                        // Property-bag CE bodies capture assignments as named props.
741                        // This must win even when `foo` also exists as a lexical binding,
742                        // so authored payloads like `row_name = row_name` survive.
743                        builder.named.push((name.0.clone(), val));
744                        return Ok(StmtEffect::None);
745                    }
746                }
747            }
748            let val = maybe_register_live_component_value(val, ctx);
749            assign_retarget(target, val, ctx)?;
750            Ok(StmtEffect::None)
751        }
752        Statement::Expression(expr) => {
753            eval_expr_stmt(expr, ctx)?;
754            Ok(StmtEffect::None)
755        }
756        Statement::Return(r) => {
757            let val = match &r.value {
758                Some(expr) => eval_expr(expr, ctx)?,
759                None => Value::Null,
760            };
761            Ok(StmtEffect::Return(val))
762        }
763        Statement::If(if_stmt) => eval_if(if_stmt, ctx),
764        Statement::Block(block) => {
765            ctx.object_world.push_frame(FrameKind::Block);
766            let result = eval_block_stmts(&block.statements, ctx);
767            ctx.object_world.pop_frame();
768            result
769        }
770        Statement::Break => Ok(StmtEffect::Break),
771        Statement::Continue => Ok(StmtEffect::Continue),
772        Statement::ForIn {
773            binding,
774            iterable,
775            body,
776        } => {
777            let items = match eval_expr(iterable, ctx)? {
778                Value::Array(a) => a,
779                Value::Map(map) => map
780                    .into_iter()
781                    .map(|(key, value)| {
782                        Value::Map(HashMap::from([
783                            ("key".to_string(), Value::String(key)),
784                            ("value".to_string(), value),
785                        ]))
786                    })
787                    .collect(),
788                Value::Object(id) => {
789                    let Some(items) = id.with_map(|map| {
790                        map.iter()
791                            .map(|(key, value)| {
792                                Value::Map(HashMap::from([
793                                    ("key".to_string(), Value::String(key.clone())),
794                                    ("value".to_string(), value.clone()),
795                                ]))
796                            })
797                            .collect::<Vec<_>>()
798                    }) else {
799                        return Err("for/in: invalid object".into());
800                    };
801                    items
802                }
803                other => return Err(format!("for/in: expected array, got {:?}", other)),
804            };
805            ctx.object_world.push_frame(FrameKind::Block);
806            let result: Result<StmtEffect, String> = (|| {
807                'for_loop: for item in items {
808                    ctx.object_world.bind(binding.0.clone(), item);
809                    for stmt in &body.statements {
810                        match eval_stmt(stmt, ctx)? {
811                            StmtEffect::None | StmtEffect::Exported(_) => {}
812                            StmtEffect::Return(val) => return Ok(StmtEffect::Return(val)),
813                            StmtEffect::Break => return Ok(StmtEffect::None),
814                            StmtEffect::Continue => continue 'for_loop,
815                        }
816                    }
817                }
818                Ok(StmtEffect::None)
819            })();
820            ctx.object_world.pop_frame();
821            result
822        }
823        Statement::While { condition, body } => {
824            ctx.object_world.push_frame(FrameKind::Block);
825            let result: Result<StmtEffect, String> = (|| {
826                'while_loop: loop {
827                    let cond = eval_expr(condition, ctx)?;
828                    if !is_truthy(&cond) {
829                        break;
830                    }
831                    for stmt in &body.statements {
832                        match eval_stmt(stmt, ctx)? {
833                            StmtEffect::None | StmtEffect::Exported(_) => {}
834                            StmtEffect::Return(val) => return Ok(StmtEffect::Return(val)),
835                            StmtEffect::Break => break 'while_loop,
836                            StmtEffect::Continue => continue 'while_loop,
837                        }
838                    }
839                }
840                Ok(StmtEffect::None)
841            })();
842            ctx.object_world.pop_frame();
843            result
844        }
845        Statement::Import { items, path } => {
846            let resolved = resolve_import_path(path, ctx.source_path);
847            let content = std::fs::read_to_string(&resolved)
848                .map_err(|e| format!("import error: cannot read '{}': {}", path, e))?;
849            let module_val = eval_module_source(&content, Some(&resolved))?;
850            let (named, sequence) = match module_val {
851                Value::Module {
852                    named, sequence, ..
853                } => (named, sequence),
854                _ => return Err("import: internal error".to_string()),
855            };
856            for item in items {
857                match item {
858                    ImportItem::Named(id) => {
859                        let val = named.get(&id.0).cloned().ok_or_else(|| {
860                            format!("import: '{}' is not exported from '{}'", id.0, path)
861                        })?;
862                        ctx.object_world.bind(id.0.clone(), val);
863                    }
864                    ImportItem::NamedAlias { name, alias } => {
865                        let val = named.get(&name.0).cloned().ok_or_else(|| {
866                            format!("import: '{}' is not exported from '{}'", name.0, path)
867                        })?;
868                        ctx.object_world.bind(alias.0.clone(), val);
869                    }
870                    ImportItem::PositionalAlias { index, alias } => {
871                        let ce = sequence.get(*index).ok_or_else(|| {
872                            format!("import: index {} out of range in '{}'", index, path)
873                        })?;
874                        ctx.object_world
875                            .bind(alias.0.clone(), Value::ComponentExpr(Box::new(ce.clone())));
876                    }
877                }
878            }
879            Ok(StmtEffect::None)
880        }
881    }
882}
883
884fn maybe_register_live_component_value(val: Value, ctx: &mut EvalContext<'_>) -> Value {
885    if NAVIGATION_EVAL_ACTIVE.with(|active| active.get()) {
886        return val;
887    }
888    // In live mode, binding or reassigning a CE should produce a live handle
889    // rather than leave a dead ComponentExpr in scope.
890    match val {
891        Value::ComponentExpr(ce) => {
892            let component_type = ce.component_type.clone();
893            if let Some(ch) = ctx.channels.as_mut() {
894                return match ch.call(HostCallKind::Register(*ce.clone())) {
895                    Some(HostValue::ComponentId(id)) => {
896                        Value::ComponentObject { id, component_type }
897                    }
898                    _ => Value::ComponentExpr(ce),
899                };
900            }
901            if let Some(world) = ctx.host_world {
902                let registered = LIVE_SIGNAL_EMITTER.with(|slot| {
903                    let Some(host_emit) = *slot.borrow() else {
904                        return None;
905                    };
906                    unsafe {
907                        crate::scripting::component_registry::spawn_tree_uninitialized(
908                            &ce,
909                            &mut *world,
910                            &mut *host_emit,
911                        )
912                        .ok()
913                    }
914                });
915                if let Some(id) = registered {
916                    return Value::ComponentObject { id, component_type };
917                }
918            }
919            Value::ComponentExpr(ce)
920        }
921        val => val,
922    }
923}
924
925fn eval_expr_stmt(expr: &Expression, ctx: &mut EvalContext<'_>) -> Result<(), String> {
926    // Special case: emit(expr) — produced by EmitLiftTransform or written explicitly.
927    if let Expression::Call(call) = expr {
928        if matches!(call.callee.as_ref(), Expression::Identifier(id) if id.0 == "emit") {
929            if let Some(arg) = call.args.first() {
930                let val = eval_expr(arg, ctx)?;
931                push_component_emit(val, ctx);
932            }
933            return Ok(());
934        }
935
936        // Builder call interception: inside a CE body, plain calls to names not in env
937        // and not built-ins are captured as builder calls rather than erroring.
938        if let Expression::Identifier(callee_id) = call.callee.as_ref() {
939            if ctx.ce_builder.is_some()
940                && !ctx.object_world.has(&callee_id.0)
941                && !is_builtin_fn(&callee_id.0)
942            {
943                let args: Vec<Value> = call
944                    .args
945                    .iter()
946                    .map(|a| eval_expr(a, ctx))
947                    .collect::<Result<_, _>>()?;
948                ctx.ce_builder
949                    .as_mut()
950                    .unwrap()
951                    .calls
952                    .push((callee_id.0.clone(), args));
953                return Ok(());
954            }
955        }
956    }
957
958    // General case: evaluate and route result.
959    let val = eval_expr(expr, ctx)?;
960    if ctx.ce_builder.is_some() {
961        match val {
962            // String positionals captured in CE body.
963            Value::String(_) => ctx.ce_builder.as_mut().unwrap().positionals.push(val),
964            // Fresh CE children captured in CE body.
965            Value::ComponentExpr(ce) => ctx
966                .ce_builder
967                .as_mut()
968                .unwrap()
969                .children
970                .push(CeChild::Spawn(*ce)),
971            // Reference to a previously Registered live component — splice
972            // the detached subtree as a child of the parent CE rather than
973            // discarding the value or re-spawning it.
974            Value::ComponentObject { id, .. } => {
975                ctx.ce_builder
976                    .as_mut()
977                    .unwrap()
978                    .children
979                    .push(CeChild::Attach(id));
980            }
981            // Other values inside a CE body are discarded (no-op expression statements).
982            _ => {}
983        }
984    } else {
985        push_component_emit(val, ctx);
986    }
987    Ok(())
988}
989
990fn push_component_emit(val: Value, ctx: &mut EvalContext<'_>) {
991    if NAVIGATION_EVAL_ACTIVE.with(|active| active.get()) {
992        return;
993    }
994    match val {
995        Value::ComponentExpr(ce) => {
996            push_eval_intent(
997                ctx,
998                IntentValue::SpawnComponentTree {
999                    root: ce,
1000                    parent: None,
1001                },
1002            );
1003        }
1004        // Bare top-level reference to a previously Registered ComponentObject —
1005        // attach as a world root and run the deferred init walk.
1006        Value::ComponentObject { id, .. } => {
1007            if let Some(ch) = ctx.channels.as_mut() {
1008                ch.call(HostCallKind::Attach {
1009                    parent: None,
1010                    child: id,
1011                });
1012            }
1013        }
1014        _ => {}
1015    }
1016}
1017
1018fn value_to_component_ref_live(world: &World, value: &Value) -> Result<ComponentRef, String> {
1019    match value {
1020        Value::ComponentObject { id, .. } => {
1021            let guid = world
1022                .get_component_record(*id)
1023                .map(|record| record.guid)
1024                .ok_or_else(|| format!("component handle {id:?} not found in world"))?;
1025            Ok(ComponentRef::Guid(guid))
1026        }
1027        Value::String(s) | Value::Identifier(s) => {
1028            if let Some(hex) = s.strip_prefix("@uuid:") {
1029                let uuid = uuid::Uuid::parse_str(hex)
1030                    .map_err(|e| format!("invalid uuid in '@uuid:{hex}': {e}"))?;
1031                Ok(ComponentRef::Guid(uuid))
1032            } else {
1033                Ok(ComponentRef::Query(s.clone()))
1034            }
1035        }
1036        other => Err(format!(
1037            "expected component handle or selector string, got {other:?}"
1038        )),
1039    }
1040}
1041
1042fn resolve_live_component_ref_global(world: &World, src: &ComponentRef) -> Option<ComponentId> {
1043    match src {
1044        ComponentRef::Guid(uuid) => world.component_id_by_guid(*uuid),
1045        ComponentRef::Query(selector) => world
1046            .world_roots()
1047            .into_iter()
1048            .find_map(|root| world.find_component(root, selector)),
1049    }
1050}
1051
1052fn assign_retarget(
1053    target: &Expression,
1054    value: Value,
1055    ctx: &mut EvalContext<'_>,
1056) -> Result<(), String> {
1057    match target {
1058        Expression::Identifier(name) => {
1059            if ctx.object_world.has(&name.0) {
1060                return ctx.object_world.reassign(&name.0, value);
1061            }
1062            if let Some(cwd) = &ctx.implicit_cwd {
1063                let updated = match cwd {
1064                    Value::Object(id) => id
1065                        .with_map_mut(|map| {
1066                            map.get_mut(&name.0).map(|field| *field = value.clone())
1067                        })
1068                        .flatten()
1069                        .is_some(),
1070                    _ => false,
1071                };
1072                if updated {
1073                    return Ok(());
1074                }
1075            }
1076            ctx.object_world.reassign(&name.0, value)
1077        }
1078        Expression::BinaryOp {
1079            op: BinOpKind::Dot, ..
1080        } => {
1081            let mut path = Vec::new();
1082            let root_name = flatten_assign_path(target, &mut path)?;
1083            let mut root_value = ctx
1084                .object_world
1085                .lookup(&root_name)
1086                .cloned()
1087                .ok_or_else(|| format!("reassignment: '{}' is not defined", root_name))?;
1088            assign_into_value(&mut root_value, &path, value, ctx)?;
1089            ctx.object_world.reassign(&root_name, root_value)
1090        }
1091        _ => Err("invalid reassignment target".into()),
1092    }
1093}
1094
1095fn flatten_assign_path(target: &Expression, out: &mut Vec<String>) -> Result<String, String> {
1096    match target {
1097        Expression::Identifier(name) => Ok(name.0.clone()),
1098        Expression::BinaryOp {
1099            op: BinOpKind::Dot,
1100            lhs,
1101            rhs,
1102        } => {
1103            let root = flatten_assign_path(lhs, out)?;
1104            let Expression::Identifier(field) = rhs.as_ref() else {
1105                return Err("invalid reassignment target".into());
1106            };
1107            out.push(field.0.clone());
1108            Ok(root)
1109        }
1110        _ => Err("invalid reassignment target".into()),
1111    }
1112}
1113
1114fn assign_into_value(
1115    current: &mut Value,
1116    path: &[String],
1117    value: Value,
1118    ctx: &mut EvalContext<'_>,
1119) -> Result<(), String> {
1120    let Some((field, rest)) = path.split_first() else {
1121        *current = value;
1122        return Ok(());
1123    };
1124
1125    match current {
1126        Value::Map(map) => {
1127            if rest.is_empty() {
1128                map.insert(field.clone(), value);
1129                return Ok(());
1130            }
1131            let next = map
1132                .get_mut(field)
1133                .ok_or_else(|| format!("field assignment: '{}' not found", field))?;
1134            assign_into_value(next, rest, value, ctx)
1135        }
1136        Value::Object(id) => {
1137            if rest.is_empty() {
1138                let Some(()) = id.with_map_mut(|map| {
1139                    map.insert(field.clone(), value);
1140                }) else {
1141                    return Err("field assignment: invalid object".into());
1142                };
1143                return Ok(());
1144            }
1145
1146            let Some(mut next) = id.with_map(|map| map.get(field).cloned()).flatten() else {
1147                return Err(format!("field assignment: '{}' not found", field));
1148            };
1149            assign_into_value(&mut next, rest, value, ctx)?;
1150            let Some(()) = id.with_map_mut(|map| {
1151                map.insert(field.clone(), next);
1152            }) else {
1153                return Err("field assignment: invalid object".into());
1154            };
1155            Ok(())
1156        }
1157        other => Err(format!(
1158            "field assignment: cannot assign through '{}': {:?}",
1159            field, other
1160        )),
1161    }
1162}
1163
1164fn is_builtin_fn(name: &str) -> bool {
1165    matches!(
1166        name,
1167        "print"
1168            | "assert"
1169            | "range"
1170            | "emit"
1171            | "on"
1172            | "query"
1173            | "query_all"
1174            | "emit_data"
1175            | "tree"
1176            | "dump"
1177            | "help"
1178            | "clear"
1179            | "reset"
1180    )
1181}
1182
1183fn eval_if(if_stmt: &IfStatement, ctx: &mut EvalContext<'_>) -> Result<StmtEffect, String> {
1184    let cond = eval_expr(&if_stmt.condition, ctx)?;
1185    let branch = if is_truthy(&cond) {
1186        Some(&if_stmt.then_branch)
1187    } else {
1188        None
1189    };
1190    if let Some(block) = branch {
1191        ctx.object_world.push_frame(FrameKind::Block);
1192        let result = eval_block_stmts(&block.statements, ctx);
1193        ctx.object_world.pop_frame();
1194        return result;
1195    }
1196
1197    match &if_stmt.else_branch {
1198        Some(ElseBranch::Block(block)) => {
1199            ctx.object_world.push_frame(FrameKind::Block);
1200            let result = eval_block_stmts(&block.statements, ctx);
1201            ctx.object_world.pop_frame();
1202            result
1203        }
1204        Some(ElseBranch::If(next_if)) => eval_if(next_if, ctx),
1205        None => Ok(StmtEffect::None),
1206    }
1207}
1208
1209// ---------------------------------------------------------------------------
1210// Expression evaluation
1211// ---------------------------------------------------------------------------
1212
1213/// Evaluate a `ComponentExpression` AST node into a `MaterializedCE`.
1214///
1215/// All constructor args are evaluated against the current env. The body block
1216/// is evaluated as a full MMS block statement in a CE builder context:
1217/// - CE emissions → captured as children
1218/// - Calls to names not in env → captured as builder calls
1219/// - `Value::String` expression statements → captured as positionals
1220/// - Named assignments (`name = expr`) → read from env after block if pre-injected
1221fn eval_ce(ce: &ComponentExpression, ctx: &mut EvalContext<'_>) -> Result<Value, String> {
1222    let component_property_assignment_only =
1223        component_expr_uses_property_assignment_only(&ce.component_type.0);
1224    let is_keyframe = matches!(ce.component_type.0.as_str(), "KF" | "Keyframe");
1225    // Evaluate all constructor calls.
1226    let mut ctor_method: Option<String> = None;
1227    let mut ctor_args: Vec<Value> = vec![];
1228    let mut extra_ctor_calls: Vec<(String, Vec<Value>)> = vec![];
1229    for (i, ctor) in ce.constructors.iter().enumerate() {
1230        let args: Vec<Value> = ctor
1231            .args
1232            .iter()
1233            .map(|a| eval_expr(a, ctx))
1234            .collect::<Result<_, _>>()?;
1235        if i == 0 {
1236            ctor_method = Some(ctor.method.0.clone());
1237            ctor_args = args;
1238        } else {
1239            extra_ctor_calls.push((ctor.method.0.clone(), args));
1240        }
1241    }
1242
1243    if is_keyframe {
1244        let mce = MaterializedCE {
1245            component_type: ce.component_type.0.clone(),
1246            component_property_assignment_only,
1247            ctor_method,
1248            ctor_args,
1249            calls: extra_ctor_calls,
1250            named: vec![],
1251            positionals: vec![],
1252            deferred_block: Some(RuntimeClosure {
1253                body: ce.body.clone(),
1254                captured_env: Arc::new(ctx.object_world.snapshot_visible()),
1255                heap: ctx.object_world.heap().clone(),
1256                analysis: Some(BlockEffectAnalyzer::analyze_keyframe_block(&ce.body)),
1257            }),
1258            children: vec![],
1259        };
1260        return Ok(Value::ComponentExpr(Box::new(mce)));
1261    }
1262
1263    // Evaluate the body block with a CE builder context.
1264    let mut builder = CeBuilder {
1265        component_property_assignment_only,
1266        calls: extra_ctor_calls,
1267        named: vec![],
1268        positionals: vec![],
1269        children: vec![],
1270    };
1271
1272    // Evaluate the body block, routing CE emissions and builder calls to `builder`.
1273    // CE bodies are plain Block frames — fully transparent for read & write.
1274    ctx.object_world.push_frame(FrameKind::Block);
1275    let body_result = {
1276        let mut body_ctx = EvalContext {
1277            emits: ctx.emits,
1278            source_path: ctx.source_path,
1279            channels: ctx.channels.as_mut().map(|c| &mut **c),
1280            ce_builder: Some(&mut builder),
1281            object_world: ctx.object_world,
1282            host_world: ctx.host_world,
1283            exec_scope: ctx.exec_scope,
1284            runtime_closure_mode: ctx.runtime_closure_mode,
1285            implicit_cwd: ctx.implicit_cwd.clone(),
1286        };
1287        eval_block_stmts(&ce.body.statements, &mut body_ctx)
1288    };
1289    ctx.object_world.pop_frame();
1290    body_result?;
1291
1292    let mce = MaterializedCE {
1293        component_type: ce.component_type.0.clone(),
1294        component_property_assignment_only,
1295        ctor_method,
1296        ctor_args,
1297        calls: builder.calls,
1298        named: builder.named,
1299        positionals: builder.positionals,
1300        deferred_block: None,
1301        children: builder.children,
1302    };
1303    Ok(Value::ComponentExpr(Box::new(mce)))
1304}
1305
1306fn eval_expr(expr: &Expression, ctx: &mut EvalContext<'_>) -> Result<Value, String> {
1307    match expr {
1308        Expression::Null => Ok(Value::Null),
1309        Expression::Bool(b) => Ok(Value::Bool(*b)),
1310        Expression::Number(n) => Ok(Value::Number(*n)),
1311        Expression::Dimension(n, unit) => Ok(Value::Dimension {
1312            value: *n,
1313            unit: *unit,
1314        }),
1315        Expression::String(s) => Ok(Value::String(s.clone())),
1316        Expression::Array(items) => {
1317            let vals = items
1318                .iter()
1319                .map(|e| eval_expr(e, ctx))
1320                .collect::<Result<Vec<_>, _>>()?;
1321            Ok(Value::Array(vals))
1322        }
1323        Expression::Table(fields) => {
1324            let mut map = HashMap::with_capacity(fields.len());
1325            for field in fields {
1326                map.insert(field.name.0.clone(), eval_expr(&field.value, ctx)?);
1327            }
1328            Ok(Value::Object(
1329                ctx.object_world.alloc_object(Object::Map(map)),
1330            ))
1331        }
1332        Expression::Index { base, index } => {
1333            let base = eval_expr(base, ctx)?;
1334            let index = eval_expr(index, ctx)?;
1335            let Value::Array(items) = base else {
1336                return Err(format!("index: expected array, got {:?}", base));
1337            };
1338            let Value::Number(n) = index else {
1339                return Err(format!("index: expected numeric index, got {:?}", index));
1340            };
1341            if n.fract() != 0.0 || n < 0.0 {
1342                return Err(format!("index: expected non-negative integer, got {n}"));
1343            }
1344            items
1345                .get(n as usize)
1346                .cloned()
1347                .ok_or_else(|| format!("index: {n} out of bounds for array of {}", items.len()))
1348        }
1349        Expression::Identifier(id) => {
1350            if id.0 == "cwd" {
1351                return Ok(ctx
1352                    .implicit_cwd
1353                    .clone()
1354                    .unwrap_or_else(|| Value::Identifier("__mms_world__".into())));
1355            }
1356            // Look up in scope chain; fall back to bare identifier value (for enum-like flags).
1357            match ctx.object_world.lookup(&id.0) {
1358                Some(val) => Ok(val.clone()),
1359                None => {
1360                    if let Some(value) = ctx.implicit_cwd.as_ref().and_then(|cwd| match cwd {
1361                        Value::Object(object) => {
1362                            object.with_map(|map| map.get(&id.0).cloned()).flatten()
1363                        }
1364                        Value::Map(map) => map.get(&id.0).cloned(),
1365                        _ => None,
1366                    }) {
1367                        return Ok(value);
1368                    }
1369                    match id.0.as_str() {
1370                        "Math" => Ok(Value::BuiltinTable(BuiltinTableKind::Math)),
1371                        "MusicNote" => Ok(Value::BuiltinTable(BuiltinTableKind::MusicNote)),
1372                        _ => Ok(Value::Identifier(id.0.clone())),
1373                    }
1374                }
1375            }
1376        }
1377        Expression::Component(ce) => eval_ce(ce, ctx),
1378        Expression::Function { params, body } => {
1379            let captured_env = ctx.object_world.snapshot_visible();
1380            Ok(Value::Function {
1381                params: params.iter().map(|p| p.0.clone()).collect(),
1382                body: body.clone(),
1383                captured_env: Arc::new(captured_env),
1384                heap: ctx.object_world.heap().clone(),
1385            })
1386        }
1387        Expression::Call(call) => eval_call(call, ctx),
1388        Expression::BinaryOp { op, lhs, rhs } => eval_binop(op, lhs, rhs, ctx),
1389        Expression::UnaryOp { op, operand } => eval_unaryop(op, operand, ctx),
1390    }
1391}
1392
1393fn eval_call(call: &CallExpression, ctx: &mut EvalContext<'_>) -> Result<Value, String> {
1394    // Method call: `obj.method(args)` — callee is BinaryOp(Dot, lhs, rhs).
1395    if let Expression::BinaryOp {
1396        op: BinOpKind::Dot,
1397        lhs,
1398        rhs,
1399    } = call.callee.as_ref()
1400    {
1401        let receiver = eval_expr(lhs, ctx)?;
1402        let method_name = match rhs.as_ref() {
1403            Expression::Identifier(id) => id.0.clone(),
1404            other => {
1405                return Err(format!(
1406                    "method call: RHS of '.' must be an identifier, got {:?}",
1407                    other
1408                ));
1409            }
1410        };
1411        let args: Vec<Value> = call
1412            .args
1413            .iter()
1414            .map(|a| eval_expr(a, ctx))
1415            .collect::<Result<_, _>>()?;
1416        return eval_method_call(receiver, &method_name, args, ctx);
1417    }
1418
1419    let callee_name = match call.callee.as_ref() {
1420        Expression::Identifier(id) => &id.0,
1421        other => return Err(format!("cannot call {:?} as a function", other)),
1422    };
1423
1424    // Built-in: print(value)
1425    if callee_name == "print" {
1426        let arg = call
1427            .args
1428            .first()
1429            .map(|a| eval_expr(a, ctx))
1430            .transpose()?
1431            .unwrap_or(Value::Null);
1432        println!("[mms] {}", value_display(&arg));
1433        return Ok(Value::Null);
1434    }
1435
1436    // Built-in: assert(cond, msg)
1437    if callee_name == "assert" {
1438        let cond = call
1439            .args
1440            .first()
1441            .map(|a| eval_expr(a, ctx))
1442            .transpose()?
1443            .unwrap_or(Value::Null);
1444        if !is_truthy(&cond) {
1445            let msg = call
1446                .args
1447                .get(1)
1448                .map(|a| eval_expr(a, ctx))
1449                .transpose()?
1450                .unwrap_or(Value::String("assertion failed".into()));
1451            return Err(format!("assert: {}", value_display(&msg)));
1452        }
1453        return Ok(Value::Null);
1454    }
1455
1456    // Built-in: range(n) or range(start, end)
1457    if callee_name == "range" {
1458        let args: Vec<Value> = call
1459            .args
1460            .iter()
1461            .map(|a| eval_expr(a, ctx))
1462            .collect::<Result<_, _>>()?;
1463        let (start, end) = match args.as_slice() {
1464            [Value::Number(n)] => (0.0_f64, *n),
1465            [Value::Number(s), Value::Number(e)] => (*s, *e),
1466            _ => return Err("range() takes 1 or 2 numeric arguments".into()),
1467        };
1468        let count = ((end - start).max(0.0).floor()) as usize;
1469        let arr = (0..count)
1470            .map(|i| Value::Number(start + i as f64))
1471            .collect();
1472        return Ok(Value::Array(arr));
1473    }
1474
1475    if matches!(
1476        callee_name.as_str(),
1477        "tree" | "dump" | "help" | "clear" | "reset"
1478    ) {
1479        let args: Vec<Value> = call
1480            .args
1481            .iter()
1482            .map(|a| eval_expr(a, ctx))
1483            .collect::<Result<_, _>>()?;
1484        match callee_name.as_str() {
1485            "tree" => {
1486                let value = args
1487                    .first()
1488                    .cloned()
1489                    .unwrap_or_else(|| Value::Identifier("__mms_world__".into()));
1490                let max_depth = match args.get(1) {
1491                    Some(Value::Number(n)) if *n >= 0.0 => Some(*n as usize),
1492                    Some(other) => {
1493                        return Err(format!("tree(): max_depth must be a number, got {other:?}"));
1494                    }
1495                    None => None,
1496                };
1497                if let Some(ch) = ctx.channels.as_mut() {
1498                    ch.call(HostCallKind::ReplTree { value, max_depth });
1499                }
1500            }
1501            "dump" => {
1502                let value = args
1503                    .first()
1504                    .cloned()
1505                    .unwrap_or_else(|| Value::Identifier("__mms_world__".into()));
1506                if let Some(ch) = ctx.channels.as_mut() {
1507                    ch.call(HostCallKind::ReplDump { value });
1508                }
1509            }
1510            "help" => {
1511                if let Some(ch) = ctx.channels.as_mut() {
1512                    ch.call(HostCallKind::ReplHelp);
1513                }
1514            }
1515            "clear" => {
1516                if let Some(ch) = ctx.channels.as_mut() {
1517                    ch.call(HostCallKind::ReplClear);
1518                }
1519            }
1520            "reset" => {
1521                ctx.object_world.request_reset();
1522            }
1523            _ => unreachable!(),
1524        }
1525        return Ok(Value::Null);
1526    }
1527
1528    // Built-in: query(selector) / query(selector, handler)
1529    //           query_all(selector) / query_all(selector, handler)
1530    if callee_name == "query" || callee_name == "query_all" {
1531        let multiple = callee_name == "query_all";
1532        let args: Vec<Value> = call
1533            .args
1534            .iter()
1535            .map(|a| eval_expr(a, ctx))
1536            .collect::<Result<_, _>>()?;
1537        let has_explicit_scope = args.first().is_some_and(|value| match value {
1538            Value::ComponentObject { .. } => true,
1539            Value::Identifier(value) => value == "__mms_world__",
1540            _ => false,
1541        });
1542        if !has_explicit_scope && matches!(ctx.implicit_cwd, Some(Value::ComponentExpr(_))) {
1543            return Err(format!(
1544                "{}(): detached component expressions have no live query scope; emit it or pass world explicitly",
1545                callee_name
1546            ));
1547        }
1548        let (scope, selector_index) = match args.first() {
1549            Some(Value::ComponentObject { id, .. }) => (Some(*id), 1),
1550            Some(Value::Identifier(s)) if s == "__mms_world__" => (None, 1),
1551            _ => (
1552                match &ctx.implicit_cwd {
1553                    Some(Value::ComponentObject { id, .. }) => Some(*id),
1554                    _ => None,
1555                },
1556                0,
1557            ),
1558        };
1559        let selector = match args.get(selector_index) {
1560            Some(Value::String(s)) => s.clone(),
1561            other => {
1562                return Err(format!(
1563                    "{}(): selector must be a string, got {:?}",
1564                    callee_name, other
1565                ));
1566            }
1567        };
1568        let handler = match args.get(selector_index + 1) {
1569            Some(f @ Value::Function { .. }) => Some(f.clone()),
1570            None => None,
1571            other => {
1572                return Err(format!(
1573                    "{}(): arg 1 (optional) must be a function, got {:?}",
1574                    callee_name, other
1575                ));
1576            }
1577        };
1578        let result = run_world_query(selector, scope, multiple, ctx)?;
1579        return dispatch_query_result(result, handler, multiple, ctx);
1580    }
1581
1582    // Built-in:
1583    //   on(component_object, "SignalKind", fn(event) { ... })
1584    //   on(component_object, "SignalKind", "handler_name", fn(event) { ... })
1585    if callee_name == "on" {
1586        let args: Vec<Value> = call
1587            .args
1588            .iter()
1589            .map(|a| eval_expr(a, ctx))
1590            .collect::<Result<_, _>>()?;
1591        let scope = match args.get(0) {
1592            Some(Value::ComponentObject { id, .. }) => *id,
1593            other => {
1594                return Err(format!(
1595                    "on(): arg 0 must be a ComponentObject, got {:?}",
1596                    other
1597                ));
1598            }
1599        };
1600        let signal_kind = match args.get(1) {
1601            Some(Value::String(s)) => parse_signal_kind(s)?,
1602            other => {
1603                return Err(format!(
1604                    "on(): arg 1 must be a signal kind string, got {:?}",
1605                    other
1606                ));
1607            }
1608        };
1609        let (name, handler_idx) = match args.get(2) {
1610            Some(Value::String(name)) => (Some(name.clone()), 3),
1611            _ => (None, 2),
1612        };
1613        let handler = match args.get(handler_idx) {
1614            Some(f @ Value::Function { .. }) => f.clone(),
1615            other => {
1616                return Err(format!(
1617                    "on(): arg {} must be a function, got {:?}",
1618                    handler_idx, other
1619                ));
1620            }
1621        };
1622        if let Some(ch) = ctx.channels.as_mut() {
1623            ch.call(HostCallKind::RegisterHandler {
1624                scope,
1625                signal_kind,
1626                name,
1627                handler,
1628            });
1629        }
1630        return Ok(Value::Null);
1631    }
1632
1633    // Built-in:
1634    //   emit_data(scope_component, "name")
1635    //   emit_data(scope_component, "name", payload_component)
1636    if callee_name == "emit_data" {
1637        let args: Vec<Value> = call
1638            .args
1639            .iter()
1640            .map(|a| eval_expr(a, ctx))
1641            .collect::<Result<_, _>>()?;
1642        let scope = match args.first() {
1643            Some(Value::ComponentObject { id, .. }) => *id,
1644            other => {
1645                return Err(format!(
1646                    "emit_data(): arg 0 must be a ComponentObject, got {:?}",
1647                    other
1648                ));
1649            }
1650        };
1651        let name = match args.get(1) {
1652            Some(Value::String(name)) => name.clone(),
1653            other => {
1654                return Err(format!(
1655                    "emit_data(): arg 1 must be a string, got {:?}",
1656                    other
1657                ));
1658            }
1659        };
1660        let payload = match args.get(2) {
1661            Some(Value::ComponentObject { id, .. }) => Some(*id),
1662            Some(Value::Null) | None => None,
1663            other => {
1664                return Err(format!(
1665                    "emit_data(): arg 2 must be a ComponentObject or null, got {:?}",
1666                    other
1667                ));
1668            }
1669        };
1670        let emitted = LIVE_SIGNAL_EMITTER.with(|slot| {
1671            let Some(host_emit) = *slot.borrow() else {
1672                return false;
1673            };
1674            unsafe {
1675                (&mut *host_emit).push_event(
1676                    scope,
1677                    crate::engine::ecs::EventSignal::DataEvent { name, payload },
1678                );
1679            }
1680            true
1681        });
1682        if !emitted {
1683            return Err("emit_data(): no live signal emitter".into());
1684        }
1685        return Ok(Value::Null);
1686    }
1687
1688    let callee_val = match ctx.object_world.lookup(callee_name) {
1689        Some(v) => v.clone(),
1690        None => return Err(format!("undefined: '{}'", callee_name)),
1691    };
1692
1693    match callee_val {
1694        Value::Function {
1695            params,
1696            body,
1697            captured_env,
1698            ..
1699        } => {
1700            let args: Vec<Value> = call
1701                .args
1702                .iter()
1703                .map(|a| eval_expr(a, ctx))
1704                .collect::<Result<_, _>>()?;
1705
1706            // Push a Function frame seeded with the closure's captured env;
1707            // bind arg values into the same frame so they shadow captured names.
1708            ctx.object_world.push_function_frame(captured_env);
1709            for (index, param) in params.iter().enumerate() {
1710                let arg = args.get(index).cloned().unwrap_or(Value::Null);
1711                ctx.object_world.bind(param.clone(), arg);
1712            }
1713            let result = {
1714                let mut func_ctx = EvalContext {
1715                    emits: ctx.emits,
1716                    source_path: None,
1717                    channels: ctx.channels.as_mut().map(|c| &mut **c),
1718                    ce_builder: None,
1719                    object_world: ctx.object_world,
1720                    host_world: ctx.host_world,
1721                    exec_scope: ctx.exec_scope,
1722                    runtime_closure_mode: ctx.runtime_closure_mode,
1723                    implicit_cwd: ctx.implicit_cwd.clone(),
1724                };
1725                eval_block_stmts(&body.statements, &mut func_ctx)
1726            };
1727            ctx.object_world.pop_frame();
1728            match result? {
1729                StmtEffect::Return(val) => Ok(val),
1730                StmtEffect::None => Ok(Value::Null),
1731                StmtEffect::Break | StmtEffect::Continue => {
1732                    Err("break/continue cannot escape a function body".into())
1733                }
1734                StmtEffect::Exported(_) => Ok(Value::Null),
1735            }
1736        }
1737        other => Err(format!("cannot call {:?} as a function", other)),
1738    }
1739}
1740
1741/// Issue a Query HostCall and decode the reply into a list of
1742/// `Value::ComponentObject`s. For `multiple = false` the list contains 0 or 1.
1743/// Returns Err if the host had no channels available (fire-and-forget mode).
1744fn run_world_query(
1745    selector: String,
1746    scope: Option<ComponentId>,
1747    multiple: bool,
1748    ctx: &mut EvalContext<'_>,
1749) -> Result<Vec<Value>, String> {
1750    if let Some(ch) = ctx.channels.as_mut() {
1751        let reply = ch.call(HostCallKind::Query {
1752            selector,
1753            scope,
1754            multiple,
1755        });
1756        let out = match reply {
1757            None => Vec::new(),
1758            Some(HostValue::Component { id, component_type }) => {
1759                vec![Value::ComponentObject { id, component_type }]
1760            }
1761            Some(HostValue::ComponentList(list)) => list
1762                .into_iter()
1763                .map(|(id, component_type)| Value::ComponentObject { id, component_type })
1764                .collect(),
1765            Some(other) => {
1766                return Err(format!("query: unexpected HostValue reply: {:?}", other));
1767            }
1768        };
1769        return Ok(out);
1770    }
1771
1772    let Some(world) = ctx.host_world else {
1773        // No host (fire-and-forget runner) — return empty.
1774        return Ok(Vec::new());
1775    };
1776    let world = unsafe { &mut *world };
1777
1778    let roots: Vec<ComponentId> = match scope {
1779        Some(id) => vec![id],
1780        None => world
1781            .all_components()
1782            .filter(|&id| world.parent_of(id).is_none())
1783            .collect(),
1784    };
1785
1786    let mut all_ids: Vec<ComponentId> = Vec::new();
1787    for root in roots {
1788        if multiple {
1789            all_ids.extend(world.find_all_components(root, &selector));
1790        } else if let Some(found) = world.find_component(root, &selector) {
1791            all_ids.push(found);
1792            break;
1793        }
1794    }
1795
1796    let out = if multiple {
1797        all_ids
1798            .into_iter()
1799            .filter_map(|id| {
1800                world
1801                    .component_name(id)
1802                    .map(|component_type| Value::ComponentObject {
1803                        id,
1804                        component_type: component_type.to_string(),
1805                    })
1806            })
1807            .collect()
1808    } else {
1809        match all_ids.into_iter().next() {
1810            Some(id) => match world.component_name(id) {
1811                Some(component_type) => vec![Value::ComponentObject {
1812                    id,
1813                    component_type: component_type.to_string(),
1814                }],
1815                None => Vec::new(),
1816            },
1817            None => Vec::new(),
1818        }
1819    };
1820    Ok(out)
1821}
1822
1823/// Shape the query reply: scalar/null for `query`, Array for `query_all`,
1824/// or invoke the handler when one was supplied.
1825///
1826/// - no callback, multiple=false → first match (`Value::ComponentObject`) or `Null`
1827/// - no callback, multiple=true  → `Value::Array` of matches (possibly empty)
1828/// - callback,    multiple=false → handler called once with first match (or `Null`); returns `Null`
1829/// - callback,    multiple=true  → handler called once per match; returns `Null`
1830fn dispatch_query_result(
1831    mut matches: Vec<Value>,
1832    handler: Option<Value>,
1833    multiple: bool,
1834    ctx: &mut EvalContext<'_>,
1835) -> Result<Value, String> {
1836    if let Some(handler) = handler {
1837        if multiple {
1838            for m in matches {
1839                eval_user_fn(&handler, vec![m], ctx)?;
1840            }
1841        } else {
1842            let arg = matches.into_iter().next().unwrap_or(Value::Null);
1843            eval_user_fn(&handler, vec![arg], ctx)?;
1844        }
1845        return Ok(Value::Null);
1846    }
1847    if multiple {
1848        Ok(Value::Array(matches))
1849    } else {
1850        Ok(matches.drain(..).next().unwrap_or(Value::Null))
1851    }
1852}
1853
1854/// Call an MMS `Value::Function` with the given args using the current eval
1855/// context. Returns whatever the function returns (or `Null`).
1856fn eval_user_fn(
1857    handler: &Value,
1858    args: Vec<Value>,
1859    ctx: &mut EvalContext<'_>,
1860) -> Result<Value, String> {
1861    let Value::Function {
1862        params,
1863        body,
1864        captured_env,
1865        ..
1866    } = handler
1867    else {
1868        return Err(format!("expected function, got {:?}", handler));
1869    };
1870    ctx.object_world.push_function_frame(captured_env.clone());
1871    for (index, param) in params.iter().enumerate() {
1872        let arg = args.get(index).cloned().unwrap_or(Value::Null);
1873        ctx.object_world.bind(param.clone(), arg);
1874    }
1875    let result = {
1876        let mut func_ctx = EvalContext {
1877            emits: ctx.emits,
1878            source_path: None,
1879            channels: ctx.channels.as_mut().map(|c| &mut **c),
1880            ce_builder: None,
1881            object_world: ctx.object_world,
1882            host_world: ctx.host_world,
1883            exec_scope: ctx.exec_scope,
1884            runtime_closure_mode: ctx.runtime_closure_mode,
1885            implicit_cwd: ctx.implicit_cwd.clone(),
1886        };
1887        eval_block_stmts(&body.statements, &mut func_ctx)
1888    };
1889    ctx.object_world.pop_frame();
1890    match result? {
1891        StmtEffect::Return(val) => Ok(val),
1892        _ => Ok(Value::Null),
1893    }
1894}
1895
1896/// Dispatch a method call on a `Value::ComponentObject`.
1897///
1898/// Produces intents (emitted via `ctx.emits`) or returns a value.
1899/// Currently supports animation methods: `play`, `pause`, `loop_anim`.
1900fn eval_method_call(
1901    receiver: Value,
1902    method: &str,
1903    args: Vec<Value>,
1904    ctx: &mut EvalContext<'_>,
1905) -> Result<Value, String> {
1906    match receiver {
1907        Value::BuiltinTable(BuiltinTableKind::Math) => eval_math_method(
1908            MathReceiverKind::BuiltinTable(BuiltinTableKind::Math),
1909            method,
1910            &args,
1911        ),
1912        Value::BuiltinTable(BuiltinTableKind::MusicNote) => {
1913            let pitch_ctor = match method {
1914                "a" => MusicNote::a,
1915                "b" => MusicNote::b,
1916                "c" => MusicNote::c,
1917                "d" => MusicNote::d,
1918                "e" => MusicNote::e,
1919                "f" => MusicNote::f,
1920                "g" => MusicNote::g,
1921                _ => return Err(format!("MusicNote: unknown note '{}'", method)),
1922            };
1923
1924            let octave = match args.first() {
1925                Some(Value::Number(n)) if n.is_finite() && *n >= 0.0 && n.fract() == 0.0 => {
1926                    *n as u16
1927                }
1928                other => {
1929                    return Err(format!(
1930                        "MusicNote.{}(): arg 0 must be a non-negative integer octave, got {:?}",
1931                        method, other
1932                    ));
1933                }
1934            };
1935            let duration_beats = match args.get(1) {
1936                Some(Value::Number(n)) => *n as f32,
1937                other => {
1938                    return Err(format!(
1939                        "MusicNote.{}(): arg 1 must be a numeric duration, got {:?}",
1940                        method, other
1941                    ));
1942                }
1943            };
1944            let mut note = pitch_ctor(octave, duration_beats);
1945            if let Some(Value::Number(velocity)) = args.get(3) {
1946                note = note.with_velocity(*velocity as f32);
1947            }
1948
1949            let Some(world) = ctx.host_world else {
1950                return Err(format!("MusicNote.{}(): no host world", method));
1951            };
1952            let world = unsafe { &mut *world };
1953            let target_source = match args.get(2) {
1954                Some(value) => Some(value_to_component_ref_live(world, value)?),
1955                None => None,
1956            };
1957            let target = target_source
1958                .as_ref()
1959                .and_then(|src| resolve_live_component_ref_global(world, src))
1960                .ok_or_else(|| {
1961                    format!(
1962                        "MusicNote.{}(): arg 2 must resolve to an audio target",
1963                        method
1964                    )
1965                })?;
1966
1967            push_eval_intent(
1968                ctx,
1969                IntentValue::AudioSchedulePlay {
1970                    component_ids: vec![target],
1971                    beat_offset: 0.0,
1972                    beat_context: None,
1973                    note: Some(note),
1974                    gain: None,
1975                    rate: None,
1976                    duration: None,
1977                },
1978            );
1979            Ok(Value::Null)
1980        }
1981        Value::ComponentObject {
1982            id,
1983            ref component_type,
1984        } => {
1985            // Subtree query — `comp.query("sel")` / `comp.query_all("sel")`.
1986            // Also accepts an optional handler arg (same shape as the free builtins).
1987            if method == "query" || method == "query_all" {
1988                let multiple = method == "query_all";
1989                let selector = match args.first() {
1990                    Some(Value::String(s)) => s.clone(),
1991                    other => {
1992                        return Err(format!(
1993                            "{}(): arg 0 must be a string selector, got {:?}",
1994                            method, other
1995                        ));
1996                    }
1997                };
1998                let handler = match args.get(1) {
1999                    Some(f @ Value::Function { .. }) => Some(f.clone()),
2000                    None => None,
2001                    other => {
2002                        return Err(format!(
2003                            "{}(): arg 1 (optional) must be a function, got {:?}",
2004                            method, other
2005                        ));
2006                    }
2007                };
2008                let result = run_world_query(selector, Some(id), multiple, ctx)?;
2009                return dispatch_query_result(result, handler, multiple, ctx);
2010            }
2011
2012            if supports_component_method(component_type, method) {
2013                if let Some(world) = ctx.host_world {
2014                    let world = unsafe { &mut *world };
2015                    return invoke_component_method(
2016                        world,
2017                        id,
2018                        component_type,
2019                        method,
2020                        &args,
2021                        |intent| push_eval_intent(ctx, intent),
2022                    );
2023                }
2024                if let Some(ch) = ctx.channels.as_mut() {
2025                    match ch.call(HostCallKind::InvokeComponentMethod {
2026                        id,
2027                        component_type: component_type.clone(),
2028                        method: method.to_string(),
2029                        args: args.clone(),
2030                    }) {
2031                        Some(HostValue::Null) | None => return Ok(Value::Null),
2032                        Some(HostValue::Component { id, component_type }) => {
2033                            return Ok(Value::ComponentObject { id, component_type });
2034                        }
2035                        Some(HostValue::ComponentId(component_id)) => {
2036                            return Ok(Value::ComponentObject {
2037                                id: component_id,
2038                                component_type: component_type.clone(),
2039                            });
2040                        }
2041                        Some(other) => {
2042                            return Err(format!(
2043                                "InvokeComponentMethod returned unexpected value {:?}",
2044                                other
2045                            ));
2046                        }
2047                    }
2048                }
2049            }
2050
2051            // Animation playback.
2052            let anim_state = match method {
2053                "play"
2054                    if matches!(
2055                        component_type.as_str(),
2056                        "A" | "Animation" | "AnimationComponent" | "animation"
2057                    ) =>
2058                {
2059                    Some(AnimationState::Playing)
2060                }
2061                "loop_anim"
2062                    if matches!(
2063                        component_type.as_str(),
2064                        "A" | "Animation" | "AnimationComponent" | "animation"
2065                    ) =>
2066                {
2067                    Some(AnimationState::Looping)
2068                }
2069                "pause"
2070                    if matches!(
2071                        component_type.as_str(),
2072                        "A" | "Animation" | "AnimationComponent" | "animation"
2073                    ) =>
2074                {
2075                    Some(AnimationState::Paused)
2076                }
2077                _ => None,
2078            };
2079            if let Some(state) = anim_state {
2080                push_eval_intent(
2081                    ctx,
2082                    IntentValue::SetAnimationState {
2083                        component_ids: vec![id],
2084                        state,
2085                    },
2086                );
2087                return Ok(Value::Null);
2088            }
2089
2090            // Layout getter: layout.available_width() → current width as Number.
2091            if matches!(
2092                component_type.as_str(),
2093                "layout" | "LayoutRoot" | "LayoutComponent"
2094            ) && method == "available_width"
2095            {
2096                use crate::engine::ecs::system::layout::measure::layout_root_available_bounds;
2097                let Some(world) = ctx.host_world else {
2098                    return Err("available_width(): no host world".into());
2099                };
2100                let world = unsafe { &mut *world };
2101                let (w, _, _) = layout_root_available_bounds(world, id);
2102                if world
2103                    .get_component_by_id_as::<crate::engine::ecs::component::LayoutComponent>(id)
2104                    .is_none()
2105                {
2106                    return Err("available_width(): not a LayoutComponent".into());
2107                }
2108                let w = w as f64;
2109                return Ok(Value::Number(w));
2110            }
2111
2112            if matches!(
2113                component_type.as_str(),
2114                "layout" | "LayoutRoot" | "LayoutComponent"
2115            ) && method == "available_height"
2116            {
2117                use crate::engine::ecs::system::layout::measure::layout_root_available_bounds;
2118                let Some(world) = ctx.host_world else {
2119                    return Err("available_height(): no host world".into());
2120                };
2121                let world = unsafe { &mut *world };
2122                if world
2123                    .get_component_by_id_as::<crate::engine::ecs::component::LayoutComponent>(id)
2124                    .is_none()
2125                {
2126                    return Err("available_height(): not a LayoutComponent".into());
2127                }
2128                let (_, h, _) = layout_root_available_bounds(world, id);
2129                let h = h
2130                    .map(|value| value as f64)
2131                    .ok_or_else(|| "available_height(): height is unset".to_string())?;
2132                return Ok(Value::Number(h));
2133            }
2134
2135            // Layout mutation: layout.set_available_width(N).
2136            if matches!(
2137                component_type.as_str(),
2138                "layout" | "LayoutRoot" | "LayoutComponent"
2139            ) && method == "set_available_width"
2140            {
2141                use crate::engine::ecs::component::style::SizeDimension;
2142                use crate::scripting::token::Unit;
2143
2144                let width = match args.first() {
2145                    Some(Value::Number(n)) => SizeDimension::GlyphUnits(*n as f32),
2146                    Some(Value::Dimension {
2147                        value,
2148                        unit: Unit::GlyphUnits,
2149                    }) => SizeDimension::GlyphUnits(*value as f32),
2150                    Some(Value::Dimension {
2151                        value,
2152                        unit: Unit::WorldUnits,
2153                    }) => SizeDimension::WorldUnits(*value as f32),
2154                    Some(Value::Dimension { unit, .. }) => {
2155                        return Err(format!(
2156                            "set_available_width: expected gu or wu dimension, got {:?}",
2157                            unit
2158                        ));
2159                    }
2160                    Some(other) => {
2161                        return Err(format!(
2162                            "set_available_width: expected number or dimension argument, got {:?}",
2163                            other
2164                        ));
2165                    }
2166                    None => {
2167                        return Err(
2168                            "set_available_width: missing number or dimension argument".into()
2169                        );
2170                    }
2171                };
2172                push_eval_intent(
2173                    ctx,
2174                    IntentValue::SetLayoutAvailableWidth {
2175                        component_ids: vec![id],
2176                        width,
2177                    },
2178                );
2179                return Ok(Value::Null);
2180            }
2181
2182            if matches!(
2183                component_type.as_str(),
2184                "layout" | "LayoutRoot" | "LayoutComponent"
2185            ) && method == "set_available_height"
2186            {
2187                use crate::engine::ecs::component::style::SizeDimension;
2188                use crate::scripting::token::Unit;
2189
2190                let height = match args.first() {
2191                    Some(Value::Number(n)) => SizeDimension::GlyphUnits(*n as f32),
2192                    Some(Value::Dimension {
2193                        value,
2194                        unit: Unit::GlyphUnits,
2195                    }) => SizeDimension::GlyphUnits(*value as f32),
2196                    Some(Value::Dimension {
2197                        value,
2198                        unit: Unit::WorldUnits,
2199                    }) => SizeDimension::WorldUnits(*value as f32),
2200                    Some(Value::Dimension { unit, .. }) => {
2201                        return Err(format!(
2202                            "set_available_height: expected gu or wu dimension, got {:?}",
2203                            unit
2204                        ));
2205                    }
2206                    Some(other) => {
2207                        return Err(format!(
2208                            "set_available_height: expected number or dimension argument, got {:?}",
2209                            other
2210                        ));
2211                    }
2212                    None => {
2213                        return Err(
2214                            "set_available_height: missing number or dimension argument".into()
2215                        );
2216                    }
2217                };
2218                push_eval_intent(
2219                    ctx,
2220                    IntentValue::SetLayoutAvailableHeight {
2221                        component_ids: vec![id],
2222                        height,
2223                    },
2224                );
2225                return Ok(Value::Null);
2226            }
2227
2228            // Layout viz toggle: layout.set_inspect(bool) / .enable_inspect() / .disable_inspect().
2229            if matches!(
2230                component_type.as_str(),
2231                "layout" | "LayoutRoot" | "LayoutComponent"
2232            ) && matches!(method, "set_inspect" | "enable_inspect" | "disable_inspect")
2233            {
2234                let enabled = match (method, args.first()) {
2235                    ("enable_inspect", _) => true,
2236                    ("disable_inspect", _) => false,
2237                    ("set_inspect", Some(Value::Bool(b))) => *b,
2238                    ("set_inspect", Some(other)) => {
2239                        return Err(format!(
2240                            "set_inspect: expected bool argument, got {:?}",
2241                            other
2242                        ));
2243                    }
2244                    ("set_inspect", None) => {
2245                        return Err("set_inspect: missing bool argument".into());
2246                    }
2247                    _ => unreachable!(),
2248                };
2249                push_eval_intent(
2250                    ctx,
2251                    IntentValue::SetLayoutInspect {
2252                        component_ids: vec![id],
2253                        enabled,
2254                    },
2255                );
2256                return Ok(Value::Null);
2257            }
2258
2259            // Text mutation: text.set_text("...").
2260            if matches!(
2261                component_type.as_str(),
2262                "Text" | "TXT" | "TextComponent" | "text"
2263            ) && method == "set_text"
2264            {
2265                let text = match args.first() {
2266                    Some(Value::String(s)) => s.clone(),
2267                    Some(other) => {
2268                        return Err(format!(
2269                            "set_text: expected string argument, got {:?}",
2270                            other
2271                        ));
2272                    }
2273                    None => return Err("set_text: missing string argument".into()),
2274                };
2275                push_eval_intent(
2276                    ctx,
2277                    IntentValue::SetText {
2278                        component_ids: vec![id],
2279                        text,
2280                    },
2281                );
2282                return Ok(Value::Null);
2283            }
2284
2285            if matches!(
2286                component_type.as_str(),
2287                "T" | "Transform" | "TransformComponent" | "transform"
2288            ) && method == "set_position"
2289            {
2290                let [x, y, z] = match args.as_slice() {
2291                    [Value::Number(x), Value::Number(y), Value::Number(z)] => {
2292                        [*x as f32, *y as f32, *z as f32]
2293                    }
2294                    other => {
2295                        return Err(format!(
2296                            "set_position: expected three numeric arguments, got {:?}",
2297                            other
2298                        ));
2299                    }
2300                };
2301                let Some(world) = ctx.host_world else {
2302                    return Err("set_position(): no host world".into());
2303                };
2304                let world = unsafe { &mut *world };
2305                let t = world
2306                    .get_component_by_id_as::<crate::engine::ecs::component::TransformComponent>(id)
2307                    .ok_or_else(|| "set_position(): not a TransformComponent".to_string())?;
2308                let mut next = t.transform;
2309                next.translation = [x, y, z];
2310                next.recompute_model();
2311                push_eval_intent(
2312                    ctx,
2313                    IntentValue::UpdateTransform {
2314                        component_ids: vec![id],
2315                        translation: next.translation,
2316                        rotation_quat_xyzw: next.rotation,
2317                        scale: next.scale,
2318                    },
2319                );
2320                return Ok(Value::Null);
2321            }
2322
2323            if matches!(
2324                component_type.as_str(),
2325                "Camera3D" | "Camera3DComponent" | "camera3d" | "C3D"
2326            ) && matches!(method, "enabled" | "make_active_camera")
2327            {
2328                let Some(world) = ctx.host_world else {
2329                    return Err(format!("{method}(): no host world"));
2330                };
2331                let world = unsafe { &mut *world };
2332                if method == "enabled" {
2333                    if args.is_empty() {
2334                        let enabled = world
2335                            .get_component_by_id_as::<crate::engine::ecs::component::Camera3DComponent>(id)
2336                            .ok_or_else(|| "enabled(): not a Camera3DComponent".to_string())?
2337                            .enabled;
2338                        return Ok(Value::Bool(enabled));
2339                    }
2340                    let enabled = match args.first() {
2341                        Some(Value::Bool(b)) => *b,
2342                        Some(other) => {
2343                            return Err(format!(
2344                                "enabled: expected bool argument, got {:?}",
2345                                other
2346                            ));
2347                        }
2348                        None => unreachable!(),
2349                    };
2350                    let camera = world
2351                        .get_component_by_id_as_mut::<crate::engine::ecs::component::Camera3DComponent>(id)
2352                        .ok_or_else(|| "enabled(): not a Camera3DComponent".to_string())?;
2353                    camera.enabled = enabled;
2354                    return Ok(Value::Null);
2355                }
2356                push_eval_intent(
2357                    ctx,
2358                    IntentValue::MakeActiveCamera {
2359                        component_ids: vec![id],
2360                    },
2361                );
2362                return Ok(Value::Null);
2363            }
2364
2365            if matches!(
2366                component_type.as_str(),
2367                "CameraXR" | "CameraXRComponent" | "camera_xr" | "CXR"
2368            ) && matches!(method, "enabled" | "make_active_camera")
2369            {
2370                let Some(world) = ctx.host_world else {
2371                    return Err(format!("{method}(): no host world"));
2372                };
2373                let world = unsafe { &mut *world };
2374                if method == "enabled" {
2375                    if args.is_empty() {
2376                        let enabled = world
2377                            .get_component_by_id_as::<crate::engine::ecs::component::CameraXRComponent>(id)
2378                            .ok_or_else(|| "enabled(): not a CameraXRComponent".to_string())?
2379                            .enabled;
2380                        return Ok(Value::Bool(enabled));
2381                    }
2382                    let enabled = match args.first() {
2383                        Some(Value::Bool(b)) => *b,
2384                        Some(other) => {
2385                            return Err(format!(
2386                                "enabled: expected bool argument, got {:?}",
2387                                other
2388                            ));
2389                        }
2390                        None => unreachable!(),
2391                    };
2392                    let camera = world
2393                        .get_component_by_id_as_mut::<crate::engine::ecs::component::CameraXRComponent>(id)
2394                        .ok_or_else(|| "enabled(): not a CameraXRComponent".to_string())?;
2395                    camera.enabled = enabled;
2396                    return Ok(Value::Null);
2397                }
2398                push_eval_intent(
2399                    ctx,
2400                    IntentValue::MakeActiveCamera {
2401                        component_ids: vec![id],
2402                    },
2403                );
2404                return Ok(Value::Null);
2405            }
2406
2407            if matches!(
2408                component_type.as_str(),
2409                "Text" | "TXT" | "TextComponent" | "text"
2410            ) && method == "set_font_size"
2411            {
2412                let font_size = match args.first() {
2413                    Some(Value::Number(n)) => *n as f32,
2414                    Some(other) => {
2415                        return Err(format!(
2416                            "set_font_size: expected number argument, got {:?}",
2417                            other
2418                        ));
2419                    }
2420                    None => return Err("set_font_size: missing number argument".into()),
2421                };
2422                let Some(world) = ctx.host_world else {
2423                    return Err("set_font_size(): no host world".into());
2424                };
2425                let world = unsafe { &mut *world };
2426                let cur_text = world
2427                    .get_component_by_id_as::<crate::engine::ecs::component::TextComponent>(id)
2428                    .map(|t| t.text.clone())
2429                    .ok_or_else(|| "set_font_size(): not a TextComponent".to_string())?;
2430                if let Some(t) = world
2431                    .get_component_by_id_as_mut::<crate::engine::ecs::component::TextComponent>(id)
2432                {
2433                    t.set_font_size(font_size);
2434                }
2435                push_eval_intent(
2436                    ctx,
2437                    IntentValue::SetText {
2438                        component_ids: vec![id],
2439                        text: cur_text,
2440                    },
2441                );
2442                return Ok(Value::Null);
2443            }
2444
2445            if matches!(
2446                component_type.as_str(),
2447                "ObserverRouter" | "signal_observer_router" | "SignalObserverRouterComponent"
2448            ) && matches!(method, "blacklist" | "whitelist" | "block" | "allow")
2449            {
2450                let Some(world) = ctx.host_world else {
2451                    return Err(format!("{method}(): no host world"));
2452                };
2453                let world = unsafe { &mut *world };
2454                let router = world
2455                    .get_component_by_id_as_mut::<crate::engine::ecs::component::SignalObserverRouterComponent>(
2456                        id,
2457                    )
2458                    .ok_or_else(|| format!("{method}(): not a SignalObserverRouterComponent"))?;
2459                match method {
2460                    "blacklist" | "whitelist" => {
2461                        let values = match args.first() {
2462                            Some(Value::Array(values)) => values,
2463                            Some(other) => {
2464                                return Err(format!(
2465                                    "{method}(): expected array argument, got {:?}",
2466                                    other
2467                                ));
2468                            }
2469                            None => return Err(format!("{method}(): missing array argument")),
2470                        };
2471                        let mut out = Vec::with_capacity(values.len());
2472                        for value in values {
2473                            match value {
2474                                Value::String(s) => out.push(s.clone()),
2475                                other => {
2476                                    return Err(format!(
2477                                        "{method}(): expected string array, got {:?}",
2478                                        other
2479                                    ));
2480                                }
2481                            }
2482                        }
2483                        match method {
2484                            "blacklist" => router.blacklist = out,
2485                            "whitelist" => router.whitelist = out,
2486                            _ => unreachable!(),
2487                        }
2488                    }
2489                    "block" | "allow" => {
2490                        let name = match args.first() {
2491                            Some(Value::String(name)) => name.clone(),
2492                            Some(other) => {
2493                                return Err(format!(
2494                                    "{method}(): expected string argument, got {:?}",
2495                                    other
2496                                ));
2497                            }
2498                            None => return Err(format!("{method}(): missing string argument")),
2499                        };
2500                        if method == "block" {
2501                            if !router.blacklist.iter().any(|item| item == &name) {
2502                                router.blacklist.push(name);
2503                            }
2504                        } else {
2505                            router.blacklist.retain(|item| item != &name);
2506                        }
2507                    }
2508                    _ => unreachable!(),
2509                }
2510                return Ok(Value::Null);
2511            }
2512
2513            // AudioClip.instance([start_beat], [stop_beat]) — produce a
2514            // new clip that shares the receiver's decoded buffer but
2515            // gets its own playhead. Mirrors `let x = CE` semantics:
2516            // returns a detached handle, caller attaches by referencing
2517            // the binding inside a CE body (or manually). See
2518            // docs/draft/audio-clip-instance-cloning.md §3.
2519            if matches!(
2520                component_type.as_str(),
2521                "AudioClip" | "AudioClipComponent" | "audio_clip"
2522            ) && method == "instance"
2523            {
2524                let start_beat = match args.first() {
2525                    Some(Value::Number(n)) => Some(*n),
2526                    Some(Value::Null) | None => None,
2527                    Some(other) => {
2528                        return Err(format!(
2529                            "instance(): start_beat must be a number, got {:?}",
2530                            other
2531                        ));
2532                    }
2533                };
2534                let stop_beat = match args.get(1) {
2535                    Some(Value::Number(n)) => Some(*n),
2536                    Some(Value::Null) | None => None,
2537                    Some(other) => {
2538                        return Err(format!(
2539                            "instance(): stop_beat must be a number, got {:?}",
2540                            other
2541                        ));
2542                    }
2543                };
2544
2545                let Some(ch) = ctx.channels.as_mut() else {
2546                    return Err("instance(): no host channel".into());
2547                };
2548                let new_id = match ch.call(HostCallKind::AudioClipInstance {
2549                    source: id,
2550                    start_beat,
2551                    stop_beat,
2552                }) {
2553                    Some(HostValue::ComponentId(cid)) => cid,
2554                    _ => return Err("instance(): host AudioClipInstance failed".into()),
2555                };
2556                return Ok(Value::ComponentObject {
2557                    id: new_id,
2558                    component_type: "AudioClip".to_string(),
2559                });
2560            }
2561
2562            Err(format!(
2563                "no method '{}' on component type '{}'",
2564                method, component_type
2565            ))
2566        }
2567        other => Err(format!(
2568            "method call '{}': receiver is not a ComponentObject, got {:?}",
2569            method, other
2570        )),
2571    }
2572}
2573
2574fn eval_binop(
2575    op: &BinOpKind,
2576    lhs: &Expression,
2577    rhs: &Expression,
2578    ctx: &mut EvalContext<'_>,
2579) -> Result<Value, String> {
2580    // Short-circuit logical ops.
2581    match op {
2582        BinOpKind::And => {
2583            let l = eval_expr(lhs, ctx)?;
2584            if !is_truthy(&l) {
2585                return Ok(Value::Bool(false));
2586            }
2587            let r = eval_expr(rhs, ctx)?;
2588            return Ok(Value::Bool(is_truthy(&r)));
2589        }
2590        BinOpKind::Or => {
2591            let l = eval_expr(lhs, ctx)?;
2592            if is_truthy(&l) {
2593                return Ok(Value::Bool(true));
2594            }
2595            let r = eval_expr(rhs, ctx)?;
2596            return Ok(Value::Bool(is_truthy(&r)));
2597        }
2598        BinOpKind::Query => {
2599            // QueryDesugarTransform rewrites all `->` nodes into query()/query_all() calls
2600            // before eval runs. This arm is only reached if the transform missed one.
2601            return Err(
2602                "query operator '->' was not desugared by QueryDesugarTransform".to_string(),
2603            );
2604        }
2605        BinOpKind::Pipe => {
2606            let lhs_val = eval_expr(lhs, ctx)?;
2607            let rhs_val = eval_expr(rhs, ctx)?;
2608            match rhs_val {
2609                Value::Function {
2610                    params,
2611                    body,
2612                    captured_env,
2613                    ..
2614                } => {
2615                    ctx.object_world.push_function_frame(captured_env);
2616                    if let Some(param) = params.first() {
2617                        ctx.object_world.bind(param.clone(), lhs_val);
2618                    }
2619                    let result = {
2620                        let mut func_ctx = EvalContext {
2621                            emits: ctx.emits,
2622                            source_path: None,
2623                            channels: None,
2624                            ce_builder: None,
2625                            object_world: ctx.object_world,
2626                            host_world: None,
2627                            exec_scope: None,
2628                            runtime_closure_mode: RuntimeClosureExecMode::Full,
2629                            implicit_cwd: ctx.implicit_cwd.clone(),
2630                        };
2631                        eval_block_stmts(&body.statements, &mut func_ctx)
2632                    };
2633                    ctx.object_world.pop_frame();
2634                    match result? {
2635                        StmtEffect::Return(val) => return Ok(val),
2636                        _ => return Ok(Value::Null),
2637                    }
2638                }
2639                other => return Err(format!("pipe: RHS must be a function, got {:?}", other)),
2640            }
2641        }
2642        BinOpKind::Dot => {
2643            let lhs_val = eval_expr(lhs, ctx)?;
2644            let Expression::Identifier(field) = rhs else {
2645                return Err(format!(
2646                    "field access: RHS of '.' must be an identifier, got {:?}",
2647                    rhs
2648                ));
2649            };
2650            return match lhs_val {
2651                Value::BuiltinTable(BuiltinTableKind::Math) => builtin_math_field(&field.0)
2652                    .ok_or_else(|| format!("field access: '{}' not found", field.0)),
2653                Value::BuiltinTable(BuiltinTableKind::MusicNote) => match field.0.as_str() {
2654                    "a" | "b" | "c" | "d" | "e" | "f" | "g" => {
2655                        Ok(Value::Identifier(format!("MusicNote.{}", field.0)))
2656                    }
2657                    _ => Err(format!("field access: '{}' not found", field.0)),
2658                },
2659                Value::Map(fields) => fields
2660                    .get(&field.0)
2661                    .cloned()
2662                    .ok_or_else(|| format!("field access: '{}' not found", field.0)),
2663                Value::Object(id) => match id.with_map(|fields| fields.get(&field.0).cloned()) {
2664                    Some(Some(value)) => Ok(value),
2665                    Some(None) => Err(format!("field access: '{}' not found", field.0)),
2666                    None => Err("field access: invalid object".into()),
2667                },
2668                other => Err(format!(
2669                    "field access: cannot read '{}' from {:?}",
2670                    field.0, other
2671                )),
2672            };
2673        }
2674        _ => {}
2675    }
2676
2677    let l = eval_expr(lhs, ctx)?;
2678    let r = eval_expr(rhs, ctx)?;
2679
2680    match op {
2681        BinOpKind::Add => match (l, r) {
2682            (Value::Number(a), Value::Number(b)) => Ok(Value::Number(a + b)),
2683            (Value::Dimension { value: a, unit: au }, Value::Dimension { value: b, unit: bu }) => {
2684                dimension_add(a, au, b, bu)
2685            }
2686            (Value::String(a), Value::String(b)) => Ok(Value::String(a + &b)),
2687            (Value::String(a), r) => Ok(Value::String(a + &value_display(&r))),
2688            (l, Value::String(b)) => Ok(Value::String(value_display(&l) + &b)),
2689            (l, r) => Err(format!("type error: cannot add {:?} and {:?}", l, r)),
2690        },
2691        BinOpKind::Sub => match (l, r) {
2692            (Value::Number(a), Value::Number(b)) => Ok(Value::Number(a - b)),
2693            (Value::Dimension { value: a, unit: au }, Value::Dimension { value: b, unit: bu }) => {
2694                dimension_sub(a, au, b, bu)
2695            }
2696            (l, r) => Err(format!("type error: cannot subtract {:?} from {:?}", r, l)),
2697        },
2698        BinOpKind::Mul => match (l, r) {
2699            (Value::Number(a), Value::Number(b)) => Ok(Value::Number(a * b)),
2700            (Value::Dimension { value, unit }, Value::Number(n))
2701            | (Value::Number(n), Value::Dimension { value, unit }) => {
2702                dimension_scale(value, unit, n)
2703            }
2704            (l, r) => Err(format!("type error: cannot multiply {:?} and {:?}", l, r)),
2705        },
2706        BinOpKind::Div => match (l, r) {
2707            (Value::Number(a), Value::Number(b)) => {
2708                if b == 0.0 {
2709                    return Err("division by zero".to_string());
2710                }
2711                Ok(Value::Number(a / b))
2712            }
2713            (Value::Dimension { value, unit }, Value::Number(n)) => {
2714                if n == 0.0 {
2715                    return Err("division by zero".to_string());
2716                }
2717                dimension_scale(value, unit, 1.0 / n)
2718            }
2719            (l, r) => Err(format!("type error: cannot divide {:?} by {:?}", l, r)),
2720        },
2721        BinOpKind::Rem => match (l, r) {
2722            (Value::Number(a), Value::Number(b)) => Ok(Value::Number(a % b)),
2723            (l, r) => Err(format!("type error: cannot rem {:?} by {:?}", l, r)),
2724        },
2725        BinOpKind::Eq => Ok(Value::Bool(values_equal(&l, &r))),
2726        BinOpKind::NotEq => Ok(Value::Bool(!values_equal(&l, &r))),
2727        BinOpKind::Lt => num_cmp(l, r, |a, b| a < b),
2728        BinOpKind::Gt => num_cmp(l, r, |a, b| a > b),
2729        BinOpKind::LtEq => num_cmp(l, r, |a, b| a <= b),
2730        BinOpKind::GtEq => num_cmp(l, r, |a, b| a >= b),
2731        BinOpKind::And | BinOpKind::Or | BinOpKind::Pipe | BinOpKind::Query => {
2732            unreachable!("handled above")
2733        }
2734        BinOpKind::Dot => unreachable!("handled above"),
2735    }
2736}
2737
2738fn eval_unaryop(
2739    op: &UnaryOpKind,
2740    operand: &Expression,
2741    ctx: &mut EvalContext<'_>,
2742) -> Result<Value, String> {
2743    let val = eval_expr(operand, ctx)?;
2744    match op {
2745        UnaryOpKind::Neg => match val {
2746            Value::Number(n) => Ok(Value::Number(-n)),
2747            Value::Dimension { value, unit } => Ok(Value::Dimension {
2748                value: -value,
2749                unit,
2750            }),
2751            v => Err(format!("type error: cannot negate {:?}", v)),
2752        },
2753        UnaryOpKind::Not => Ok(Value::Bool(!is_truthy(&val))),
2754    }
2755}
2756
2757fn dimension_add(
2758    lhs: f64,
2759    lhs_unit: crate::scripting::token::Unit,
2760    rhs: f64,
2761    rhs_unit: crate::scripting::token::Unit,
2762) -> Result<Value, String> {
2763    use crate::scripting::token::Unit;
2764    if lhs_unit != rhs_unit {
2765        return Err(format!(
2766            "type error: cannot add dimensions with different units {:?} and {:?}",
2767            lhs_unit, rhs_unit
2768        ));
2769    }
2770    if lhs_unit == Unit::Percent {
2771        return Err("type error: percent arithmetic requires a layout boundary".into());
2772    }
2773    Ok(Value::Dimension {
2774        value: lhs + rhs,
2775        unit: lhs_unit,
2776    })
2777}
2778
2779fn dimension_sub(
2780    lhs: f64,
2781    lhs_unit: crate::scripting::token::Unit,
2782    rhs: f64,
2783    rhs_unit: crate::scripting::token::Unit,
2784) -> Result<Value, String> {
2785    dimension_add(lhs, lhs_unit, -rhs, rhs_unit)
2786}
2787
2788fn dimension_scale(
2789    value: f64,
2790    unit: crate::scripting::token::Unit,
2791    scale: f64,
2792) -> Result<Value, String> {
2793    use crate::scripting::token::Unit;
2794    if unit == Unit::Percent {
2795        return Err("type error: percent arithmetic requires a layout boundary".into());
2796    }
2797    Ok(Value::Dimension {
2798        value: value * scale,
2799        unit,
2800    })
2801}
2802
2803// ---------------------------------------------------------------------------
2804// Helpers
2805// ---------------------------------------------------------------------------
2806
2807fn value_as_f32(value: &Value) -> Result<f32, String> {
2808    match value {
2809        Value::Number(n) => Ok(*n as f32),
2810        other => Err(format!("expected number, got {other:?}")),
2811    }
2812}
2813
2814fn value_as_f64(value: &Value) -> Result<f64, String> {
2815    match value {
2816        Value::Number(n) => Ok(*n),
2817        other => Err(format!("expected number, got {other:?}")),
2818    }
2819}
2820
2821fn value_as_u16(value: &Value) -> Result<u16, String> {
2822    match value {
2823        Value::Number(n) if n.is_finite() && *n >= 0.0 && n.fract() == 0.0 => Ok(*n as u16),
2824        other => Err(format!("expected non-negative integer, got {other:?}")),
2825    }
2826}
2827
2828fn value_display(val: &Value) -> String {
2829    match val {
2830        Value::Null => "null".into(),
2831        Value::Bool(b) => b.to_string(),
2832        Value::Number(n) => {
2833            if n.fract() == 0.0 && n.abs() < 1e15 {
2834                format!("{}", *n as i64)
2835            } else {
2836                n.to_string()
2837            }
2838        }
2839        Value::String(s) => s.clone(),
2840        Value::Array(arr) => format!(
2841            "[{}]",
2842            arr.iter().map(value_display).collect::<Vec<_>>().join(", ")
2843        ),
2844        Value::Map(map) => format!(
2845            "{{{}}}",
2846            map.iter()
2847                .map(|(key, value)| format!("{key}: {}", value_display(value)))
2848                .collect::<Vec<_>>()
2849                .join(", ")
2850        ),
2851        Value::Function { .. } => "<fn>".into(),
2852        Value::ComponentObject { id, component_type } => format!("<{}:{:?}>", component_type, id),
2853        Value::ComponentExpr(_) => "<ce>".into(),
2854        Value::Object(id) => id
2855            .with_map(|map| {
2856                format!(
2857                    "{{{}}}",
2858                    map.iter()
2859                        .map(|(key, value)| format!("{key}: {}", value_display(value)))
2860                        .collect::<Vec<_>>()
2861                        .join(", ")
2862                )
2863            })
2864            .unwrap_or_else(|| "<object>".into()),
2865        Value::Identifier(s) => s.clone(),
2866        Value::BuiltinTable(BuiltinTableKind::Math) => "<builtin Math>".into(),
2867        Value::BuiltinTable(BuiltinTableKind::MusicNote) => "<builtin MusicNote>".into(),
2868        Value::Module { .. } => "<module>".into(),
2869        Value::Dimension { value, unit } => {
2870            let suffix = match unit {
2871                crate::scripting::token::Unit::Percent => "%",
2872                crate::scripting::token::Unit::GlyphUnits => "gu",
2873                crate::scripting::token::Unit::WorldUnits => "wu",
2874                crate::scripting::token::Unit::Degrees => "deg",
2875                crate::scripting::token::Unit::Radians => "rad",
2876            };
2877            format!("{}{}", value, suffix)
2878        }
2879    }
2880}
2881
2882#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2883enum MathReceiverKind {
2884    BuiltinTable(BuiltinTableKind),
2885}
2886
2887fn eval_math_method(
2888    receiver: MathReceiverKind,
2889    method: &str,
2890    args: &[Value],
2891) -> Result<Value, String> {
2892    let prefix = match receiver {
2893        MathReceiverKind::BuiltinTable(BuiltinTableKind::Math) => "Math",
2894        MathReceiverKind::BuiltinTable(other) => {
2895            return Err(format!("unsupported math receiver: {:?}", other));
2896        }
2897    };
2898
2899    match method {
2900        "sin" => Ok(Value::Number(math_arg1(prefix, method, args)?.sin())),
2901        "cos" => Ok(Value::Number(math_arg1(prefix, method, args)?.cos())),
2902        "sqrt" => Ok(Value::Number(math_arg1(prefix, method, args)?.sqrt())),
2903        "tan" => Ok(Value::Number(math_arg1(prefix, method, args)?.tan())),
2904        "atan" => Ok(Value::Number(math_arg1(prefix, method, args)?.atan())),
2905        "atan2" => {
2906            let (y, x) = math_arg2(prefix, method, args)?;
2907            Ok(Value::Number(y.atan2(x)))
2908        }
2909        "floor" => Ok(Value::Number(math_arg1(prefix, method, args)?.floor())),
2910        "ceil" => Ok(Value::Number(math_arg1(prefix, method, args)?.ceil())),
2911        "round" => Ok(Value::Number(math_arg1(prefix, method, args)?.round())),
2912        "abs" => Ok(Value::Number(math_arg1(prefix, method, args)?.abs())),
2913        "perlin" => match args {
2914            [Value::Number(x), Value::Number(y)] => {
2915                Ok(Value::Number(crate::utils::math::perlin(*x, *y, None)))
2916            }
2917            [Value::Number(x), Value::Number(y), Value::Number(z)] => {
2918                Ok(Value::Number(crate::utils::math::perlin(*x, *y, Some(*z))))
2919            }
2920            _ => Err(format!(
2921                "{prefix}.{method}(): expected 2 or 3 numeric arguments, got {:?}",
2922                args
2923            )),
2924        },
2925        "clamp" => {
2926            let (x, min, max) = math_arg3(prefix, method, args)?;
2927            Ok(Value::Number(x.max(min).min(max)))
2928        }
2929        "smoothstep" => {
2930            let (x, edge0, edge1) = math_arg3(prefix, method, args)?;
2931            if (edge1 - edge0).abs() <= f64::EPSILON {
2932                return Err(format!(
2933                    "{prefix}.{method}(): edge0 and edge1 must be distinct"
2934                ));
2935            }
2936            let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
2937            Ok(Value::Number(t * t * (3.0 - 2.0 * t)))
2938        }
2939        "dot" => {
2940            let (a, b) = math_array_arg2(prefix, method, args)?;
2941            if a.len() != b.len() {
2942                return Err(format!(
2943                    "{prefix}.{method}(): expected arrays of equal length, got {} and {}",
2944                    a.len(),
2945                    b.len()
2946                ));
2947            }
2948            Ok(Value::Number(
2949                a.iter().zip(b.iter()).map(|(lhs, rhs)| lhs * rhs).sum(),
2950            ))
2951        }
2952        "cross" => {
2953            let (a, b) = math_vec3_arg2(prefix, method, args)?;
2954            let cross = crate::utils::math::vec3_cross(a, b);
2955            Ok(Value::Array(
2956                cross.into_iter().map(|n| Value::Number(n as f64)).collect(),
2957            ))
2958        }
2959        _ => Err(format!("{prefix}: unknown method '{method}'")),
2960    }
2961}
2962
2963fn math_arg1(prefix: &str, method: &str, args: &[Value]) -> Result<f64, String> {
2964    match args {
2965        [Value::Number(n)] => Ok(*n),
2966        _ => Err(format!(
2967            "{prefix}.{method}(): expected 1 numeric argument, got {:?}",
2968            args
2969        )),
2970    }
2971}
2972
2973fn math_arg2(prefix: &str, method: &str, args: &[Value]) -> Result<(f64, f64), String> {
2974    match args {
2975        [Value::Number(a), Value::Number(b)] => Ok((*a, *b)),
2976        _ => Err(format!(
2977            "{prefix}.{method}(): expected 2 numeric arguments, got {:?}",
2978            args
2979        )),
2980    }
2981}
2982
2983fn math_arg3(prefix: &str, method: &str, args: &[Value]) -> Result<(f64, f64, f64), String> {
2984    match args {
2985        [Value::Number(a), Value::Number(b), Value::Number(c)] => Ok((*a, *b, *c)),
2986        _ => Err(format!(
2987            "{prefix}.{method}(): expected 3 numeric arguments, got {:?}",
2988            args
2989        )),
2990    }
2991}
2992
2993fn math_array_arg2(
2994    prefix: &str,
2995    method: &str,
2996    args: &[Value],
2997) -> Result<(Vec<f64>, Vec<f64>), String> {
2998    match args {
2999        [lhs, rhs] => Ok((
3000            value_as_number_array(lhs)
3001                .map_err(|err| format!("{prefix}.{method}(): arg 0 {err}"))?,
3002            value_as_number_array(rhs)
3003                .map_err(|err| format!("{prefix}.{method}(): arg 1 {err}"))?,
3004        )),
3005        _ => Err(format!(
3006            "{prefix}.{method}(): expected 2 array arguments, got {:?}",
3007            args
3008        )),
3009    }
3010}
3011
3012fn math_vec3_arg2(
3013    prefix: &str,
3014    method: &str,
3015    args: &[Value],
3016) -> Result<([f32; 3], [f32; 3]), String> {
3017    match args {
3018        [lhs, rhs] => Ok((
3019            value_as_f32_array(lhs).map_err(|err| format!("{prefix}.{method}(): arg 0 {err}"))?,
3020            value_as_f32_array(rhs).map_err(|err| format!("{prefix}.{method}(): arg 1 {err}"))?,
3021        )),
3022        _ => Err(format!(
3023            "{prefix}.{method}(): expected 2 array arguments, got {:?}",
3024            args
3025        )),
3026    }
3027}
3028
3029fn builtin_math_field(field: &str) -> Option<Value> {
3030    match field {
3031        "pi" => Some(Value::Number(std::f64::consts::PI)),
3032        "tau" => Some(Value::Number(std::f64::consts::TAU)),
3033        "e" => Some(Value::Number(std::f64::consts::E)),
3034        "sin" | "cos" | "sqrt" | "tan" | "atan" | "atan2" | "floor" | "ceil" | "round" | "abs"
3035        | "perlin" | "dot" | "cross" | "clamp" | "smoothstep" => {
3036            Some(Value::Identifier(format!("Math.{field}")))
3037        }
3038        _ => None,
3039    }
3040}
3041
3042fn value_as_f32_array<const N: usize>(value: &Value) -> Result<[f32; N], String> {
3043    match value {
3044        Value::Array(items) => {
3045            if items.len() != N {
3046                return Err(format!("expected array of {N}, got {}", items.len()));
3047            }
3048            let mut out = [0.0_f32; N];
3049            for (index, item) in items.iter().enumerate() {
3050                match item {
3051                    Value::Number(n) => out[index] = *n as f32,
3052                    other => {
3053                        return Err(format!(
3054                            "expected numeric array element at {index}, got {:?}",
3055                            other
3056                        ));
3057                    }
3058                }
3059            }
3060            Ok(out)
3061        }
3062        other => Err(format!("expected array, got {:?}", other)),
3063    }
3064}
3065
3066fn value_as_number_array(value: &Value) -> Result<Vec<f64>, String> {
3067    match value {
3068        Value::Array(items) => {
3069            let mut out = Vec::with_capacity(items.len());
3070            for (index, item) in items.iter().enumerate() {
3071                match item {
3072                    Value::Number(n) => out.push(*n),
3073                    other => {
3074                        return Err(format!(
3075                            "expected numeric array element at {index}, got {:?}",
3076                            other
3077                        ));
3078                    }
3079                }
3080            }
3081            Ok(out)
3082        }
3083        other => Err(format!("expected array, got {:?}", other)),
3084    }
3085}
3086
3087fn is_truthy(val: &Value) -> bool {
3088    match val {
3089        Value::Bool(b) => *b,
3090        Value::Null => false,
3091        _ => true,
3092    }
3093}
3094
3095fn values_equal(a: &Value, b: &Value) -> bool {
3096    match (a, b) {
3097        (Value::Null, Value::Null) => true,
3098        (Value::Bool(a), Value::Bool(b)) => a == b,
3099        (Value::Number(a), Value::Number(b)) => a == b,
3100        (Value::String(a), Value::String(b)) => a == b,
3101        _ => false,
3102    }
3103}
3104
3105fn num_cmp(l: Value, r: Value, f: impl Fn(f64, f64) -> bool) -> Result<Value, String> {
3106    match (l, r) {
3107        (Value::Number(a), Value::Number(b)) => Ok(Value::Bool(f(a, b))),
3108        (l, r) => Err(format!("type error: cannot compare {:?} and {:?}", l, r)),
3109    }
3110}
3111
3112fn parse_signal_kind(s: &str) -> Result<SignalKind, String> {
3113    match s {
3114        "Click" => Ok(SignalKind::Click),
3115        "DragStart" => Ok(SignalKind::DragStart),
3116        "DragMove" => Ok(SignalKind::DragMove),
3117        "DragEnd" => Ok(SignalKind::DragEnd),
3118        "RayIntersected" => Ok(SignalKind::RayIntersected),
3119        "ParentChanged" => Ok(SignalKind::ParentChanged),
3120        "CollisionStarted" => Ok(SignalKind::CollisionStarted),
3121        "CollisionEnded" => Ok(SignalKind::CollisionEnded),
3122        "SelectionChanged" => Ok(SignalKind::SelectionChanged),
3123        "SelectionAdded" => Ok(SignalKind::SelectionAdded),
3124        "SelectionRemoved" => Ok(SignalKind::SelectionRemoved),
3125        "SelectionCleared" => Ok(SignalKind::SelectionCleared),
3126        "Scrolling" => Ok(SignalKind::Scrolling),
3127        "DataEvent" => Ok(SignalKind::DataEvent),
3128        "XrButtonDown" => Ok(SignalKind::XrButtonDown),
3129        "XrButtonUp" => Ok(SignalKind::XrButtonUp),
3130        "XrButtonChanged" => Ok(SignalKind::XrButtonChanged),
3131        "XrAxisChanged" => Ok(SignalKind::XrAxisChanged),
3132        "TextInputFocusChanged" => Ok(SignalKind::TextInputFocusChanged),
3133        "TextInputChanged" => Ok(SignalKind::TextInputChanged),
3134        "HttpRequest" => Ok(SignalKind::HttpRequest),
3135        "HttpResponse" => Ok(SignalKind::HttpResponse),
3136        "HttpError" => Ok(SignalKind::HttpError),
3137        other => Err(format!("unknown signal kind: '{}'", other)),
3138    }
3139}
3140
3141/// Evaluate an MMS `Value::Function` with the given positional args.
3142///
3143/// Runs inline on the calling thread (no evaluator channel round-trips).
3144/// `channels` is `None`; when `world_host` is present, live world queries run
3145/// directly against that world. Intents emitted by the body (e.g. from method
3146/// dispatch like `anim.play()` or `text.set_text(...)`) are forwarded to `emit`
3147/// when provided.
3148pub(crate) fn eval_mms_fn(
3149    fn_val: &Value,
3150    args: Vec<Value>,
3151    channels: Option<&mut EvalChannels>,
3152    world_host: Option<&mut World>,
3153    mut emit: Option<&mut dyn SignalEmitter>,
3154) -> Result<Value, String> {
3155    let Value::Function {
3156        params,
3157        body,
3158        captured_env,
3159        heap,
3160    } = fn_val
3161    else {
3162        return Err(format!("eval_mms_fn: expected Function, got {:?}", fn_val));
3163    };
3164    let mut emits: Vec<IntentValue> = Vec::new();
3165    let mut world = ObjectWorld::with_heap(heap.clone());
3166    world.push_function_frame(captured_env.clone());
3167    for (index, param) in params.iter().enumerate() {
3168        let arg = args.get(index).cloned().unwrap_or(Value::Null);
3169        world.bind(param.clone(), arg);
3170    }
3171    let mut ctx = EvalContext {
3172        emits: &mut emits,
3173        source_path: None,
3174        channels,
3175        ce_builder: None,
3176        object_world: &mut world,
3177        host_world: world_host.map(|world| world as *mut World),
3178        exec_scope: None,
3179        runtime_closure_mode: RuntimeClosureExecMode::Full,
3180        implicit_cwd: None,
3181    };
3182    let live_emit = emit.as_mut().map(|em| unsafe {
3183        std::mem::transmute::<&mut dyn SignalEmitter, *mut dyn SignalEmitter>(&mut **em)
3184    });
3185    let result = with_live_signal_emitter(live_emit, || {
3186        match eval_block_stmts(&body.statements, &mut ctx)? {
3187            StmtEffect::Return(val) => Ok::<Value, String>(val),
3188            _ => Ok::<Value, String>(Value::Null),
3189        }
3190    })?;
3191    if let Some(em) = emit {
3192        for iv in emits {
3193            em.push_intent_now(ComponentId::default(), iv);
3194        }
3195    }
3196    Ok(result)
3197}
3198
3199pub(crate) fn eval_runtime_closure(
3200    closure: &RuntimeClosure,
3201    channels: Option<&mut EvalChannels>,
3202    world_host: Option<&mut World>,
3203    mut emit: Option<&mut dyn SignalEmitter>,
3204    exec_scope: Option<ComponentId>,
3205    mode: RuntimeClosureExecMode,
3206) -> Result<(), String> {
3207    let mut emits: Vec<IntentValue> = Vec::new();
3208    let mut world = ObjectWorld::with_heap(closure.heap.clone());
3209    world.push_function_frame(closure.captured_env.clone());
3210    let mut ctx = EvalContext {
3211        emits: &mut emits,
3212        source_path: None,
3213        channels,
3214        ce_builder: None,
3215        object_world: &mut world,
3216        host_world: world_host.map(|world| world as *mut World),
3217        exec_scope,
3218        runtime_closure_mode: mode,
3219        implicit_cwd: None,
3220    };
3221    let live_emit = emit.as_mut().map(|em| unsafe {
3222        std::mem::transmute::<&mut dyn SignalEmitter, *mut dyn SignalEmitter>(&mut **em)
3223    });
3224    with_live_signal_emitter(live_emit, || {
3225        let _ = eval_block_stmts(&closure.body.statements, &mut ctx)?;
3226        Ok::<(), String>(())
3227    })?;
3228    if let Some(em) = emit {
3229        for iv in emits {
3230            em.push_intent_now(ComponentId::default(), iv);
3231        }
3232    }
3233    Ok(())
3234}
3235
3236/// Evaluate a source file as a module (sandboxed — emits go to `sequence`, not the engine).
3237/// Returns `Value::Module { named, sequence }`.
3238pub(crate) fn eval_module_source(source: &str, source_path: Option<&str>) -> Result<Value, String> {
3239    let mut stmts = parse_source(source)?;
3240    EmitLiftTransform::apply(&mut stmts);
3241    QueryDesugarTransform::apply(&mut stmts);
3242
3243    let mut emits: Vec<IntentValue> = Vec::new();
3244    let mut named: HashMap<String, Value> = HashMap::new();
3245    let mut world = ObjectWorld::new();
3246    let mut ctx = EvalContext {
3247        emits: &mut emits,
3248        source_path,
3249        channels: None,
3250        ce_builder: None,
3251        object_world: &mut world,
3252        host_world: None,
3253        exec_scope: None,
3254        runtime_closure_mode: RuntimeClosureExecMode::Full,
3255        implicit_cwd: None,
3256    };
3257
3258    for stmt in &stmts {
3259        match eval_stmt(stmt, &mut ctx)? {
3260            StmtEffect::Exported(name) => {
3261                // The binding is already in object_world; copy it into the
3262                // module's named-exports map.
3263                if let Some(val) = ctx.object_world.lookup(&name).cloned() {
3264                    named.insert(name, val);
3265                }
3266            }
3267            StmtEffect::None => {}
3268            StmtEffect::Return(_) | StmtEffect::Break | StmtEffect::Continue => {}
3269        }
3270    }
3271
3272    let sequence: Vec<MaterializedCE> = emits
3273        .into_iter()
3274        .filter_map(|iv| match iv {
3275            IntentValue::SpawnComponentTree { root, .. } => Some(*root),
3276            _ => None,
3277        })
3278        .collect();
3279
3280    Ok(Value::Module {
3281        named,
3282        sequence,
3283        heap: world.heap().clone(),
3284    })
3285}
3286
3287fn resolve_import_path(path: &str, source_path: Option<&str>) -> String {
3288    if let Some(src) = source_path {
3289        if let Some(parent) = std::path::Path::new(src).parent() {
3290            return parent.join(path).to_string_lossy().into_owned();
3291        }
3292    }
3293    path.to_string()
3294}
3295
3296fn parse_source(source: &str) -> Result<Vec<Statement>, String> {
3297    let tokens = MeowMeowTokenizer::new(source)
3298        .tokenize()
3299        .map_err(|e| tokenize_err_to_string(source, e))?;
3300    MeowMeowParser::new(tokens)
3301        .parse_program()
3302        .map_err(|e| parse_err_to_string(source, e))
3303}
3304
3305fn parse_only(source: &str) -> Result<String, String> {
3306    let stmts = parse_source(source)?;
3307    Ok(format!("{stmts:#?}"))
3308}
3309
3310fn byte_offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
3311    let offset = offset.min(source.len());
3312    let before = &source[..offset];
3313    let line = before.bytes().filter(|&b| b == b'\n').count() + 1;
3314    let col = before.rfind('\n').map(|p| offset - p).unwrap_or(offset + 1);
3315    (line, col)
3316}
3317
3318fn format_source_context(source: &str, line: usize, col: usize) -> String {
3319    let line_text = source.lines().nth(line.saturating_sub(1)).unwrap_or("");
3320    let caret_pad = " ".repeat(col.saturating_sub(1));
3321    format!("\n  {line_text}\n  {caret_pad}^")
3322}
3323
3324fn tokenize_err_to_string(source: &str, e: TokenizeError) -> String {
3325    let (line, col) = byte_offset_to_line_col(source, e.span.start);
3326    format!(
3327        "tokenize error at {}:{}: {}{}",
3328        line,
3329        col,
3330        e.message,
3331        format_source_context(source, line, col),
3332    )
3333}
3334
3335fn parse_err_to_string(source: &str, e: ParseError) -> String {
3336    let (line, col) = byte_offset_to_line_col(source, e.span.start);
3337    format!(
3338        "parse error at {}:{}: {}{}",
3339        line,
3340        col,
3341        e.message,
3342        format_source_context(source, line, col),
3343    )
3344}
3345
3346#[cfg(test)]
3347mod tests {
3348    use super::{
3349        EvalRequest, EvalResponse, HostValue, MeowMeowEvaluator, MeowMeowEvaluatorHandle,
3350        eval_module_source,
3351    };
3352    use crate::scripting::object::Value;
3353
3354    #[test]
3355    fn component_body_named_props_can_reference_same_named_bindings() {
3356        let source = r#"
3357fn make_payload(row_name, label, mode_value) {
3358    return Data {
3359        row_name = row_name
3360        label = label
3361        mode_value = mode_value
3362        row_kind = "EditorMode"
3363        interactive = true
3364    }
3365}
3366
3367export let payload = make_payload("editor_settings_mode_cursor_3d", "3D Cursor", "cursor_3d")
3368"#;
3369
3370        let module = eval_module_source(source, None).expect("module eval");
3371        let Value::Module { named, .. } = module else {
3372            panic!("expected module value");
3373        };
3374        let payload = named.get("payload").expect("exported payload");
3375        let Value::ComponentExpr(component) = payload else {
3376            panic!("expected component expr");
3377        };
3378
3379        assert!(component.named.iter().any(|(key, value)| {
3380            key == "row_name"
3381                && matches!(value, Value::String(value) if value == "editor_settings_mode_cursor_3d")
3382        }));
3383        assert!(component.named.iter().any(|(key, value)| {
3384            key == "label" && matches!(value, Value::String(value) if value == "3D Cursor")
3385        }));
3386        assert!(component.named.iter().any(|(key, value)| {
3387            key == "mode_value" && matches!(value, Value::String(value) if value == "cursor_3d")
3388        }));
3389        assert!(component.component_property_assignment_only);
3390    }
3391
3392    #[test]
3393    fn structural_component_bodies_keep_normal_reassign_semantics() {
3394        let source = r#"
3395fn rename_label() {
3396    let label = "hello"
3397    return T {
3398        label = "goodbye"
3399        Text { label }
3400    }
3401}
3402
3403export let payload = rename_label()
3404"#;
3405
3406        let module = eval_module_source(source, None).expect("module eval");
3407        let Value::Module { named, .. } = module else {
3408            panic!("expected module value");
3409        };
3410        let payload = named.get("payload").expect("exported payload");
3411        let Value::ComponentExpr(component) = payload else {
3412            panic!("expected component expr");
3413        };
3414
3415        assert!(!component.component_property_assignment_only);
3416        assert!(component.named.is_empty());
3417        let Some(crate::scripting::object::CeChild::Spawn(text_child)) = component.children.first()
3418        else {
3419            panic!("expected text child");
3420        };
3421        assert!(matches!(
3422            text_child.positionals.first(),
3423            Some(Value::String(label)) if label == "goodbye"
3424        ));
3425    }
3426
3427    #[test]
3428    fn structural_component_bodies_still_capture_universal_named_props() {
3429        let source = r#"
3430export let payload = T {
3431    name = "paint_panel_root"
3432    id = "paint_panel_root"
3433    Text { "hello" }
3434}
3435"#;
3436
3437        let module = eval_module_source(source, None).expect("module eval");
3438        let Value::Module { named, .. } = module else {
3439            panic!("expected module value");
3440        };
3441        let payload = named.get("payload").expect("exported payload");
3442        let Value::ComponentExpr(component) = payload else {
3443            panic!("expected component expr");
3444        };
3445
3446        assert!(!component.component_property_assignment_only);
3447        assert!(component.named.iter().any(|(key, value)| {
3448            key == "name" && matches!(value, Value::String(value) if value == "paint_panel_root")
3449        }));
3450        assert!(component.named.iter().any(|(key, value)| {
3451            key == "id" && matches!(value, Value::String(value) if value == "paint_panel_root")
3452        }));
3453    }
3454
3455    #[test]
3456    fn module_eval_supports_table_literals_and_field_access() {
3457        let source = r#"
3458export let label = {
3459    theme = {
3460        label = "aurora"
3461    }
3462}.theme.label
3463"#;
3464
3465        let module = eval_module_source(source, None).expect("module eval");
3466        let Value::Module { named, .. } = module else {
3467            panic!("expected module value");
3468        };
3469        assert!(matches!(
3470            named.get("label"),
3471            Some(Value::String(label)) if label == "aurora"
3472        ));
3473    }
3474
3475    #[test]
3476    fn module_eval_supports_for_in_over_tables() {
3477        let source = r#"
3478let total = 0
3479for entry in {
3480    apples = 2
3481    pears = 5
3482} {
3483    total = total + entry.value
3484}
3485export let total = total
3486"#;
3487
3488        let module = eval_module_source(source, None).expect("module eval");
3489        let Value::Module { named, .. } = module else {
3490            panic!("expected module value");
3491        };
3492        assert!(
3493            matches!(named.get("total"), Some(Value::Number(total)) if (*total - 7.0).abs() < 1e-6)
3494        );
3495    }
3496
3497    fn run_test_snippet(
3498        handle: &mut MeowMeowEvaluatorHandle,
3499        source: &str,
3500    ) -> Result<Option<Value>, String> {
3501        run_test_snippet_with_cwd(handle, source, None)
3502    }
3503
3504    fn run_test_snippet_with_cwd(
3505        handle: &mut MeowMeowEvaluatorHandle,
3506        source: &str,
3507        cwd: Option<Value>,
3508    ) -> Result<Option<Value>, String> {
3509        handle
3510            .requests
3511            .push(EvalRequest::EvalSnippet {
3512                source: source.into(),
3513                cwd,
3514            })
3515            .unwrap();
3516        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(1);
3517        loop {
3518            match handle.responses.pop() {
3519                Ok(EvalResponse::SnippetComplete { result }) => return result,
3520                Ok(EvalResponse::HostCall { kind, .. }) => {
3521                    panic!("navigation unexpectedly requested a host call: {kind:?}")
3522                }
3523                Ok(_) | Err(rtrb::PopError::Empty) if std::time::Instant::now() < deadline => {
3524                    std::thread::yield_now()
3525                }
3526                other => panic!("snippet timed out or returned unexpected response: {other:?}"),
3527            }
3528        }
3529    }
3530
3531    fn run_test_navigation(
3532        handle: &mut MeowMeowEvaluatorHandle,
3533        source: &str,
3534        cwd: Option<Value>,
3535    ) -> Result<Value, String> {
3536        handle
3537            .requests
3538            .push(EvalRequest::EvalNavigation {
3539                source: source.into(),
3540                cwd,
3541            })
3542            .unwrap();
3543        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(1);
3544        loop {
3545            match handle.responses.pop() {
3546                Ok(EvalResponse::NavigationComplete { result }) => return result,
3547                Ok(EvalResponse::HostCall { id, .. }) => {
3548                    handle
3549                        .requests
3550                        .push(EvalRequest::HostCallResult {
3551                            id,
3552                            value: HostValue::Null,
3553                        })
3554                        .unwrap();
3555                }
3556                Ok(_) | Err(rtrb::PopError::Empty) if std::time::Instant::now() < deadline => {
3557                    std::thread::yield_now()
3558                }
3559                other => panic!("navigation timed out or returned unexpected response: {other:?}"),
3560            }
3561        }
3562    }
3563
3564    #[test]
3565    fn repl_snippets_persist_echo_reset_and_recover() {
3566        let mut handle = MeowMeowEvaluator::spawn(32);
3567        assert_eq!(
3568            run_test_snippet(&mut handle, "let answer = 42").unwrap(),
3569            None
3570        );
3571        assert_eq!(
3572            run_test_snippet(&mut handle, "answer").unwrap(),
3573            Some(Value::Number(42.0))
3574        );
3575        assert!(run_test_snippet(&mut handle, "1 + true").is_err());
3576        assert_eq!(
3577            run_test_snippet(&mut handle, "answer").unwrap(),
3578            Some(Value::Number(42.0))
3579        );
3580        assert_eq!(
3581            run_test_snippet(&mut handle, "reset()\nanswer").unwrap(),
3582            Some(Value::Number(42.0))
3583        );
3584        assert!(run_test_snippet(&mut handle, "answer + 1").is_err());
3585        handle.shutdown_and_join();
3586    }
3587
3588    #[test]
3589    fn repl_navigation_is_detached_and_supplies_dynamic_table_context() {
3590        let mut handle = MeowMeowEvaluator::spawn(32);
3591        run_test_snippet(
3592            &mut handle,
3593            r#"let settings = { theme = { tone = "dark" } }"#,
3594        )
3595        .unwrap();
3596        let settings = run_test_navigation(&mut handle, "settings", None).unwrap();
3597        let theme = run_test_navigation(&mut handle, "settings.theme", None).unwrap();
3598        assert_eq!(
3599            run_test_snippet_with_cwd(&mut handle, "tone", Some(theme.clone())).unwrap(),
3600            Some(Value::String("dark".into()))
3601        );
3602        run_test_snippet_with_cwd(&mut handle, r#"tone = "light""#, Some(theme.clone())).unwrap();
3603        assert_eq!(
3604            run_test_navigation(&mut handle, "settings.theme.tone", Some(settings)).unwrap(),
3605            Value::String("light".into())
3606        );
3607
3608        run_test_snippet(&mut handle, "let read_tone = fn() { return tone }").unwrap();
3609        assert_eq!(
3610            run_test_snippet_with_cwd(&mut handle, "read_tone()", Some(theme.clone())).unwrap(),
3611            Some(Value::String("light".into()))
3612        );
3613        run_test_snippet_with_cwd(
3614            &mut handle,
3615            r#"let tone = "lexical"
3616tone = "shadowed""#,
3617            Some(theme.clone()),
3618        )
3619        .unwrap();
3620        assert_eq!(
3621            run_test_snippet_with_cwd(&mut handle, "tone", Some(theme.clone())).unwrap(),
3622            Some(Value::String("shadowed".into()))
3623        );
3624        assert_eq!(
3625            run_test_navigation(&mut handle, "settings.theme.tone", None).unwrap(),
3626            Value::String("light".into())
3627        );
3628
3629        let detached = run_test_navigation(&mut handle, r#"T { Text { "x" } }"#, None).unwrap();
3630        assert!(matches!(detached, Value::ComponentExpr(_)));
3631        run_test_snippet(
3632            &mut handle,
3633            "let make_detached = fn() { let node = T {} return node }",
3634        )
3635        .unwrap();
3636        let detached = run_test_navigation(&mut handle, "make_detached()", None).unwrap();
3637        assert!(matches!(detached, Value::ComponentExpr(_)));
3638        handle.shutdown_and_join();
3639    }
3640}