use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::Arc;
use std::thread::{self, JoinHandle};
use rtrb::{Consumer, Producer, RingBuffer};
use crate::engine::ecs::ComponentId;
use crate::engine::ecs::IntentValue;
use crate::engine::ecs::SignalEmitter;
use crate::engine::ecs::SignalKind;
use crate::engine::ecs::World;
use crate::engine::ecs::component::{AnimationState, ComponentRef, MusicNote};
use crate::scripting::ast::{
BinOpKind, CallExpression, ComponentExpression, ElseBranch, Expression, IfStatement,
ImportItem, Statement, UnaryOpKind,
};
use crate::scripting::block_effect_analyzer::BlockEffectAnalyzer;
use crate::scripting::component_method_registry::{
invoke_component_method, supports_component_method,
};
use crate::scripting::component_registry::{
component_expr_uses_property_assignment_only, is_universal_component_named_prop,
};
use crate::scripting::object::{
BuiltinTableKind, CeChild, FrameKind, MaterializedCE, Object, ObjectWorld, RuntimeClosure,
Value,
};
use crate::scripting::parser::{MeowMeowParser, ParseError};
use crate::scripting::token::TokenizeError;
use crate::scripting::tokenizer::MeowMeowTokenizer;
use crate::scripting::transform::{EmitLiftTransform, QueryDesugarTransform};
#[derive(Debug, Clone)]
pub enum EvalRequest {
EvalScript {
source: String,
source_path: Option<String>,
},
EvalSnippet {
source: String,
cwd: Option<Value>,
},
EvalNavigation {
source: String,
cwd: Option<Value>,
},
ParseScript {
source: String,
},
HostCallResult {
id: u32,
value: HostValue,
},
Shutdown,
}
#[derive(Debug, Clone)]
pub enum EvalResponse {
Intent(IntentValue),
ParsedOk {
debug_ast: String,
},
Error {
message: String,
},
SnippetComplete {
result: Result<Option<Value>, String>,
},
NavigationComplete {
result: Result<Value, String>,
},
ReplReset,
ShutdownAck,
HostCall {
id: u32,
kind: HostCallKind,
},
}
#[derive(Debug, Clone)]
pub enum HostCallKind {
Spawn(MaterializedCE),
Register(MaterializedCE),
Attach {
parent: Option<ComponentId>,
child: ComponentId,
},
RegisterHandler {
scope: ComponentId,
signal_kind: SignalKind,
name: Option<String>,
handler: Value,
},
RegisterGlobalHandler {
signal_kind: SignalKind,
name: Option<String>,
handler: Value,
},
Query {
selector: String,
scope: Option<ComponentId>,
multiple: bool,
},
ReplTree {
value: Value,
max_depth: Option<usize>,
},
ReplDump {
value: Value,
},
ReplHelp,
ReplClear,
AudioClipInstance {
source: ComponentId,
start_beat: Option<f64>,
stop_beat: Option<f64>,
},
InvokeComponentMethod {
id: ComponentId,
component_type: String,
method: String,
args: Vec<Value>,
},
}
#[derive(Debug, Clone)]
pub enum HostValue {
ComponentId(ComponentId),
Component {
id: ComponentId,
component_type: String,
},
ComponentList(Vec<(ComponentId, String)>),
Value(Value),
Null,
}
#[derive(Debug)]
pub struct MeowMeowEvaluatorHandle {
pub requests: Producer<EvalRequest>,
pub responses: Consumer<EvalResponse>,
join: Option<JoinHandle<()>>,
}
impl MeowMeowEvaluatorHandle {
pub fn shutdown_and_join(mut self) {
let _ = self.requests.push(EvalRequest::Shutdown);
if let Some(j) = self.join.take() {
let _ = j.join();
}
}
}
pub struct MeowMeowEvaluator;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum RuntimeClosureExecMode {
Full,
KeyframeAudioOnly { beat_context: f64 },
KeyframeVisualOnly,
}
impl MeowMeowEvaluator {
pub fn spawn(queue_capacity: usize) -> MeowMeowEvaluatorHandle {
let (req_prod, req_cons) = RingBuffer::<EvalRequest>::new(queue_capacity);
let (res_prod, res_cons) = RingBuffer::<EvalResponse>::new(queue_capacity);
let join = thread::spawn(move || evaluator_thread(req_cons, res_prod));
MeowMeowEvaluatorHandle {
requests: req_prod,
responses: res_cons,
join: Some(join),
}
}
}
fn evaluator_thread(requests: Consumer<EvalRequest>, responses: Producer<EvalResponse>) {
let mut ch = EvalChannels::new(requests, responses);
let mut session_world = ObjectWorld::new();
session_world.bind("world", Value::Identifier("__mms_world__".into()));
loop {
if ch.shutdown_requested {
while ch.responses.push(EvalResponse::ShutdownAck).is_err() {
std::thread::yield_now();
}
break;
}
match ch.requests.pop() {
Ok(EvalRequest::EvalScript {
source,
source_path,
}) => {
eval_script(&source, source_path.as_deref(), &mut ch);
}
Ok(EvalRequest::EvalSnippet { source, cwd }) => {
let result = eval_snippet(&source, cwd, &mut session_world, &mut ch);
while ch
.responses
.push(EvalResponse::SnippetComplete {
result: result.clone(),
})
.is_err()
{
std::thread::yield_now();
}
}
Ok(EvalRequest::EvalNavigation { source, cwd }) => {
let result = eval_navigation(&source, cwd, &mut session_world, &mut ch);
while ch
.responses
.push(EvalResponse::NavigationComplete {
result: result.clone(),
})
.is_err()
{
std::thread::yield_now();
}
}
Ok(EvalRequest::ParseScript { source }) => {
let resp = parse_only(&source)
.map(|dbg| EvalResponse::ParsedOk { debug_ast: dbg })
.unwrap_or_else(|msg| EvalResponse::Error { message: msg });
let _ = ch.responses.push(resp);
}
Ok(EvalRequest::HostCallResult { .. }) => {
}
Ok(EvalRequest::Shutdown) => {
let _ = ch.responses.push(EvalResponse::ShutdownAck);
break;
}
Err(rtrb::PopError::Empty) => {
std::thread::yield_now();
}
}
}
}
fn host_call(
id: u32,
kind: HostCallKind,
requests: &mut Consumer<EvalRequest>,
responses: &mut Producer<EvalResponse>,
shutdown_requested: &mut bool,
) -> Option<HostValue> {
while responses
.push(EvalResponse::HostCall {
id,
kind: kind.clone(),
})
.is_err()
{
std::thread::yield_now();
}
loop {
match requests.pop() {
Ok(EvalRequest::HostCallResult {
id: reply_id,
value,
}) if reply_id == id => {
return match value {
HostValue::Null => None,
v => Some(v),
};
}
Ok(EvalRequest::HostCallResult { .. }) => {
}
Ok(EvalRequest::Shutdown) => {
*shutdown_requested = true;
}
Ok(other) => {
let _ = other; }
Err(rtrb::PopError::Empty) => {
std::thread::yield_now();
}
}
}
}
struct EvalContext<'a> {
emits: &'a mut Vec<IntentValue>,
source_path: Option<&'a str>,
channels: Option<&'a mut EvalChannels>,
ce_builder: Option<&'a mut CeBuilder>,
object_world: &'a mut ObjectWorld,
host_world: Option<*mut World>,
exec_scope: Option<ComponentId>,
runtime_closure_mode: RuntimeClosureExecMode,
implicit_cwd: Option<Value>,
}
thread_local! {
static LIVE_SIGNAL_EMITTER: RefCell<Option<*mut dyn SignalEmitter>> = RefCell::new(None);
static NAVIGATION_EVAL_ACTIVE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
fn with_live_signal_emitter<R>(emit: Option<*mut dyn SignalEmitter>, f: impl FnOnce() -> R) -> R {
LIVE_SIGNAL_EMITTER.with(|slot| {
let prev = slot.replace(emit);
let result = f();
let _ = slot.replace(prev);
result
})
}
fn with_navigation_eval<R>(f: impl FnOnce() -> R) -> R {
NAVIGATION_EVAL_ACTIVE.with(|active| {
let previous = active.replace(true);
let result = f();
active.set(previous);
result
})
}
fn push_eval_intent(ctx: &mut EvalContext<'_>, mut intent: IntentValue) {
match ctx.runtime_closure_mode {
RuntimeClosureExecMode::Full => {}
RuntimeClosureExecMode::KeyframeAudioOnly { beat_context } => match &mut intent {
IntentValue::AudioSchedulePlay {
beat_context: signal_beat_context,
..
}
| IntentValue::OscillatorScheduleSetPitch {
beat_context: signal_beat_context,
..
} => {
*signal_beat_context = Some(beat_context);
}
_ => return,
},
RuntimeClosureExecMode::KeyframeVisualOnly => match intent {
IntentValue::AudioSchedulePlay { .. }
| IntentValue::OscillatorScheduleSetPitch { .. } => {
return;
}
_ => {}
},
}
ctx.emits.push(intent);
}
struct CeBuilder {
component_property_assignment_only: bool,
calls: Vec<(String, Vec<Value>)>,
named: Vec<(String, Value)>,
positionals: Vec<Value>,
children: Vec<CeChild>,
}
pub struct EvalChannels {
pub requests: Consumer<EvalRequest>,
pub responses: Producer<EvalResponse>,
next_id: u32,
shutdown_requested: bool,
}
impl EvalChannels {
pub fn new(requests: Consumer<EvalRequest>, responses: Producer<EvalResponse>) -> Self {
Self {
requests,
responses,
next_id: 0,
shutdown_requested: false,
}
}
pub fn call(&mut self, kind: HostCallKind) -> Option<HostValue> {
let id = self.next_id;
self.next_id = self.next_id.wrapping_add(1);
host_call(
id,
kind,
&mut self.requests,
&mut self.responses,
&mut self.shutdown_requested,
)
}
}
fn eval_script(source: &str, source_path: Option<&str>, ch: &mut EvalChannels) {
let mut stmts = match parse_source(source) {
Ok(s) => s,
Err(msg) => {
let _ = ch.responses.push(EvalResponse::Error { message: msg });
return;
}
};
EmitLiftTransform::apply(&mut stmts);
QueryDesugarTransform::apply(&mut stmts);
let mut emits: Vec<IntentValue> = Vec::new();
let mut world = ObjectWorld::new();
let eval_result = {
let mut ctx = EvalContext {
emits: &mut emits,
source_path,
channels: Some(ch),
ce_builder: None,
object_world: &mut world,
host_world: None,
exec_scope: None,
runtime_closure_mode: RuntimeClosureExecMode::Full,
implicit_cwd: None,
};
eval_block_stmts(&stmts, &mut ctx)
};
match eval_result {
Ok(_) => {}
Err(msg) => {
let _ = ch.responses.push(EvalResponse::Error { message: msg });
return;
}
}
for intent in emits {
while ch
.responses
.push(EvalResponse::Intent(intent.clone()))
.is_err()
{
std::thread::yield_now();
}
}
}
fn eval_snippet(
source: &str,
cwd: Option<Value>,
object_world: &mut ObjectWorld,
ch: &mut EvalChannels,
) -> Result<Option<Value>, String> {
let result = eval_snippet_inner(source, cwd, object_world, ch);
if object_world.take_reset_requested() {
*object_world = ObjectWorld::new();
object_world.bind("world", Value::Identifier("__mms_world__".into()));
while ch.responses.push(EvalResponse::ReplReset).is_err() {
std::thread::yield_now();
}
}
result
}
fn eval_snippet_inner(
source: &str,
cwd: Option<Value>,
object_world: &mut ObjectWorld,
ch: &mut EvalChannels,
) -> Result<Option<Value>, String> {
let mut stmts = parse_source(source)?;
QueryDesugarTransform::apply(&mut stmts);
let mut emits = Vec::new();
let mut last = None;
let count = stmts.len();
for (index, stmt) in stmts.iter().enumerate() {
let mut ctx = EvalContext {
emits: &mut emits,
source_path: None,
channels: Some(ch),
ce_builder: None,
object_world,
host_world: None,
exec_scope: None,
runtime_closure_mode: RuntimeClosureExecMode::Full,
implicit_cwd: cwd.clone(),
};
if let Statement::Expression(expr) = stmt {
let value = if let Expression::Call(call) = expr
&& matches!(call.callee.as_ref(), Expression::Identifier(id) if id.0 == "emit")
{
if let Some(arg) = call.args.first() {
let value = eval_expr(arg, &mut ctx)?;
push_component_emit(value, &mut ctx);
}
Value::Null
} else {
eval_expr(expr, &mut ctx)?
};
let value = match value {
Value::ComponentExpr(ce) => {
let component_type = ce.component_type.clone();
match ctx
.channels
.as_mut()
.and_then(|c| c.call(HostCallKind::Spawn(*ce)))
{
Some(HostValue::ComponentId(id)) => {
Value::ComponentObject { id, component_type }
}
_ => return Err("unable to spawn component expression".into()),
}
}
other => other,
};
if index + 1 == count {
last = Some(value);
}
} else {
match eval_stmt(stmt, &mut ctx)? {
StmtEffect::None | StmtEffect::Exported(_) => {}
StmtEffect::Return(_) => return Err("return cannot escape a REPL snippet".into()),
StmtEffect::Break | StmtEffect::Continue => {
return Err("break/continue cannot escape a REPL snippet".into());
}
}
}
flush_live_statement_emits(&mut ctx);
}
Ok(last)
}
fn eval_navigation(
source: &str,
cwd: Option<Value>,
object_world: &mut ObjectWorld,
ch: &mut EvalChannels,
) -> Result<Value, String> {
let mut stmts = parse_source(source)?;
QueryDesugarTransform::apply(&mut stmts);
let [Statement::Expression(expr)] = stmts.as_slice() else {
return Err("cd: expected one MMS expression".into());
};
let mut emits = Vec::new();
let mut ctx = EvalContext {
emits: &mut emits,
source_path: None,
channels: Some(ch),
ce_builder: None,
object_world,
host_world: None,
exec_scope: None,
runtime_closure_mode: RuntimeClosureExecMode::Full,
implicit_cwd: cwd,
};
with_navigation_eval(|| eval_expr(expr, &mut ctx))
}
fn flush_live_statement_emits(ctx: &mut EvalContext<'_>) {
if ctx.channels.is_none() || ctx.ce_builder.is_some() || ctx.emits.is_empty() {
return;
}
let mut deferred: Vec<IntentValue> = Vec::new();
for intent in std::mem::take(ctx.emits) {
match intent {
IntentValue::SpawnComponentTree { root, parent: None } => {
if let Some(ch) = ctx.channels.as_mut() {
let spawn_result = ch.call(HostCallKind::Spawn((*root).clone()));
if !matches!(spawn_result, Some(HostValue::ComponentId(_))) {
deferred.push(IntentValue::SpawnComponentTree { root, parent: None });
}
} else {
deferred.push(IntentValue::SpawnComponentTree { root, parent: None });
}
}
other => deferred.push(other),
}
}
*ctx.emits = deferred;
}
enum StmtEffect {
None,
Exported(String),
Return(Value),
Break,
Continue,
}
fn eval_block_stmts(stmts: &[Statement], ctx: &mut EvalContext<'_>) -> Result<StmtEffect, String> {
for stmt in stmts {
let effect = eval_stmt(stmt, ctx)?;
flush_live_statement_emits(ctx);
match effect {
StmtEffect::None => {}
StmtEffect::Exported(_) => {}
effect => return Ok(effect),
}
}
Ok(StmtEffect::None)
}
fn eval_stmt(stmt: &Statement, ctx: &mut EvalContext<'_>) -> Result<StmtEffect, String> {
match stmt {
Statement::Assignment(a) => {
if a.name.0 == "cwd" && ctx.implicit_cwd.is_some() {
return Err("cwd is reserved by the interactive REPL".into());
}
let val = eval_expr(&a.value, ctx)?;
let val = maybe_register_live_component_value(val, ctx);
ctx.object_world.bind(a.name.0.clone(), val);
if a.exported {
Ok(StmtEffect::Exported(a.name.0.clone()))
} else {
Ok(StmtEffect::None)
}
}
Statement::Reassign { target, value } => {
if matches!(target, Expression::Identifier(name) if name.0 == "cwd")
&& ctx.implicit_cwd.is_some()
{
return Err("cwd is reserved by the interactive REPL".into());
}
let val = eval_expr(value, ctx)?;
if let Some(builder) = ctx.ce_builder.as_mut() {
if let Expression::Identifier(name) = target {
if builder.component_property_assignment_only
|| is_universal_component_named_prop(&name.0)
{
builder.named.push((name.0.clone(), val));
return Ok(StmtEffect::None);
}
}
}
let val = maybe_register_live_component_value(val, ctx);
assign_retarget(target, val, ctx)?;
Ok(StmtEffect::None)
}
Statement::Expression(expr) => {
eval_expr_stmt(expr, ctx)?;
Ok(StmtEffect::None)
}
Statement::Return(r) => {
let val = match &r.value {
Some(expr) => eval_expr(expr, ctx)?,
None => Value::Null,
};
Ok(StmtEffect::Return(val))
}
Statement::If(if_stmt) => eval_if(if_stmt, ctx),
Statement::Block(block) => {
ctx.object_world.push_frame(FrameKind::Block);
let result = eval_block_stmts(&block.statements, ctx);
ctx.object_world.pop_frame();
result
}
Statement::Break => Ok(StmtEffect::Break),
Statement::Continue => Ok(StmtEffect::Continue),
Statement::ForIn {
binding,
iterable,
body,
} => {
let items = match eval_expr(iterable, ctx)? {
Value::Array(a) => a,
Value::Map(map) => map
.into_iter()
.map(|(key, value)| {
Value::Map(HashMap::from([
("key".to_string(), Value::String(key)),
("value".to_string(), value),
]))
})
.collect(),
Value::Object(id) => {
let Some(items) = id.with_map(|map| {
map.iter()
.map(|(key, value)| {
Value::Map(HashMap::from([
("key".to_string(), Value::String(key.clone())),
("value".to_string(), value.clone()),
]))
})
.collect::<Vec<_>>()
}) else {
return Err("for/in: invalid object".into());
};
items
}
other => return Err(format!("for/in: expected array, got {:?}", other)),
};
ctx.object_world.push_frame(FrameKind::Block);
let result: Result<StmtEffect, String> = (|| {
'for_loop: for item in items {
ctx.object_world.bind(binding.0.clone(), item);
for stmt in &body.statements {
match eval_stmt(stmt, ctx)? {
StmtEffect::None | StmtEffect::Exported(_) => {}
StmtEffect::Return(val) => return Ok(StmtEffect::Return(val)),
StmtEffect::Break => return Ok(StmtEffect::None),
StmtEffect::Continue => continue 'for_loop,
}
}
}
Ok(StmtEffect::None)
})();
ctx.object_world.pop_frame();
result
}
Statement::While { condition, body } => {
ctx.object_world.push_frame(FrameKind::Block);
let result: Result<StmtEffect, String> = (|| {
'while_loop: loop {
let cond = eval_expr(condition, ctx)?;
if !is_truthy(&cond) {
break;
}
for stmt in &body.statements {
match eval_stmt(stmt, ctx)? {
StmtEffect::None | StmtEffect::Exported(_) => {}
StmtEffect::Return(val) => return Ok(StmtEffect::Return(val)),
StmtEffect::Break => break 'while_loop,
StmtEffect::Continue => continue 'while_loop,
}
}
}
Ok(StmtEffect::None)
})();
ctx.object_world.pop_frame();
result
}
Statement::Import { items, path } => {
let resolved = resolve_import_path(path, ctx.source_path);
let content = std::fs::read_to_string(&resolved)
.map_err(|e| format!("import error: cannot read '{}': {}", path, e))?;
let module_val = eval_module_source(&content, Some(&resolved))?;
let (named, sequence) = match module_val {
Value::Module {
named, sequence, ..
} => (named, sequence),
_ => return Err("import: internal error".to_string()),
};
for item in items {
match item {
ImportItem::Named(id) => {
let val = named.get(&id.0).cloned().ok_or_else(|| {
format!("import: '{}' is not exported from '{}'", id.0, path)
})?;
ctx.object_world.bind(id.0.clone(), val);
}
ImportItem::NamedAlias { name, alias } => {
let val = named.get(&name.0).cloned().ok_or_else(|| {
format!("import: '{}' is not exported from '{}'", name.0, path)
})?;
ctx.object_world.bind(alias.0.clone(), val);
}
ImportItem::PositionalAlias { index, alias } => {
let ce = sequence.get(*index).ok_or_else(|| {
format!("import: index {} out of range in '{}'", index, path)
})?;
ctx.object_world
.bind(alias.0.clone(), Value::ComponentExpr(Box::new(ce.clone())));
}
}
}
Ok(StmtEffect::None)
}
}
}
fn maybe_register_live_component_value(val: Value, ctx: &mut EvalContext<'_>) -> Value {
if NAVIGATION_EVAL_ACTIVE.with(|active| active.get()) {
return val;
}
match val {
Value::ComponentExpr(ce) => {
let component_type = ce.component_type.clone();
if let Some(ch) = ctx.channels.as_mut() {
return match ch.call(HostCallKind::Register(*ce.clone())) {
Some(HostValue::ComponentId(id)) => {
Value::ComponentObject { id, component_type }
}
_ => Value::ComponentExpr(ce),
};
}
if let Some(world) = ctx.host_world {
let registered = LIVE_SIGNAL_EMITTER.with(|slot| {
let Some(host_emit) = *slot.borrow() else {
return None;
};
unsafe {
crate::scripting::component_registry::spawn_tree_uninitialized(
&ce,
&mut *world,
&mut *host_emit,
)
.ok()
}
});
if let Some(id) = registered {
return Value::ComponentObject { id, component_type };
}
}
Value::ComponentExpr(ce)
}
val => val,
}
}
fn eval_expr_stmt(expr: &Expression, ctx: &mut EvalContext<'_>) -> Result<(), String> {
if let Expression::Call(call) = expr {
if matches!(call.callee.as_ref(), Expression::Identifier(id) if id.0 == "emit") {
if let Some(arg) = call.args.first() {
let val = eval_expr(arg, ctx)?;
push_component_emit(val, ctx);
}
return Ok(());
}
if let Expression::Identifier(callee_id) = call.callee.as_ref() {
if ctx.ce_builder.is_some()
&& !ctx.object_world.has(&callee_id.0)
&& !is_builtin_fn(&callee_id.0)
{
let args: Vec<Value> = call
.args
.iter()
.map(|a| eval_expr(a, ctx))
.collect::<Result<_, _>>()?;
ctx.ce_builder
.as_mut()
.unwrap()
.calls
.push((callee_id.0.clone(), args));
return Ok(());
}
}
}
let val = eval_expr(expr, ctx)?;
if ctx.ce_builder.is_some() {
match val {
Value::String(_) => ctx.ce_builder.as_mut().unwrap().positionals.push(val),
Value::ComponentExpr(ce) => ctx
.ce_builder
.as_mut()
.unwrap()
.children
.push(CeChild::Spawn(*ce)),
Value::ComponentObject { id, .. } => {
ctx.ce_builder
.as_mut()
.unwrap()
.children
.push(CeChild::Attach(id));
}
_ => {}
}
} else {
push_component_emit(val, ctx);
}
Ok(())
}
fn push_component_emit(val: Value, ctx: &mut EvalContext<'_>) {
if NAVIGATION_EVAL_ACTIVE.with(|active| active.get()) {
return;
}
match val {
Value::ComponentExpr(ce) => {
push_eval_intent(
ctx,
IntentValue::SpawnComponentTree {
root: ce,
parent: None,
},
);
}
Value::ComponentObject { id, .. } => {
if let Some(ch) = ctx.channels.as_mut() {
ch.call(HostCallKind::Attach {
parent: None,
child: id,
});
}
}
_ => {}
}
}
fn value_to_component_ref_live(world: &World, value: &Value) -> Result<ComponentRef, String> {
match value {
Value::ComponentObject { id, .. } => {
let guid = world
.get_component_record(*id)
.map(|record| record.guid)
.ok_or_else(|| format!("component handle {id:?} not found in world"))?;
Ok(ComponentRef::Guid(guid))
}
Value::String(s) | Value::Identifier(s) => {
if let Some(hex) = s.strip_prefix("@uuid:") {
let uuid = uuid::Uuid::parse_str(hex)
.map_err(|e| format!("invalid uuid in '@uuid:{hex}': {e}"))?;
Ok(ComponentRef::Guid(uuid))
} else {
Ok(ComponentRef::Query(s.clone()))
}
}
other => Err(format!(
"expected component handle or selector string, got {other:?}"
)),
}
}
fn resolve_live_component_ref_global(world: &World, src: &ComponentRef) -> Option<ComponentId> {
match src {
ComponentRef::Guid(uuid) => world.component_id_by_guid(*uuid),
ComponentRef::Query(selector) => world
.world_roots()
.into_iter()
.find_map(|root| world.find_component(root, selector)),
}
}
fn assign_retarget(
target: &Expression,
value: Value,
ctx: &mut EvalContext<'_>,
) -> Result<(), String> {
match target {
Expression::Identifier(name) => {
if ctx.object_world.has(&name.0) {
return ctx.object_world.reassign(&name.0, value);
}
if let Some(cwd) = &ctx.implicit_cwd {
let updated = match cwd {
Value::Object(id) => id
.with_map_mut(|map| {
map.get_mut(&name.0).map(|field| *field = value.clone())
})
.flatten()
.is_some(),
_ => false,
};
if updated {
return Ok(());
}
}
ctx.object_world.reassign(&name.0, value)
}
Expression::BinaryOp {
op: BinOpKind::Dot, ..
} => {
let mut path = Vec::new();
let root_name = flatten_assign_path(target, &mut path)?;
let mut root_value = ctx
.object_world
.lookup(&root_name)
.cloned()
.ok_or_else(|| format!("reassignment: '{}' is not defined", root_name))?;
assign_into_value(&mut root_value, &path, value, ctx)?;
ctx.object_world.reassign(&root_name, root_value)
}
_ => Err("invalid reassignment target".into()),
}
}
fn flatten_assign_path(target: &Expression, out: &mut Vec<String>) -> Result<String, String> {
match target {
Expression::Identifier(name) => Ok(name.0.clone()),
Expression::BinaryOp {
op: BinOpKind::Dot,
lhs,
rhs,
} => {
let root = flatten_assign_path(lhs, out)?;
let Expression::Identifier(field) = rhs.as_ref() else {
return Err("invalid reassignment target".into());
};
out.push(field.0.clone());
Ok(root)
}
_ => Err("invalid reassignment target".into()),
}
}
fn assign_into_value(
current: &mut Value,
path: &[String],
value: Value,
ctx: &mut EvalContext<'_>,
) -> Result<(), String> {
let Some((field, rest)) = path.split_first() else {
*current = value;
return Ok(());
};
match current {
Value::Map(map) => {
if rest.is_empty() {
map.insert(field.clone(), value);
return Ok(());
}
let next = map
.get_mut(field)
.ok_or_else(|| format!("field assignment: '{}' not found", field))?;
assign_into_value(next, rest, value, ctx)
}
Value::Object(id) => {
if rest.is_empty() {
let Some(()) = id.with_map_mut(|map| {
map.insert(field.clone(), value);
}) else {
return Err("field assignment: invalid object".into());
};
return Ok(());
}
let Some(mut next) = id.with_map(|map| map.get(field).cloned()).flatten() else {
return Err(format!("field assignment: '{}' not found", field));
};
assign_into_value(&mut next, rest, value, ctx)?;
let Some(()) = id.with_map_mut(|map| {
map.insert(field.clone(), next);
}) else {
return Err("field assignment: invalid object".into());
};
Ok(())
}
other => Err(format!(
"field assignment: cannot assign through '{}': {:?}",
field, other
)),
}
}
fn is_builtin_fn(name: &str) -> bool {
matches!(
name,
"print"
| "assert"
| "range"
| "emit"
| "on"
| "query"
| "query_all"
| "emit_data"
| "tree"
| "dump"
| "help"
| "clear"
| "reset"
)
}
fn eval_if(if_stmt: &IfStatement, ctx: &mut EvalContext<'_>) -> Result<StmtEffect, String> {
let cond = eval_expr(&if_stmt.condition, ctx)?;
let branch = if is_truthy(&cond) {
Some(&if_stmt.then_branch)
} else {
None
};
if let Some(block) = branch {
ctx.object_world.push_frame(FrameKind::Block);
let result = eval_block_stmts(&block.statements, ctx);
ctx.object_world.pop_frame();
return result;
}
match &if_stmt.else_branch {
Some(ElseBranch::Block(block)) => {
ctx.object_world.push_frame(FrameKind::Block);
let result = eval_block_stmts(&block.statements, ctx);
ctx.object_world.pop_frame();
result
}
Some(ElseBranch::If(next_if)) => eval_if(next_if, ctx),
None => Ok(StmtEffect::None),
}
}
fn eval_ce(ce: &ComponentExpression, ctx: &mut EvalContext<'_>) -> Result<Value, String> {
let component_property_assignment_only =
component_expr_uses_property_assignment_only(&ce.component_type.0);
let is_keyframe = matches!(ce.component_type.0.as_str(), "KF" | "Keyframe");
let mut ctor_method: Option<String> = None;
let mut ctor_args: Vec<Value> = vec![];
let mut extra_ctor_calls: Vec<(String, Vec<Value>)> = vec![];
for (i, ctor) in ce.constructors.iter().enumerate() {
let args: Vec<Value> = ctor
.args
.iter()
.map(|a| eval_expr(a, ctx))
.collect::<Result<_, _>>()?;
if i == 0 {
ctor_method = Some(ctor.method.0.clone());
ctor_args = args;
} else {
extra_ctor_calls.push((ctor.method.0.clone(), args));
}
}
if is_keyframe {
let mce = MaterializedCE {
component_type: ce.component_type.0.clone(),
component_property_assignment_only,
ctor_method,
ctor_args,
calls: extra_ctor_calls,
named: vec![],
positionals: vec![],
deferred_block: Some(RuntimeClosure {
body: ce.body.clone(),
captured_env: Arc::new(ctx.object_world.snapshot_visible()),
heap: ctx.object_world.heap().clone(),
analysis: Some(BlockEffectAnalyzer::analyze_keyframe_block(&ce.body)),
}),
children: vec![],
};
return Ok(Value::ComponentExpr(Box::new(mce)));
}
let mut builder = CeBuilder {
component_property_assignment_only,
calls: extra_ctor_calls,
named: vec![],
positionals: vec![],
children: vec![],
};
ctx.object_world.push_frame(FrameKind::Block);
let body_result = {
let mut body_ctx = EvalContext {
emits: ctx.emits,
source_path: ctx.source_path,
channels: ctx.channels.as_mut().map(|c| &mut **c),
ce_builder: Some(&mut builder),
object_world: ctx.object_world,
host_world: ctx.host_world,
exec_scope: ctx.exec_scope,
runtime_closure_mode: ctx.runtime_closure_mode,
implicit_cwd: ctx.implicit_cwd.clone(),
};
eval_block_stmts(&ce.body.statements, &mut body_ctx)
};
ctx.object_world.pop_frame();
body_result?;
let mce = MaterializedCE {
component_type: ce.component_type.0.clone(),
component_property_assignment_only,
ctor_method,
ctor_args,
calls: builder.calls,
named: builder.named,
positionals: builder.positionals,
deferred_block: None,
children: builder.children,
};
Ok(Value::ComponentExpr(Box::new(mce)))
}
fn eval_expr(expr: &Expression, ctx: &mut EvalContext<'_>) -> Result<Value, String> {
match expr {
Expression::Null => Ok(Value::Null),
Expression::Bool(b) => Ok(Value::Bool(*b)),
Expression::Number(n) => Ok(Value::Number(*n)),
Expression::Dimension(n, unit) => Ok(Value::Dimension {
value: *n,
unit: *unit,
}),
Expression::String(s) => Ok(Value::String(s.clone())),
Expression::Array(items) => {
let vals = items
.iter()
.map(|e| eval_expr(e, ctx))
.collect::<Result<Vec<_>, _>>()?;
Ok(Value::Array(vals))
}
Expression::Table(fields) => {
let mut map = HashMap::with_capacity(fields.len());
for field in fields {
map.insert(field.name.0.clone(), eval_expr(&field.value, ctx)?);
}
Ok(Value::Object(
ctx.object_world.alloc_object(Object::Map(map)),
))
}
Expression::Index { base, index } => {
let base = eval_expr(base, ctx)?;
let index = eval_expr(index, ctx)?;
let Value::Array(items) = base else {
return Err(format!("index: expected array, got {:?}", base));
};
let Value::Number(n) = index else {
return Err(format!("index: expected numeric index, got {:?}", index));
};
if n.fract() != 0.0 || n < 0.0 {
return Err(format!("index: expected non-negative integer, got {n}"));
}
items
.get(n as usize)
.cloned()
.ok_or_else(|| format!("index: {n} out of bounds for array of {}", items.len()))
}
Expression::Identifier(id) => {
if id.0 == "cwd" {
return Ok(ctx
.implicit_cwd
.clone()
.unwrap_or_else(|| Value::Identifier("__mms_world__".into())));
}
match ctx.object_world.lookup(&id.0) {
Some(val) => Ok(val.clone()),
None => {
if let Some(value) = ctx.implicit_cwd.as_ref().and_then(|cwd| match cwd {
Value::Object(object) => {
object.with_map(|map| map.get(&id.0).cloned()).flatten()
}
Value::Map(map) => map.get(&id.0).cloned(),
_ => None,
}) {
return Ok(value);
}
match id.0.as_str() {
"Math" => Ok(Value::BuiltinTable(BuiltinTableKind::Math)),
"MusicNote" => Ok(Value::BuiltinTable(BuiltinTableKind::MusicNote)),
_ => Ok(Value::Identifier(id.0.clone())),
}
}
}
}
Expression::Component(ce) => eval_ce(ce, ctx),
Expression::Function { params, body } => {
let captured_env = ctx.object_world.snapshot_visible();
Ok(Value::Function {
params: params.iter().map(|p| p.0.clone()).collect(),
body: body.clone(),
captured_env: Arc::new(captured_env),
heap: ctx.object_world.heap().clone(),
})
}
Expression::Call(call) => eval_call(call, ctx),
Expression::BinaryOp { op, lhs, rhs } => eval_binop(op, lhs, rhs, ctx),
Expression::UnaryOp { op, operand } => eval_unaryop(op, operand, ctx),
}
}
fn eval_call(call: &CallExpression, ctx: &mut EvalContext<'_>) -> Result<Value, String> {
if let Expression::BinaryOp {
op: BinOpKind::Dot,
lhs,
rhs,
} = call.callee.as_ref()
{
let receiver = eval_expr(lhs, ctx)?;
let method_name = match rhs.as_ref() {
Expression::Identifier(id) => id.0.clone(),
other => {
return Err(format!(
"method call: RHS of '.' must be an identifier, got {:?}",
other
));
}
};
let args: Vec<Value> = call
.args
.iter()
.map(|a| eval_expr(a, ctx))
.collect::<Result<_, _>>()?;
return eval_method_call(receiver, &method_name, args, ctx);
}
let callee_name = match call.callee.as_ref() {
Expression::Identifier(id) => &id.0,
other => return Err(format!("cannot call {:?} as a function", other)),
};
if callee_name == "print" {
let arg = call
.args
.first()
.map(|a| eval_expr(a, ctx))
.transpose()?
.unwrap_or(Value::Null);
println!("[mms] {}", value_display(&arg));
return Ok(Value::Null);
}
if callee_name == "assert" {
let cond = call
.args
.first()
.map(|a| eval_expr(a, ctx))
.transpose()?
.unwrap_or(Value::Null);
if !is_truthy(&cond) {
let msg = call
.args
.get(1)
.map(|a| eval_expr(a, ctx))
.transpose()?
.unwrap_or(Value::String("assertion failed".into()));
return Err(format!("assert: {}", value_display(&msg)));
}
return Ok(Value::Null);
}
if callee_name == "range" {
let args: Vec<Value> = call
.args
.iter()
.map(|a| eval_expr(a, ctx))
.collect::<Result<_, _>>()?;
let (start, end) = match args.as_slice() {
[Value::Number(n)] => (0.0_f64, *n),
[Value::Number(s), Value::Number(e)] => (*s, *e),
_ => return Err("range() takes 1 or 2 numeric arguments".into()),
};
let count = ((end - start).max(0.0).floor()) as usize;
let arr = (0..count)
.map(|i| Value::Number(start + i as f64))
.collect();
return Ok(Value::Array(arr));
}
if matches!(
callee_name.as_str(),
"tree" | "dump" | "help" | "clear" | "reset"
) {
let args: Vec<Value> = call
.args
.iter()
.map(|a| eval_expr(a, ctx))
.collect::<Result<_, _>>()?;
match callee_name.as_str() {
"tree" => {
let value = args
.first()
.cloned()
.unwrap_or_else(|| Value::Identifier("__mms_world__".into()));
let max_depth = match args.get(1) {
Some(Value::Number(n)) if *n >= 0.0 => Some(*n as usize),
Some(other) => {
return Err(format!("tree(): max_depth must be a number, got {other:?}"));
}
None => None,
};
if let Some(ch) = ctx.channels.as_mut() {
ch.call(HostCallKind::ReplTree { value, max_depth });
}
}
"dump" => {
let value = args
.first()
.cloned()
.unwrap_or_else(|| Value::Identifier("__mms_world__".into()));
if let Some(ch) = ctx.channels.as_mut() {
ch.call(HostCallKind::ReplDump { value });
}
}
"help" => {
if let Some(ch) = ctx.channels.as_mut() {
ch.call(HostCallKind::ReplHelp);
}
}
"clear" => {
if let Some(ch) = ctx.channels.as_mut() {
ch.call(HostCallKind::ReplClear);
}
}
"reset" => {
ctx.object_world.request_reset();
}
_ => unreachable!(),
}
return Ok(Value::Null);
}
if callee_name == "query" || callee_name == "query_all" {
let multiple = callee_name == "query_all";
let args: Vec<Value> = call
.args
.iter()
.map(|a| eval_expr(a, ctx))
.collect::<Result<_, _>>()?;
let has_explicit_scope = args.first().is_some_and(|value| match value {
Value::ComponentObject { .. } => true,
Value::Identifier(value) => value == "__mms_world__",
_ => false,
});
if !has_explicit_scope && matches!(ctx.implicit_cwd, Some(Value::ComponentExpr(_))) {
return Err(format!(
"{}(): detached component expressions have no live query scope; emit it or pass world explicitly",
callee_name
));
}
let (scope, selector_index) = match args.first() {
Some(Value::ComponentObject { id, .. }) => (Some(*id), 1),
Some(Value::Identifier(s)) if s == "__mms_world__" => (None, 1),
_ => (
match &ctx.implicit_cwd {
Some(Value::ComponentObject { id, .. }) => Some(*id),
_ => None,
},
0,
),
};
let selector = match args.get(selector_index) {
Some(Value::String(s)) => s.clone(),
other => {
return Err(format!(
"{}(): selector must be a string, got {:?}",
callee_name, other
));
}
};
let handler = match args.get(selector_index + 1) {
Some(f @ Value::Function { .. }) => Some(f.clone()),
None => None,
other => {
return Err(format!(
"{}(): arg 1 (optional) must be a function, got {:?}",
callee_name, other
));
}
};
let result = run_world_query(selector, scope, multiple, ctx)?;
return dispatch_query_result(result, handler, multiple, ctx);
}
if callee_name == "on_global" {
let args: Vec<Value> = call
.args
.iter()
.map(|a| eval_expr(a, ctx))
.collect::<Result<_, _>>()?;
let signal_kind = match args.first() {
Some(Value::String(s)) => parse_signal_kind(s)?,
other => {
return Err(format!(
"on_global(): arg 0 must be a signal kind string, got {other:?}"
));
}
};
let (name, handler_idx) = match args.get(1) {
Some(Value::String(name)) => (Some(name.clone()), 2),
_ => (None, 1),
};
let handler = match args.get(handler_idx) {
Some(f @ Value::Function { .. }) => f.clone(),
other => {
return Err(format!(
"on_global(): arg {handler_idx} must be a function, got {other:?}"
));
}
};
if let Some(ch) = ctx.channels.as_mut() {
ch.call(HostCallKind::RegisterGlobalHandler {
signal_kind,
name,
handler,
});
}
return Ok(Value::Null);
}
if callee_name == "on" {
let args: Vec<Value> = call
.args
.iter()
.map(|a| eval_expr(a, ctx))
.collect::<Result<_, _>>()?;
let scope = match args.get(0) {
Some(Value::ComponentObject { id, .. }) => *id,
other => {
return Err(format!(
"on(): arg 0 must be a ComponentObject, got {:?}",
other
));
}
};
let signal_kind = match args.get(1) {
Some(Value::String(s)) => parse_signal_kind(s)?,
other => {
return Err(format!(
"on(): arg 1 must be a signal kind string, got {:?}",
other
));
}
};
let (name, handler_idx) = match args.get(2) {
Some(Value::String(name)) => (Some(name.clone()), 3),
_ => (None, 2),
};
let handler = match args.get(handler_idx) {
Some(f @ Value::Function { .. }) => f.clone(),
other => {
return Err(format!(
"on(): arg {} must be a function, got {:?}",
handler_idx, other
));
}
};
if let Some(ch) = ctx.channels.as_mut() {
ch.call(HostCallKind::RegisterHandler {
scope,
signal_kind,
name,
handler,
});
}
return Ok(Value::Null);
}
if callee_name == "emit_data" {
let args: Vec<Value> = call
.args
.iter()
.map(|a| eval_expr(a, ctx))
.collect::<Result<_, _>>()?;
let scope = match args.first() {
Some(Value::ComponentObject { id, .. }) => *id,
other => {
return Err(format!(
"emit_data(): arg 0 must be a ComponentObject, got {:?}",
other
));
}
};
let name = match args.get(1) {
Some(Value::String(name)) => name.clone(),
other => {
return Err(format!(
"emit_data(): arg 1 must be a string, got {:?}",
other
));
}
};
let payload = match args.get(2) {
Some(Value::ComponentObject { id, .. }) => Some(*id),
Some(Value::Null) | None => None,
other => {
return Err(format!(
"emit_data(): arg 2 must be a ComponentObject or null, got {:?}",
other
));
}
};
let emitted = LIVE_SIGNAL_EMITTER.with(|slot| {
let Some(host_emit) = *slot.borrow() else {
return false;
};
unsafe {
(&mut *host_emit).push_event(
scope,
crate::engine::ecs::EventSignal::DataEvent { name, payload },
);
}
true
});
if !emitted {
return Err("emit_data(): no live signal emitter".into());
}
return Ok(Value::Null);
}
let callee_val = match ctx.object_world.lookup(callee_name) {
Some(v) => v.clone(),
None => return Err(format!("undefined: '{}'", callee_name)),
};
match callee_val {
Value::Function {
params,
body,
captured_env,
..
} => {
let args: Vec<Value> = call
.args
.iter()
.map(|a| eval_expr(a, ctx))
.collect::<Result<_, _>>()?;
ctx.object_world.push_function_frame(captured_env);
for (index, param) in params.iter().enumerate() {
let arg = args.get(index).cloned().unwrap_or(Value::Null);
ctx.object_world.bind(param.clone(), arg);
}
let result = {
let mut func_ctx = EvalContext {
emits: ctx.emits,
source_path: None,
channels: ctx.channels.as_mut().map(|c| &mut **c),
ce_builder: None,
object_world: ctx.object_world,
host_world: ctx.host_world,
exec_scope: ctx.exec_scope,
runtime_closure_mode: ctx.runtime_closure_mode,
implicit_cwd: ctx.implicit_cwd.clone(),
};
eval_block_stmts(&body.statements, &mut func_ctx)
};
ctx.object_world.pop_frame();
match result? {
StmtEffect::Return(val) => Ok(val),
StmtEffect::None => Ok(Value::Null),
StmtEffect::Break | StmtEffect::Continue => {
Err("break/continue cannot escape a function body".into())
}
StmtEffect::Exported(_) => Ok(Value::Null),
}
}
other => Err(format!("cannot call {:?} as a function", other)),
}
}
fn run_world_query(
selector: String,
scope: Option<ComponentId>,
multiple: bool,
ctx: &mut EvalContext<'_>,
) -> Result<Vec<Value>, String> {
if let Some(ch) = ctx.channels.as_mut() {
let reply = ch.call(HostCallKind::Query {
selector,
scope,
multiple,
});
let out = match reply {
None => Vec::new(),
Some(HostValue::Component { id, component_type }) => {
vec![Value::ComponentObject { id, component_type }]
}
Some(HostValue::ComponentList(list)) => list
.into_iter()
.map(|(id, component_type)| Value::ComponentObject { id, component_type })
.collect(),
Some(other) => {
return Err(format!("query: unexpected HostValue reply: {:?}", other));
}
};
return Ok(out);
}
let Some(world) = ctx.host_world else {
return Ok(Vec::new());
};
let world = unsafe { &mut *world };
let roots: Vec<ComponentId> = match scope {
Some(id) => world.scripting_query_roots(id),
None => world
.all_components()
.filter(|&id| world.parent_of(id).is_none())
.collect(),
};
let mut all_ids: Vec<ComponentId> = Vec::new();
for root in roots {
if multiple {
all_ids.extend(world.find_all_components(root, &selector));
} else if let Some(found) = world.find_component(root, &selector) {
all_ids.push(found);
break;
}
}
let out = if multiple {
all_ids
.into_iter()
.filter_map(|id| {
world
.component_name(id)
.map(|component_type| Value::ComponentObject {
id,
component_type: component_type.to_string(),
})
})
.collect()
} else {
match all_ids.into_iter().next() {
Some(id) => match world.component_name(id) {
Some(component_type) => vec![Value::ComponentObject {
id,
component_type: component_type.to_string(),
}],
None => Vec::new(),
},
None => Vec::new(),
}
};
Ok(out)
}
fn dispatch_query_result(
mut matches: Vec<Value>,
handler: Option<Value>,
multiple: bool,
ctx: &mut EvalContext<'_>,
) -> Result<Value, String> {
if let Some(handler) = handler {
if multiple {
for m in matches {
eval_user_fn(&handler, vec![m], ctx)?;
}
} else {
let arg = matches.into_iter().next().unwrap_or(Value::Null);
eval_user_fn(&handler, vec![arg], ctx)?;
}
return Ok(Value::Null);
}
if multiple {
Ok(Value::Array(matches))
} else {
Ok(matches.drain(..).next().unwrap_or(Value::Null))
}
}
fn eval_user_fn(
handler: &Value,
args: Vec<Value>,
ctx: &mut EvalContext<'_>,
) -> Result<Value, String> {
let Value::Function {
params,
body,
captured_env,
..
} = handler
else {
return Err(format!("expected function, got {:?}", handler));
};
ctx.object_world.push_function_frame(captured_env.clone());
for (index, param) in params.iter().enumerate() {
let arg = args.get(index).cloned().unwrap_or(Value::Null);
ctx.object_world.bind(param.clone(), arg);
}
let result = {
let mut func_ctx = EvalContext {
emits: ctx.emits,
source_path: None,
channels: ctx.channels.as_mut().map(|c| &mut **c),
ce_builder: None,
object_world: ctx.object_world,
host_world: ctx.host_world,
exec_scope: ctx.exec_scope,
runtime_closure_mode: ctx.runtime_closure_mode,
implicit_cwd: ctx.implicit_cwd.clone(),
};
eval_block_stmts(&body.statements, &mut func_ctx)
};
ctx.object_world.pop_frame();
match result? {
StmtEffect::Return(val) => Ok(val),
_ => Ok(Value::Null),
}
}
fn eval_method_call(
receiver: Value,
method: &str,
args: Vec<Value>,
ctx: &mut EvalContext<'_>,
) -> Result<Value, String> {
match receiver {
Value::BuiltinTable(BuiltinTableKind::Math) => eval_math_method(
MathReceiverKind::BuiltinTable(BuiltinTableKind::Math),
method,
&args,
),
Value::BuiltinTable(BuiltinTableKind::MusicNote) => {
let pitch_ctor = match method {
"a" => MusicNote::a,
"b" => MusicNote::b,
"c" => MusicNote::c,
"d" => MusicNote::d,
"e" => MusicNote::e,
"f" => MusicNote::f,
"g" => MusicNote::g,
_ => return Err(format!("MusicNote: unknown note '{}'", method)),
};
let octave = match args.first() {
Some(Value::Number(n)) if n.is_finite() && *n >= 0.0 && n.fract() == 0.0 => {
*n as u16
}
other => {
return Err(format!(
"MusicNote.{}(): arg 0 must be a non-negative integer octave, got {:?}",
method, other
));
}
};
let duration_beats = match args.get(1) {
Some(Value::Number(n)) => *n as f32,
other => {
return Err(format!(
"MusicNote.{}(): arg 1 must be a numeric duration, got {:?}",
method, other
));
}
};
let mut note = pitch_ctor(octave, duration_beats);
if let Some(Value::Number(velocity)) = args.get(3) {
note = note.with_velocity(*velocity as f32);
}
let Some(world) = ctx.host_world else {
return Err(format!("MusicNote.{}(): no host world", method));
};
let world = unsafe { &mut *world };
let target_source = match args.get(2) {
Some(value) => Some(value_to_component_ref_live(world, value)?),
None => None,
};
let target = target_source
.as_ref()
.and_then(|src| resolve_live_component_ref_global(world, src))
.ok_or_else(|| {
format!(
"MusicNote.{}(): arg 2 must resolve to an audio target",
method
)
})?;
push_eval_intent(
ctx,
IntentValue::AudioSchedulePlay {
component_id: target,
beat_offset: 0.0,
beat_context: None,
note: Some(note),
gain: None,
rate: None,
duration: None,
},
);
Ok(Value::Null)
}
Value::ComponentObject {
id,
ref component_type,
} => {
if method == "query" || method == "query_all" {
let multiple = method == "query_all";
let selector = match args.first() {
Some(Value::String(s)) => s.clone(),
other => {
return Err(format!(
"{}(): arg 0 must be a string selector, got {:?}",
method, other
));
}
};
let handler = match args.get(1) {
Some(f @ Value::Function { .. }) => Some(f.clone()),
None => None,
other => {
return Err(format!(
"{}(): arg 1 (optional) must be a function, got {:?}",
method, other
));
}
};
let result = run_world_query(selector, Some(id), multiple, ctx)?;
return dispatch_query_result(result, handler, multiple, ctx);
}
if supports_component_method(component_type, method) {
if let Some(world) = ctx.host_world {
let world = unsafe { &mut *world };
return invoke_component_method(
world,
id,
component_type,
method,
&args,
|intent| push_eval_intent(ctx, intent),
);
}
if let Some(ch) = ctx.channels.as_mut() {
match ch.call(HostCallKind::InvokeComponentMethod {
id,
component_type: component_type.clone(),
method: method.to_string(),
args: args.clone(),
}) {
Some(HostValue::Null) | None => return Ok(Value::Null),
Some(HostValue::Component { id, component_type }) => {
return Ok(Value::ComponentObject { id, component_type });
}
Some(HostValue::ComponentId(component_id)) => {
return Ok(Value::ComponentObject {
id: component_id,
component_type: component_type.clone(),
});
}
Some(HostValue::Value(value)) => return Ok(value),
Some(other) => {
return Err(format!(
"InvokeComponentMethod returned unexpected value {:?}",
other
));
}
}
}
}
let anim_state = match method {
"play"
if matches!(
component_type.as_str(),
"A" | "Animation" | "AnimationComponent" | "animation"
) =>
{
Some(AnimationState::Playing)
}
"loop_anim"
if matches!(
component_type.as_str(),
"A" | "Animation" | "AnimationComponent" | "animation"
) =>
{
Some(AnimationState::Looping)
}
"pause"
if matches!(
component_type.as_str(),
"A" | "Animation" | "AnimationComponent" | "animation"
) =>
{
Some(AnimationState::Paused)
}
_ => None,
};
if let Some(state) = anim_state {
push_eval_intent(
ctx,
IntentValue::SetAnimationState {
component_id: id,
state,
},
);
return Ok(Value::Null);
}
if matches!(
component_type.as_str(),
"layout" | "LayoutRoot" | "LayoutComponent"
) && method == "available_width"
{
use crate::engine::ecs::system::layout::measure::layout_root_available_bounds;
let Some(world) = ctx.host_world else {
return Err("available_width(): no host world".into());
};
let world = unsafe { &mut *world };
let (w, _, _) = layout_root_available_bounds(world, id);
if world
.get_component_by_id_as::<crate::engine::ecs::component::LayoutComponent>(id)
.is_none()
{
return Err("available_width(): not a LayoutComponent".into());
}
let w = w as f64;
return Ok(Value::Number(w));
}
if matches!(
component_type.as_str(),
"layout" | "LayoutRoot" | "LayoutComponent"
) && method == "available_height"
{
use crate::engine::ecs::system::layout::measure::layout_root_available_bounds;
let Some(world) = ctx.host_world else {
return Err("available_height(): no host world".into());
};
let world = unsafe { &mut *world };
if world
.get_component_by_id_as::<crate::engine::ecs::component::LayoutComponent>(id)
.is_none()
{
return Err("available_height(): not a LayoutComponent".into());
}
let (_, h, _) = layout_root_available_bounds(world, id);
let h = h
.map(|value| value as f64)
.ok_or_else(|| "available_height(): height is unset".to_string())?;
return Ok(Value::Number(h));
}
if matches!(
component_type.as_str(),
"layout" | "LayoutRoot" | "LayoutComponent"
) && method == "set_available_width"
{
use crate::engine::ecs::component::style::SizeDimension;
use crate::scripting::token::Unit;
let width = match args.first() {
Some(Value::Number(n)) => SizeDimension::GlyphUnits(*n as f32),
Some(Value::Dimension {
value,
unit: Unit::GlyphUnits,
}) => SizeDimension::GlyphUnits(*value as f32),
Some(Value::Dimension {
value,
unit: Unit::WorldUnits,
}) => SizeDimension::WorldUnits(*value as f32),
Some(Value::Dimension { unit, .. }) => {
return Err(format!(
"set_available_width: expected gu or wu dimension, got {:?}",
unit
));
}
Some(other) => {
return Err(format!(
"set_available_width: expected number or dimension argument, got {:?}",
other
));
}
None => {
return Err(
"set_available_width: missing number or dimension argument".into()
);
}
};
push_eval_intent(
ctx,
IntentValue::SetLayoutAvailableWidth {
component_id: id,
width,
},
);
return Ok(Value::Null);
}
if matches!(
component_type.as_str(),
"layout" | "LayoutRoot" | "LayoutComponent"
) && method == "set_available_height"
{
use crate::engine::ecs::component::style::SizeDimension;
use crate::scripting::token::Unit;
let height = match args.first() {
Some(Value::Number(n)) => SizeDimension::GlyphUnits(*n as f32),
Some(Value::Dimension {
value,
unit: Unit::GlyphUnits,
}) => SizeDimension::GlyphUnits(*value as f32),
Some(Value::Dimension {
value,
unit: Unit::WorldUnits,
}) => SizeDimension::WorldUnits(*value as f32),
Some(Value::Dimension { unit, .. }) => {
return Err(format!(
"set_available_height: expected gu or wu dimension, got {:?}",
unit
));
}
Some(other) => {
return Err(format!(
"set_available_height: expected number or dimension argument, got {:?}",
other
));
}
None => {
return Err(
"set_available_height: missing number or dimension argument".into()
);
}
};
push_eval_intent(
ctx,
IntentValue::SetLayoutAvailableHeight {
component_id: id,
height,
},
);
return Ok(Value::Null);
}
if matches!(
component_type.as_str(),
"layout" | "LayoutRoot" | "LayoutComponent"
) && matches!(method, "set_inspect" | "enable_inspect" | "disable_inspect")
{
let enabled = match (method, args.first()) {
("enable_inspect", _) => true,
("disable_inspect", _) => false,
("set_inspect", Some(Value::Bool(b))) => *b,
("set_inspect", Some(other)) => {
return Err(format!(
"set_inspect: expected bool argument, got {:?}",
other
));
}
("set_inspect", None) => {
return Err("set_inspect: missing bool argument".into());
}
_ => unreachable!(),
};
push_eval_intent(
ctx,
IntentValue::SetLayoutInspect {
component_id: id,
enabled,
},
);
return Ok(Value::Null);
}
if matches!(
component_type.as_str(),
"Text" | "TXT" | "TextComponent" | "text"
) && method == "set_text"
{
let text = match args.first() {
Some(Value::String(s)) => s.clone(),
Some(other) => {
return Err(format!(
"set_text: expected string argument, got {:?}",
other
));
}
None => return Err("set_text: missing string argument".into()),
};
push_eval_intent(
ctx,
IntentValue::SetText {
component_id: id,
text,
},
);
return Ok(Value::Null);
}
if matches!(
component_type.as_str(),
"T" | "Transform" | "TransformComponent" | "transform"
) && method == "set_position"
{
let [x, y, z] = match args.as_slice() {
[Value::Number(x), Value::Number(y), Value::Number(z)] => {
[*x as f32, *y as f32, *z as f32]
}
other => {
return Err(format!(
"set_position: expected three numeric arguments, got {:?}",
other
));
}
};
let Some(world) = ctx.host_world else {
return Err("set_position(): no host world".into());
};
let world = unsafe { &mut *world };
let t = world
.get_component_by_id_as::<crate::engine::ecs::component::TransformComponent>(id)
.ok_or_else(|| "set_position(): not a TransformComponent".to_string())?;
let mut next = t.transform;
next.translation = [x, y, z];
next.recompute_model();
push_eval_intent(
ctx,
IntentValue::UpdateTransform {
component_id: id,
translation: next.translation,
rotation_quat_xyzw: next.rotation,
scale: next.scale,
},
);
return Ok(Value::Null);
}
if matches!(
component_type.as_str(),
"Camera3D" | "Camera3DComponent" | "camera3d" | "C3D"
) && matches!(method, "enabled" | "make_active_camera")
{
let Some(world) = ctx.host_world else {
return Err(format!("{method}(): no host world"));
};
let world = unsafe { &mut *world };
if method == "enabled" {
if args.is_empty() {
let enabled = world
.get_component_by_id_as::<crate::engine::ecs::component::Camera3DComponent>(id)
.ok_or_else(|| "enabled(): not a Camera3DComponent".to_string())?
.enabled;
return Ok(Value::Bool(enabled));
}
let enabled = match args.first() {
Some(Value::Bool(b)) => *b,
Some(other) => {
return Err(format!(
"enabled: expected bool argument, got {:?}",
other
));
}
None => unreachable!(),
};
let camera = world
.get_component_by_id_as_mut::<crate::engine::ecs::component::Camera3DComponent>(id)
.ok_or_else(|| "enabled(): not a Camera3DComponent".to_string())?;
camera.enabled = enabled;
return Ok(Value::Null);
}
push_eval_intent(ctx, IntentValue::MakeActiveCamera { component_id: id });
return Ok(Value::Null);
}
if matches!(
component_type.as_str(),
"CameraXR" | "CameraXRComponent" | "camera_xr" | "CXR"
) && matches!(method, "enabled" | "make_active_camera")
{
let Some(world) = ctx.host_world else {
return Err(format!("{method}(): no host world"));
};
let world = unsafe { &mut *world };
if method == "enabled" {
if args.is_empty() {
let enabled = world
.get_component_by_id_as::<crate::engine::ecs::component::CameraXRComponent>(id)
.ok_or_else(|| "enabled(): not a CameraXRComponent".to_string())?
.enabled;
return Ok(Value::Bool(enabled));
}
let enabled = match args.first() {
Some(Value::Bool(b)) => *b,
Some(other) => {
return Err(format!(
"enabled: expected bool argument, got {:?}",
other
));
}
None => unreachable!(),
};
let camera = world
.get_component_by_id_as_mut::<crate::engine::ecs::component::CameraXRComponent>(id)
.ok_or_else(|| "enabled(): not a CameraXRComponent".to_string())?;
camera.enabled = enabled;
return Ok(Value::Null);
}
push_eval_intent(ctx, IntentValue::MakeActiveCamera { component_id: id });
return Ok(Value::Null);
}
if matches!(
component_type.as_str(),
"Text" | "TXT" | "TextComponent" | "text"
) && method == "set_font_size"
{
let font_size = match args.first() {
Some(Value::Number(n)) => *n as f32,
Some(other) => {
return Err(format!(
"set_font_size: expected number argument, got {:?}",
other
));
}
None => return Err("set_font_size: missing number argument".into()),
};
let Some(world) = ctx.host_world else {
return Err("set_font_size(): no host world".into());
};
let world = unsafe { &mut *world };
let cur_text = world
.get_component_by_id_as::<crate::engine::ecs::component::TextComponent>(id)
.map(|t| t.text.clone())
.ok_or_else(|| "set_font_size(): not a TextComponent".to_string())?;
if let Some(t) = world
.get_component_by_id_as_mut::<crate::engine::ecs::component::TextComponent>(id)
{
t.set_font_size(font_size);
}
push_eval_intent(
ctx,
IntentValue::SetText {
component_id: id,
text: cur_text,
},
);
return Ok(Value::Null);
}
if matches!(
component_type.as_str(),
"ObserverRouter" | "signal_observer_router" | "SignalObserverRouterComponent"
) && matches!(method, "blacklist" | "whitelist" | "block" | "allow")
{
let Some(world) = ctx.host_world else {
return Err(format!("{method}(): no host world"));
};
let world = unsafe { &mut *world };
let router = world
.get_component_by_id_as_mut::<crate::engine::ecs::component::SignalObserverRouterComponent>(
id,
)
.ok_or_else(|| format!("{method}(): not a SignalObserverRouterComponent"))?;
match method {
"blacklist" | "whitelist" => {
let values = match args.first() {
Some(Value::Array(values)) => values,
Some(other) => {
return Err(format!(
"{method}(): expected array argument, got {:?}",
other
));
}
None => return Err(format!("{method}(): missing array argument")),
};
let mut out = Vec::with_capacity(values.len());
for value in values {
match value {
Value::String(s) => out.push(s.clone()),
other => {
return Err(format!(
"{method}(): expected string array, got {:?}",
other
));
}
}
}
match method {
"blacklist" => router.blacklist = out,
"whitelist" => router.whitelist = out,
_ => unreachable!(),
}
}
"block" | "allow" => {
let name = match args.first() {
Some(Value::String(name)) => name.clone(),
Some(other) => {
return Err(format!(
"{method}(): expected string argument, got {:?}",
other
));
}
None => return Err(format!("{method}(): missing string argument")),
};
if method == "block" {
if !router.blacklist.iter().any(|item| item == &name) {
router.blacklist.push(name);
}
} else {
router.blacklist.retain(|item| item != &name);
}
}
_ => unreachable!(),
}
return Ok(Value::Null);
}
if matches!(
component_type.as_str(),
"AudioClip" | "AudioClipComponent" | "audio_clip"
) && method == "instance"
{
let start_beat = match args.first() {
Some(Value::Number(n)) => Some(*n),
Some(Value::Null) | None => None,
Some(other) => {
return Err(format!(
"instance(): start_beat must be a number, got {:?}",
other
));
}
};
let stop_beat = match args.get(1) {
Some(Value::Number(n)) => Some(*n),
Some(Value::Null) | None => None,
Some(other) => {
return Err(format!(
"instance(): stop_beat must be a number, got {:?}",
other
));
}
};
let Some(ch) = ctx.channels.as_mut() else {
return Err("instance(): no host channel".into());
};
let new_id = match ch.call(HostCallKind::AudioClipInstance {
source: id,
start_beat,
stop_beat,
}) {
Some(HostValue::ComponentId(cid)) => cid,
_ => return Err("instance(): host AudioClipInstance failed".into()),
};
return Ok(Value::ComponentObject {
id: new_id,
component_type: "AudioClip".to_string(),
});
}
Err(format!(
"no method '{}' on component type '{}'",
method, component_type
))
}
other => Err(format!(
"method call '{}': receiver is not a ComponentObject, got {:?}",
method, other
)),
}
}
fn eval_binop(
op: &BinOpKind,
lhs: &Expression,
rhs: &Expression,
ctx: &mut EvalContext<'_>,
) -> Result<Value, String> {
match op {
BinOpKind::And => {
let l = eval_expr(lhs, ctx)?;
if !is_truthy(&l) {
return Ok(Value::Bool(false));
}
let r = eval_expr(rhs, ctx)?;
return Ok(Value::Bool(is_truthy(&r)));
}
BinOpKind::Or => {
let l = eval_expr(lhs, ctx)?;
if is_truthy(&l) {
return Ok(Value::Bool(true));
}
let r = eval_expr(rhs, ctx)?;
return Ok(Value::Bool(is_truthy(&r)));
}
BinOpKind::Query => {
return Err(
"query operator '->' was not desugared by QueryDesugarTransform".to_string(),
);
}
BinOpKind::Pipe => {
let lhs_val = eval_expr(lhs, ctx)?;
let rhs_val = eval_expr(rhs, ctx)?;
match rhs_val {
Value::Function {
params,
body,
captured_env,
..
} => {
ctx.object_world.push_function_frame(captured_env);
if let Some(param) = params.first() {
ctx.object_world.bind(param.clone(), lhs_val);
}
let result = {
let mut func_ctx = EvalContext {
emits: ctx.emits,
source_path: None,
channels: None,
ce_builder: None,
object_world: ctx.object_world,
host_world: None,
exec_scope: None,
runtime_closure_mode: RuntimeClosureExecMode::Full,
implicit_cwd: ctx.implicit_cwd.clone(),
};
eval_block_stmts(&body.statements, &mut func_ctx)
};
ctx.object_world.pop_frame();
match result? {
StmtEffect::Return(val) => return Ok(val),
_ => return Ok(Value::Null),
}
}
other => return Err(format!("pipe: RHS must be a function, got {:?}", other)),
}
}
BinOpKind::Dot => {
let lhs_val = eval_expr(lhs, ctx)?;
let Expression::Identifier(field) = rhs else {
return Err(format!(
"field access: RHS of '.' must be an identifier, got {:?}",
rhs
));
};
return match lhs_val {
Value::BuiltinTable(BuiltinTableKind::Math) => builtin_math_field(&field.0)
.ok_or_else(|| format!("field access: '{}' not found", field.0)),
Value::BuiltinTable(BuiltinTableKind::MusicNote) => match field.0.as_str() {
"a" | "b" | "c" | "d" | "e" | "f" | "g" => {
Ok(Value::Identifier(format!("MusicNote.{}", field.0)))
}
_ => Err(format!("field access: '{}' not found", field.0)),
},
Value::Map(fields) => fields
.get(&field.0)
.cloned()
.ok_or_else(|| format!("field access: '{}' not found", field.0)),
Value::Object(id) => match id.with_map(|fields| fields.get(&field.0).cloned()) {
Some(Some(value)) => Ok(value),
Some(None) => Err(format!("field access: '{}' not found", field.0)),
None => Err("field access: invalid object".into()),
},
other => Err(format!(
"field access: cannot read '{}' from {:?}",
field.0, other
)),
};
}
_ => {}
}
let l = eval_expr(lhs, ctx)?;
let r = eval_expr(rhs, ctx)?;
match op {
BinOpKind::Add => match (l, r) {
(Value::Number(a), Value::Number(b)) => Ok(Value::Number(a + b)),
(Value::Dimension { value: a, unit: au }, Value::Dimension { value: b, unit: bu }) => {
dimension_add(a, au, b, bu)
}
(Value::String(a), Value::String(b)) => Ok(Value::String(a + &b)),
(Value::String(a), r) => Ok(Value::String(a + &value_display(&r))),
(l, Value::String(b)) => Ok(Value::String(value_display(&l) + &b)),
(l, r) => Err(format!("type error: cannot add {:?} and {:?}", l, r)),
},
BinOpKind::Sub => match (l, r) {
(Value::Number(a), Value::Number(b)) => Ok(Value::Number(a - b)),
(Value::Dimension { value: a, unit: au }, Value::Dimension { value: b, unit: bu }) => {
dimension_sub(a, au, b, bu)
}
(l, r) => Err(format!("type error: cannot subtract {:?} from {:?}", r, l)),
},
BinOpKind::Mul => match (l, r) {
(Value::Number(a), Value::Number(b)) => Ok(Value::Number(a * b)),
(Value::Dimension { value, unit }, Value::Number(n))
| (Value::Number(n), Value::Dimension { value, unit }) => {
dimension_scale(value, unit, n)
}
(l, r) => Err(format!("type error: cannot multiply {:?} and {:?}", l, r)),
},
BinOpKind::Div => match (l, r) {
(Value::Number(a), Value::Number(b)) => {
if b == 0.0 {
return Err("division by zero".to_string());
}
Ok(Value::Number(a / b))
}
(Value::Dimension { value, unit }, Value::Number(n)) => {
if n == 0.0 {
return Err("division by zero".to_string());
}
dimension_scale(value, unit, 1.0 / n)
}
(l, r) => Err(format!("type error: cannot divide {:?} by {:?}", l, r)),
},
BinOpKind::Rem => match (l, r) {
(Value::Number(a), Value::Number(b)) => Ok(Value::Number(a % b)),
(l, r) => Err(format!("type error: cannot rem {:?} by {:?}", l, r)),
},
BinOpKind::Eq => Ok(Value::Bool(values_equal(&l, &r))),
BinOpKind::NotEq => Ok(Value::Bool(!values_equal(&l, &r))),
BinOpKind::Lt => num_cmp(l, r, |a, b| a < b),
BinOpKind::Gt => num_cmp(l, r, |a, b| a > b),
BinOpKind::LtEq => num_cmp(l, r, |a, b| a <= b),
BinOpKind::GtEq => num_cmp(l, r, |a, b| a >= b),
BinOpKind::And | BinOpKind::Or | BinOpKind::Pipe | BinOpKind::Query => {
unreachable!("handled above")
}
BinOpKind::Dot => unreachable!("handled above"),
}
}
fn eval_unaryop(
op: &UnaryOpKind,
operand: &Expression,
ctx: &mut EvalContext<'_>,
) -> Result<Value, String> {
let val = eval_expr(operand, ctx)?;
match op {
UnaryOpKind::Neg => match val {
Value::Number(n) => Ok(Value::Number(-n)),
Value::Dimension { value, unit } => Ok(Value::Dimension {
value: -value,
unit,
}),
v => Err(format!("type error: cannot negate {:?}", v)),
},
UnaryOpKind::Not => Ok(Value::Bool(!is_truthy(&val))),
}
}
fn dimension_add(
lhs: f64,
lhs_unit: crate::scripting::token::Unit,
rhs: f64,
rhs_unit: crate::scripting::token::Unit,
) -> Result<Value, String> {
use crate::scripting::token::Unit;
if lhs_unit != rhs_unit {
return Err(format!(
"type error: cannot add dimensions with different units {:?} and {:?}",
lhs_unit, rhs_unit
));
}
if lhs_unit == Unit::Percent {
return Err("type error: percent arithmetic requires a layout boundary".into());
}
Ok(Value::Dimension {
value: lhs + rhs,
unit: lhs_unit,
})
}
fn dimension_sub(
lhs: f64,
lhs_unit: crate::scripting::token::Unit,
rhs: f64,
rhs_unit: crate::scripting::token::Unit,
) -> Result<Value, String> {
dimension_add(lhs, lhs_unit, -rhs, rhs_unit)
}
fn dimension_scale(
value: f64,
unit: crate::scripting::token::Unit,
scale: f64,
) -> Result<Value, String> {
use crate::scripting::token::Unit;
if unit == Unit::Percent {
return Err("type error: percent arithmetic requires a layout boundary".into());
}
Ok(Value::Dimension {
value: value * scale,
unit,
})
}
fn value_as_f32(value: &Value) -> Result<f32, String> {
match value {
Value::Number(n) => Ok(*n as f32),
other => Err(format!("expected number, got {other:?}")),
}
}
fn value_as_f64(value: &Value) -> Result<f64, String> {
match value {
Value::Number(n) => Ok(*n),
other => Err(format!("expected number, got {other:?}")),
}
}
fn value_as_u16(value: &Value) -> Result<u16, String> {
match value {
Value::Number(n) if n.is_finite() && *n >= 0.0 && n.fract() == 0.0 => Ok(*n as u16),
other => Err(format!("expected non-negative integer, got {other:?}")),
}
}
fn value_display(val: &Value) -> String {
match val {
Value::Null => "null".into(),
Value::Bool(b) => b.to_string(),
Value::Number(n) => {
if n.fract() == 0.0 && n.abs() < 1e15 {
format!("{}", *n as i64)
} else {
n.to_string()
}
}
Value::String(s) => s.clone(),
Value::Array(arr) => format!(
"[{}]",
arr.iter().map(value_display).collect::<Vec<_>>().join(", ")
),
Value::Map(map) => format!(
"{{{}}}",
map.iter()
.map(|(key, value)| format!("{key}: {}", value_display(value)))
.collect::<Vec<_>>()
.join(", ")
),
Value::Function { .. } => "<fn>".into(),
Value::ComponentObject { id, component_type } => format!("<{}:{:?}>", component_type, id),
Value::ComponentExpr(_) => "<ce>".into(),
Value::Object(id) => id
.with_map(|map| {
format!(
"{{{}}}",
map.iter()
.map(|(key, value)| format!("{key}: {}", value_display(value)))
.collect::<Vec<_>>()
.join(", ")
)
})
.unwrap_or_else(|| "<object>".into()),
Value::Identifier(s) => s.clone(),
Value::BuiltinTable(BuiltinTableKind::Math) => "<builtin Math>".into(),
Value::BuiltinTable(BuiltinTableKind::MusicNote) => "<builtin MusicNote>".into(),
Value::Module { .. } => "<module>".into(),
Value::Dimension { value, unit } => {
let suffix = match unit {
crate::scripting::token::Unit::Percent => "%",
crate::scripting::token::Unit::GlyphUnits => "gu",
crate::scripting::token::Unit::WorldUnits => "wu",
crate::scripting::token::Unit::Degrees => "deg",
crate::scripting::token::Unit::Radians => "rad",
};
format!("{}{}", value, suffix)
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum MathReceiverKind {
BuiltinTable(BuiltinTableKind),
}
fn eval_math_method(
receiver: MathReceiverKind,
method: &str,
args: &[Value],
) -> Result<Value, String> {
let prefix = match receiver {
MathReceiverKind::BuiltinTable(BuiltinTableKind::Math) => "Math",
MathReceiverKind::BuiltinTable(other) => {
return Err(format!("unsupported math receiver: {:?}", other));
}
};
match method {
"sin" => Ok(Value::Number(math_arg1(prefix, method, args)?.sin())),
"cos" => Ok(Value::Number(math_arg1(prefix, method, args)?.cos())),
"sqrt" => Ok(Value::Number(math_arg1(prefix, method, args)?.sqrt())),
"tan" => Ok(Value::Number(math_arg1(prefix, method, args)?.tan())),
"atan" => Ok(Value::Number(math_arg1(prefix, method, args)?.atan())),
"atan2" => {
let (y, x) = math_arg2(prefix, method, args)?;
Ok(Value::Number(y.atan2(x)))
}
"floor" => Ok(Value::Number(math_arg1(prefix, method, args)?.floor())),
"ceil" => Ok(Value::Number(math_arg1(prefix, method, args)?.ceil())),
"round" => Ok(Value::Number(math_arg1(prefix, method, args)?.round())),
"abs" => Ok(Value::Number(math_arg1(prefix, method, args)?.abs())),
"perlin" => match args {
[Value::Number(x), Value::Number(y)] => {
Ok(Value::Number(crate::utils::math::perlin(*x, *y, None)))
}
[Value::Number(x), Value::Number(y), Value::Number(z)] => {
Ok(Value::Number(crate::utils::math::perlin(*x, *y, Some(*z))))
}
_ => Err(format!(
"{prefix}.{method}(): expected 2 or 3 numeric arguments, got {:?}",
args
)),
},
"clamp" => {
let (x, min, max) = math_arg3(prefix, method, args)?;
Ok(Value::Number(x.max(min).min(max)))
}
"smoothstep" => {
let (x, edge0, edge1) = math_arg3(prefix, method, args)?;
if (edge1 - edge0).abs() <= f64::EPSILON {
return Err(format!(
"{prefix}.{method}(): edge0 and edge1 must be distinct"
));
}
let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
Ok(Value::Number(t * t * (3.0 - 2.0 * t)))
}
"dot" => {
let (a, b) = math_array_arg2(prefix, method, args)?;
if a.len() != b.len() {
return Err(format!(
"{prefix}.{method}(): expected arrays of equal length, got {} and {}",
a.len(),
b.len()
));
}
Ok(Value::Number(
a.iter().zip(b.iter()).map(|(lhs, rhs)| lhs * rhs).sum(),
))
}
"cross" => {
let (a, b) = math_vec3_arg2(prefix, method, args)?;
let cross = crate::utils::math::vec3_cross(a, b);
Ok(Value::Array(
cross.into_iter().map(|n| Value::Number(n as f64)).collect(),
))
}
_ => Err(format!("{prefix}: unknown method '{method}'")),
}
}
fn math_arg1(prefix: &str, method: &str, args: &[Value]) -> Result<f64, String> {
match args {
[Value::Number(n)] => Ok(*n),
_ => Err(format!(
"{prefix}.{method}(): expected 1 numeric argument, got {:?}",
args
)),
}
}
fn math_arg2(prefix: &str, method: &str, args: &[Value]) -> Result<(f64, f64), String> {
match args {
[Value::Number(a), Value::Number(b)] => Ok((*a, *b)),
_ => Err(format!(
"{prefix}.{method}(): expected 2 numeric arguments, got {:?}",
args
)),
}
}
fn math_arg3(prefix: &str, method: &str, args: &[Value]) -> Result<(f64, f64, f64), String> {
match args {
[Value::Number(a), Value::Number(b), Value::Number(c)] => Ok((*a, *b, *c)),
_ => Err(format!(
"{prefix}.{method}(): expected 3 numeric arguments, got {:?}",
args
)),
}
}
fn math_array_arg2(
prefix: &str,
method: &str,
args: &[Value],
) -> Result<(Vec<f64>, Vec<f64>), String> {
match args {
[lhs, rhs] => Ok((
value_as_number_array(lhs)
.map_err(|err| format!("{prefix}.{method}(): arg 0 {err}"))?,
value_as_number_array(rhs)
.map_err(|err| format!("{prefix}.{method}(): arg 1 {err}"))?,
)),
_ => Err(format!(
"{prefix}.{method}(): expected 2 array arguments, got {:?}",
args
)),
}
}
fn math_vec3_arg2(
prefix: &str,
method: &str,
args: &[Value],
) -> Result<([f32; 3], [f32; 3]), String> {
match args {
[lhs, rhs] => Ok((
value_as_f32_array(lhs).map_err(|err| format!("{prefix}.{method}(): arg 0 {err}"))?,
value_as_f32_array(rhs).map_err(|err| format!("{prefix}.{method}(): arg 1 {err}"))?,
)),
_ => Err(format!(
"{prefix}.{method}(): expected 2 array arguments, got {:?}",
args
)),
}
}
fn builtin_math_field(field: &str) -> Option<Value> {
match field {
"pi" => Some(Value::Number(std::f64::consts::PI)),
"tau" => Some(Value::Number(std::f64::consts::TAU)),
"e" => Some(Value::Number(std::f64::consts::E)),
"sin" | "cos" | "sqrt" | "tan" | "atan" | "atan2" | "floor" | "ceil" | "round" | "abs"
| "perlin" | "dot" | "cross" | "clamp" | "smoothstep" => {
Some(Value::Identifier(format!("Math.{field}")))
}
_ => None,
}
}
fn value_as_f32_array<const N: usize>(value: &Value) -> Result<[f32; N], String> {
match value {
Value::Array(items) => {
if items.len() != N {
return Err(format!("expected array of {N}, got {}", items.len()));
}
let mut out = [0.0_f32; N];
for (index, item) in items.iter().enumerate() {
match item {
Value::Number(n) => out[index] = *n as f32,
other => {
return Err(format!(
"expected numeric array element at {index}, got {:?}",
other
));
}
}
}
Ok(out)
}
other => Err(format!("expected array, got {:?}", other)),
}
}
fn value_as_number_array(value: &Value) -> Result<Vec<f64>, String> {
match value {
Value::Array(items) => {
let mut out = Vec::with_capacity(items.len());
for (index, item) in items.iter().enumerate() {
match item {
Value::Number(n) => out.push(*n),
other => {
return Err(format!(
"expected numeric array element at {index}, got {:?}",
other
));
}
}
}
Ok(out)
}
other => Err(format!("expected array, got {:?}", other)),
}
}
fn is_truthy(val: &Value) -> bool {
match val {
Value::Bool(b) => *b,
Value::Null => false,
_ => true,
}
}
fn values_equal(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Null, Value::Null) => true,
(Value::Bool(a), Value::Bool(b)) => a == b,
(Value::Number(a), Value::Number(b)) => a == b,
(Value::String(a), Value::String(b)) => a == b,
_ => false,
}
}
fn num_cmp(l: Value, r: Value, f: impl Fn(f64, f64) -> bool) -> Result<Value, String> {
match (l, r) {
(Value::Number(a), Value::Number(b)) => Ok(Value::Bool(f(a, b))),
(l, r) => Err(format!("type error: cannot compare {:?} and {:?}", l, r)),
}
}
fn parse_signal_kind(s: &str) -> Result<SignalKind, String> {
match s {
"FrameTick" => Ok(SignalKind::FrameTick),
"GLTFInitialized" => Ok(SignalKind::GltfInitialized),
"Click" => Ok(SignalKind::Click),
"ToggleChanged" => Ok(SignalKind::ToggleChanged),
"DragStart" => Ok(SignalKind::DragStart),
"GrabStart" => Ok(SignalKind::GrabStart),
"GrabEnd" => Ok(SignalKind::GrabEnd),
"DragMove" => Ok(SignalKind::DragMove),
"DragEnd" => Ok(SignalKind::DragEnd),
"RayIntersected" => Ok(SignalKind::RayIntersected),
"ParentChanged" => Ok(SignalKind::ParentChanged),
"CollisionStarted" => Ok(SignalKind::CollisionStarted),
"CollisionEnded" => Ok(SignalKind::CollisionEnded),
"SelectionChanged" => Ok(SignalKind::SelectionChanged),
"SelectionAdded" => Ok(SignalKind::SelectionAdded),
"SelectionRemoved" => Ok(SignalKind::SelectionRemoved),
"SelectionCleared" => Ok(SignalKind::SelectionCleared),
"Scrolling" => Ok(SignalKind::Scrolling),
"DataEvent" => Ok(SignalKind::DataEvent),
"XrButtonDown" => Ok(SignalKind::XrButtonDown),
"XrButtonUp" => Ok(SignalKind::XrButtonUp),
"XrButtonChanged" => Ok(SignalKind::XrButtonChanged),
"XrAxisChanged" => Ok(SignalKind::XrAxisChanged),
"TextInputFocusChanged" => Ok(SignalKind::TextInputFocusChanged),
"TextInputChanged" => Ok(SignalKind::TextInputChanged),
"HttpRequest" => Ok(SignalKind::HttpRequest),
"HttpResponse" => Ok(SignalKind::HttpResponse),
"HttpError" => Ok(SignalKind::HttpError),
other => Err(format!("unknown signal kind: '{}'", other)),
}
}
pub(crate) fn eval_mms_fn(
fn_val: &Value,
args: Vec<Value>,
channels: Option<&mut EvalChannels>,
world_host: Option<&mut World>,
mut emit: Option<&mut dyn SignalEmitter>,
) -> Result<Value, String> {
let Value::Function {
params,
body,
captured_env,
heap,
} = fn_val
else {
return Err(format!("eval_mms_fn: expected Function, got {:?}", fn_val));
};
let mut emits: Vec<IntentValue> = Vec::new();
let mut world = ObjectWorld::with_heap(heap.clone());
world.push_function_frame(captured_env.clone());
for (index, param) in params.iter().enumerate() {
let arg = args.get(index).cloned().unwrap_or(Value::Null);
world.bind(param.clone(), arg);
}
let mut ctx = EvalContext {
emits: &mut emits,
source_path: None,
channels,
ce_builder: None,
object_world: &mut world,
host_world: world_host.map(|world| world as *mut World),
exec_scope: None,
runtime_closure_mode: RuntimeClosureExecMode::Full,
implicit_cwd: None,
};
let live_emit = emit.as_mut().map(|em| unsafe {
std::mem::transmute::<&mut dyn SignalEmitter, *mut dyn SignalEmitter>(&mut **em)
});
let result = with_live_signal_emitter(live_emit, || {
match eval_block_stmts(&body.statements, &mut ctx)? {
StmtEffect::Return(val) => Ok::<Value, String>(val),
_ => Ok::<Value, String>(Value::Null),
}
})?;
if let Some(em) = emit {
for iv in emits {
em.push_intent_now(ComponentId::default(), iv);
}
}
Ok(result)
}
pub(crate) fn eval_runtime_closure(
closure: &RuntimeClosure,
channels: Option<&mut EvalChannels>,
world_host: Option<&mut World>,
mut emit: Option<&mut dyn SignalEmitter>,
exec_scope: Option<ComponentId>,
mode: RuntimeClosureExecMode,
) -> Result<(), String> {
let mut emits: Vec<IntentValue> = Vec::new();
let mut world = ObjectWorld::with_heap(closure.heap.clone());
world.push_function_frame(closure.captured_env.clone());
let mut ctx = EvalContext {
emits: &mut emits,
source_path: None,
channels,
ce_builder: None,
object_world: &mut world,
host_world: world_host.map(|world| world as *mut World),
exec_scope,
runtime_closure_mode: mode,
implicit_cwd: None,
};
let live_emit = emit.as_mut().map(|em| unsafe {
std::mem::transmute::<&mut dyn SignalEmitter, *mut dyn SignalEmitter>(&mut **em)
});
with_live_signal_emitter(live_emit, || {
let _ = eval_block_stmts(&closure.body.statements, &mut ctx)?;
Ok::<(), String>(())
})?;
if let Some(em) = emit {
for iv in emits {
em.push_intent_now(ComponentId::default(), iv);
}
}
Ok(())
}
pub(crate) fn eval_module_source(source: &str, source_path: Option<&str>) -> Result<Value, String> {
let mut stmts = parse_source(source)?;
EmitLiftTransform::apply(&mut stmts);
QueryDesugarTransform::apply(&mut stmts);
let mut emits: Vec<IntentValue> = Vec::new();
let mut named: HashMap<String, Value> = HashMap::new();
let mut world = ObjectWorld::new();
let mut ctx = EvalContext {
emits: &mut emits,
source_path,
channels: None,
ce_builder: None,
object_world: &mut world,
host_world: None,
exec_scope: None,
runtime_closure_mode: RuntimeClosureExecMode::Full,
implicit_cwd: None,
};
for stmt in &stmts {
match eval_stmt(stmt, &mut ctx)? {
StmtEffect::Exported(name) => {
if let Some(val) = ctx.object_world.lookup(&name).cloned() {
named.insert(name, val);
}
}
StmtEffect::None => {}
StmtEffect::Return(_) | StmtEffect::Break | StmtEffect::Continue => {}
}
}
let sequence: Vec<MaterializedCE> = emits
.into_iter()
.filter_map(|iv| match iv {
IntentValue::SpawnComponentTree { root, .. } => Some(*root),
_ => None,
})
.collect();
Ok(Value::Module {
named,
sequence,
heap: world.heap().clone(),
})
}
fn resolve_import_path(path: &str, source_path: Option<&str>) -> String {
if let Some(src) = source_path {
if let Some(parent) = std::path::Path::new(src).parent() {
return parent.join(path).to_string_lossy().into_owned();
}
}
path.to_string()
}
fn parse_source(source: &str) -> Result<Vec<Statement>, String> {
let tokens = MeowMeowTokenizer::new(source)
.tokenize()
.map_err(|e| tokenize_err_to_string(source, e))?;
MeowMeowParser::new(tokens)
.parse_program()
.map_err(|e| parse_err_to_string(source, e))
}
fn parse_only(source: &str) -> Result<String, String> {
let stmts = parse_source(source)?;
Ok(format!("{stmts:#?}"))
}
fn byte_offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
let offset = offset.min(source.len());
let before = &source[..offset];
let line = before.bytes().filter(|&b| b == b'\n').count() + 1;
let col = before.rfind('\n').map(|p| offset - p).unwrap_or(offset + 1);
(line, col)
}
fn format_source_context(source: &str, line: usize, col: usize) -> String {
let line_text = source.lines().nth(line.saturating_sub(1)).unwrap_or("");
let caret_pad = " ".repeat(col.saturating_sub(1));
format!("\n {line_text}\n {caret_pad}^")
}
fn tokenize_err_to_string(source: &str, e: TokenizeError) -> String {
let (line, col) = byte_offset_to_line_col(source, e.span.start);
format!(
"tokenize error at {}:{}: {}{}",
line,
col,
e.message,
format_source_context(source, line, col),
)
}
fn parse_err_to_string(source: &str, e: ParseError) -> String {
let (line, col) = byte_offset_to_line_col(source, e.span.start);
format!(
"parse error at {}:{}: {}{}",
line,
col,
e.message,
format_source_context(source, line, col),
)
}
#[cfg(test)]
mod tests {
use super::{
EvalRequest, EvalResponse, HostValue, MeowMeowEvaluator, MeowMeowEvaluatorHandle,
eval_module_source,
};
use crate::scripting::object::Value;
#[test]
fn component_body_named_props_can_reference_same_named_bindings() {
let source = r#"
fn make_payload(row_name, label, mode_value) {
return Data {
row_name = row_name
label = label
mode_value = mode_value
row_kind = "EditorMode"
interactive = true
}
}
export let payload = make_payload("editor_settings_mode_cursor_3d", "3D Cursor", "cursor_3d")
"#;
let module = eval_module_source(source, None).expect("module eval");
let Value::Module { named, .. } = module else {
panic!("expected module value");
};
let payload = named.get("payload").expect("exported payload");
let Value::ComponentExpr(component) = payload else {
panic!("expected component expr");
};
assert!(component.named.iter().any(|(key, value)| {
key == "row_name"
&& matches!(value, Value::String(value) if value == "editor_settings_mode_cursor_3d")
}));
assert!(component.named.iter().any(|(key, value)| {
key == "label" && matches!(value, Value::String(value) if value == "3D Cursor")
}));
assert!(component.named.iter().any(|(key, value)| {
key == "mode_value" && matches!(value, Value::String(value) if value == "cursor_3d")
}));
assert!(component.component_property_assignment_only);
}
#[test]
fn structural_component_bodies_keep_normal_reassign_semantics() {
let source = r#"
fn rename_label() {
let label = "hello"
return T {
label = "goodbye"
Text { label }
}
}
export let payload = rename_label()
"#;
let module = eval_module_source(source, None).expect("module eval");
let Value::Module { named, .. } = module else {
panic!("expected module value");
};
let payload = named.get("payload").expect("exported payload");
let Value::ComponentExpr(component) = payload else {
panic!("expected component expr");
};
assert!(!component.component_property_assignment_only);
assert!(component.named.is_empty());
let Some(crate::scripting::object::CeChild::Spawn(text_child)) = component.children.first()
else {
panic!("expected text child");
};
assert!(matches!(
text_child.positionals.first(),
Some(Value::String(label)) if label == "goodbye"
));
}
#[test]
fn structural_component_bodies_still_capture_universal_named_props() {
let source = r#"
export let payload = T {
name = "paint_panel_root"
id = "paint_panel_root"
Text { "hello" }
}
"#;
let module = eval_module_source(source, None).expect("module eval");
let Value::Module { named, .. } = module else {
panic!("expected module value");
};
let payload = named.get("payload").expect("exported payload");
let Value::ComponentExpr(component) = payload else {
panic!("expected component expr");
};
assert!(!component.component_property_assignment_only);
assert!(component.named.iter().any(|(key, value)| {
key == "name" && matches!(value, Value::String(value) if value == "paint_panel_root")
}));
assert!(component.named.iter().any(|(key, value)| {
key == "id" && matches!(value, Value::String(value) if value == "paint_panel_root")
}));
}
#[test]
fn module_eval_supports_table_literals_and_field_access() {
let source = r#"
export let label = {
theme = {
label = "aurora"
}
}.theme.label
"#;
let module = eval_module_source(source, None).expect("module eval");
let Value::Module { named, .. } = module else {
panic!("expected module value");
};
assert!(matches!(
named.get("label"),
Some(Value::String(label)) if label == "aurora"
));
}
#[test]
fn module_eval_supports_for_in_over_tables() {
let source = r#"
let total = 0
for entry in {
apples = 2
pears = 5
} {
total = total + entry.value
}
export let total = total
"#;
let module = eval_module_source(source, None).expect("module eval");
let Value::Module { named, .. } = module else {
panic!("expected module value");
};
assert!(
matches!(named.get("total"), Some(Value::Number(total)) if (*total - 7.0).abs() < 1e-6)
);
}
fn run_test_snippet(
handle: &mut MeowMeowEvaluatorHandle,
source: &str,
) -> Result<Option<Value>, String> {
run_test_snippet_with_cwd(handle, source, None)
}
fn run_test_snippet_with_cwd(
handle: &mut MeowMeowEvaluatorHandle,
source: &str,
cwd: Option<Value>,
) -> Result<Option<Value>, String> {
handle
.requests
.push(EvalRequest::EvalSnippet {
source: source.into(),
cwd,
})
.unwrap();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(1);
loop {
match handle.responses.pop() {
Ok(EvalResponse::SnippetComplete { result }) => return result,
Ok(EvalResponse::HostCall { kind, .. }) => {
panic!("navigation unexpectedly requested a host call: {kind:?}")
}
Ok(_) | Err(rtrb::PopError::Empty) if std::time::Instant::now() < deadline => {
std::thread::yield_now()
}
other => panic!("snippet timed out or returned unexpected response: {other:?}"),
}
}
}
fn run_test_navigation(
handle: &mut MeowMeowEvaluatorHandle,
source: &str,
cwd: Option<Value>,
) -> Result<Value, String> {
handle
.requests
.push(EvalRequest::EvalNavigation {
source: source.into(),
cwd,
})
.unwrap();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(1);
loop {
match handle.responses.pop() {
Ok(EvalResponse::NavigationComplete { result }) => return result,
Ok(EvalResponse::HostCall { id, .. }) => {
handle
.requests
.push(EvalRequest::HostCallResult {
id,
value: HostValue::Null,
})
.unwrap();
}
Ok(_) | Err(rtrb::PopError::Empty) if std::time::Instant::now() < deadline => {
std::thread::yield_now()
}
other => panic!("navigation timed out or returned unexpected response: {other:?}"),
}
}
}
#[test]
fn repl_snippets_persist_echo_reset_and_recover() {
let mut handle = MeowMeowEvaluator::spawn(32);
assert_eq!(
run_test_snippet(&mut handle, "let answer = 42").unwrap(),
None
);
assert_eq!(
run_test_snippet(&mut handle, "answer").unwrap(),
Some(Value::Number(42.0))
);
assert!(run_test_snippet(&mut handle, "1 + true").is_err());
assert_eq!(
run_test_snippet(&mut handle, "answer").unwrap(),
Some(Value::Number(42.0))
);
assert_eq!(
run_test_snippet(&mut handle, "reset()\nanswer").unwrap(),
Some(Value::Number(42.0))
);
assert!(run_test_snippet(&mut handle, "answer + 1").is_err());
handle.shutdown_and_join();
}
#[test]
fn repl_navigation_is_detached_and_supplies_dynamic_table_context() {
let mut handle = MeowMeowEvaluator::spawn(32);
run_test_snippet(
&mut handle,
r#"let settings = { theme = { tone = "dark" } }"#,
)
.unwrap();
let settings = run_test_navigation(&mut handle, "settings", None).unwrap();
let theme = run_test_navigation(&mut handle, "settings.theme", None).unwrap();
assert_eq!(
run_test_snippet_with_cwd(&mut handle, "tone", Some(theme.clone())).unwrap(),
Some(Value::String("dark".into()))
);
run_test_snippet_with_cwd(&mut handle, r#"tone = "light""#, Some(theme.clone())).unwrap();
assert_eq!(
run_test_navigation(&mut handle, "settings.theme.tone", Some(settings)).unwrap(),
Value::String("light".into())
);
run_test_snippet(&mut handle, "let read_tone = fn() { return tone }").unwrap();
assert_eq!(
run_test_snippet_with_cwd(&mut handle, "read_tone()", Some(theme.clone())).unwrap(),
Some(Value::String("light".into()))
);
run_test_snippet_with_cwd(
&mut handle,
r#"let tone = "lexical"
tone = "shadowed""#,
Some(theme.clone()),
)
.unwrap();
assert_eq!(
run_test_snippet_with_cwd(&mut handle, "tone", Some(theme.clone())).unwrap(),
Some(Value::String("shadowed".into()))
);
assert_eq!(
run_test_navigation(&mut handle, "settings.theme.tone", None).unwrap(),
Value::String("light".into())
);
let detached = run_test_navigation(&mut handle, r#"T { Text { "x" } }"#, None).unwrap();
assert!(matches!(detached, Value::ComponentExpr(_)));
run_test_snippet(
&mut handle,
"let make_detached = fn() { let node = T {} return node }",
)
.unwrap();
let detached = run_test_navigation(&mut handle, "make_detached()", None).unwrap();
assert!(matches!(detached, Value::ComponentExpr(_)));
handle.shutdown_and_join();
}
}