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#[derive(Debug, Clone)]
39pub enum EvalRequest {
40 EvalScript {
43 source: String,
44 source_path: Option<String>,
45 },
46 EvalSnippet {
48 source: String,
49 cwd: Option<Value>,
52 },
53 EvalNavigation {
56 source: String,
57 cwd: Option<Value>,
58 },
59 ParseScript {
61 source: String,
62 },
63 HostCallResult {
66 id: u32,
67 value: HostValue,
68 },
69 Shutdown,
70}
71
72#[derive(Debug, Clone)]
73pub enum EvalResponse {
74 Intent(IntentValue),
76 ParsedOk {
78 debug_ast: String,
79 },
80 Error {
81 message: String,
82 },
83 SnippetComplete {
85 result: Result<Option<Value>, String>,
86 },
87 NavigationComplete {
88 result: Result<Value, String>,
89 },
90 ReplReset,
92 ShutdownAck,
93 HostCall {
98 id: u32,
99 kind: HostCallKind,
100 },
101}
102
103#[derive(Debug, Clone)]
105pub enum HostCallKind {
106 Spawn(MaterializedCE),
110 Register(MaterializedCE),
115 Attach {
119 parent: Option<ComponentId>,
120 child: ComponentId,
121 },
122 RegisterHandler {
125 scope: ComponentId,
126 signal_kind: SignalKind,
127 name: Option<String>,
128 handler: Value,
129 },
130 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 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#[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#[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
221fn 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 }
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
289fn 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 }
327 Ok(EvalRequest::Shutdown) => {
328 *shutdown_requested = true;
329 }
330 Ok(other) => {
331 let _ = other; }
335 Err(rtrb::PopError::Empty) => {
336 std::thread::yield_now();
337 }
338 }
339 }
340}
341
342struct EvalContext<'a> {
352 emits: &'a mut Vec<IntentValue>,
354 source_path: Option<&'a str>,
358 channels: Option<&'a mut EvalChannels>,
361 ce_builder: Option<&'a mut CeBuilder>,
364 object_world: &'a mut ObjectWorld,
367 host_world: Option<*mut World>,
370 exec_scope: Option<ComponentId>,
372 runtime_closure_mode: RuntimeClosureExecMode,
374 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
430struct CeBuilder {
433 component_property_assignment_only: bool,
435 calls: Vec<(String, Vec<Value>)>,
437 named: Vec<(String, Value)>,
439 positionals: Vec<Value>,
441 children: Vec<CeChild>,
444}
445
446pub 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 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
479fn 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 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 }; 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
532fn 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 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
672enum StmtEffect {
682 None,
683 Exported(String),
687 Return(Value),
688 Break,
689 Continue,
690}
691
692fn 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 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 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 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 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 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 let val = eval_expr(expr, ctx)?;
960 if ctx.ce_builder.is_some() {
961 match val {
962 Value::String(_) => ctx.ce_builder.as_mut().unwrap().positionals.push(val),
964 Value::ComponentExpr(ce) => ctx
966 .ce_builder
967 .as_mut()
968 .unwrap()
969 .children
970 .push(CeChild::Spawn(*ce)),
971 Value::ComponentObject { id, .. } => {
975 ctx.ce_builder
976 .as_mut()
977 .unwrap()
978 .children
979 .push(CeChild::Attach(id));
980 }
981 _ => {}
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 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
1209fn 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 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 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 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 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 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 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 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 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 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 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 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 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
1741fn 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 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
1823fn 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
1854fn 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
1896fn 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 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 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 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 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 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 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 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 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 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
2803fn 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
3141pub(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
3236pub(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 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}