Skip to main content

meow_meow_script/
host.rs

1use std::collections::HashSet;
2use std::error::Error;
3use std::fmt;
4
5use crate::object::{MaterializedCE, Value};
6
7/// An opaque component identity owned by the scripting boundary.
8///
9/// Hosts may encode their native generational handle losslessly in this value,
10/// but scripts cannot inspect or manufacture its representation.
11#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
12pub struct ComponentHandle(u64);
13
14impl ComponentHandle {
15    pub fn from_raw(raw: u64) -> Self {
16        Self(raw)
17    }
18
19    pub fn into_raw(self) -> u64 {
20        self.0
21    }
22}
23
24/// Opaque identity for a script closure retained by a session.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
26pub struct CallbackHandle(u64);
27
28impl CallbackHandle {
29    pub fn from_raw(raw: u64) -> Self { Self(raw) }
30    pub fn into_raw(self) -> u64 { self.0 }
31}
32
33/// Values that are safe to own outside the MMS heap. In particular, tables
34/// are snapshots and closures are represented only by opaque handles.
35#[derive(Debug, Clone, PartialEq)]
36pub enum TransportValue {
37    Null,
38    Bool(bool),
39    Number(f64),
40    String(String),
41    Array(Vec<TransportValue>),
42    Table(Vec<(String, TransportValue)>),
43    Component(ComponentHandle),
44    Callback(CallbackHandle),
45}
46
47/// Capabilities advertised by a host before a session is created.
48#[derive(Debug, Clone, Default, PartialEq, Eq)]
49pub struct HostCapabilities {
50    pub components: HashSet<String>,
51    pub component_operations: HashSet<String>,
52    pub api_ids: HashSet<String>,
53}
54
55impl HostCapabilities {
56    pub fn supports_component(mut self, name: impl Into<String>) -> Self {
57        self.components.insert(name.into().to_lowercase()); self
58    }
59    pub fn supports_operation(mut self, operation: impl Into<String>) -> Self {
60        self.component_operations.insert(operation.into()); self
61    }
62    pub fn supports_api(mut self, id: impl Into<String>) -> Self {
63        self.api_ids.insert(id.into()); self
64    }
65}
66
67/// Allocator and ownership checker supplied to host dispatch.
68///
69/// Component handles identify host-owned resources, so effectful hosts may
70/// return handles derived from native IDs. Simple hosts can use
71/// `allocate_component` for synthetic identities. Callback handles identify
72/// MMS-owned closures and are always allocated by the runtime.
73#[derive(Debug)]
74pub struct HostContext {
75    session_tag: u32,
76    next_component: u32,
77    next_callback: u32,
78    components: HashSet<ComponentHandle>,
79    callbacks: HashSet<CallbackHandle>,
80}
81
82impl HostContext {
83    pub(crate) fn new(session_tag: u32) -> Self {
84        Self { session_tag, next_component: 1, next_callback: 1,
85            components: HashSet::new(), callbacks: HashSet::new() }
86    }
87    pub fn allocate_component(&mut self) -> ComponentHandle {
88        let handle = ComponentHandle::from_raw(
89            ((self.session_tag as u64) << 32) | self.next_component as u64);
90        self.next_component = self.next_component.checked_add(1).expect("component handle space exhausted");
91        self.components.insert(handle); handle
92    }
93    pub fn adopt_component(&mut self, handle: ComponentHandle) {
94        self.components.insert(handle);
95    }
96    pub fn allocate_callback(&mut self) -> CallbackHandle {
97        let handle = CallbackHandle::from_raw(
98            ((self.session_tag as u64) << 32) | self.next_callback as u64);
99        self.next_callback = self.next_callback.checked_add(1).expect("callback handle space exhausted");
100        self.callbacks.insert(handle); handle
101    }
102    pub fn owns_component(&self, handle: ComponentHandle) -> bool { self.components.contains(&handle) }
103    pub fn owns_callback(&self, handle: CallbackHandle) -> bool { self.callbacks.contains(&handle) }
104}
105
106impl fmt::Debug for ComponentHandle {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        f.debug_tuple("ComponentHandle").field(&self.0).finish()
109    }
110}
111
112#[derive(Debug, Clone, PartialEq)]
113pub enum HostRequest {
114    /// Register an uninitialized component. The host returns its component
115    /// identity in `HostResponse::Component`.
116    RegisterComponent { tree: MaterializedCE },
117    /// Emit a component tree. The host returns its component identity in
118    /// `HostResponse::Component`.
119    Emit { tree: MaterializedCE },
120    Spawn {
121        tree: MaterializedCE,
122    },
123    Register {
124        tree: MaterializedCE,
125    },
126    Attach {
127        parent: Option<ComponentHandle>,
128        child: ComponentHandle,
129    },
130    Query {
131        selector: String,
132        scope: Option<ComponentHandle>,
133        multiple: bool,
134    },
135    RegisterHandler {
136        scope: ComponentHandle,
137        signal: String,
138        name: Option<String>,
139        handler: Value,
140    },
141    InvokeComponentMethod {
142        component: ComponentHandle,
143        component_type: String,
144        method: String,
145        args: Vec<Value>,
146    },
147    CallApi {
148        api_id: String,
149        args: Vec<TransportValue>,
150    },
151    AudioClipInstance {
152        source: ComponentHandle,
153        start_beat: Option<f64>,
154        stop_beat: Option<f64>,
155    },
156    AudioOperation {
157        operation: String,
158        target: Option<ComponentHandle>,
159        args: Vec<Value>,
160    },
161    ReplTree {
162        value: Value,
163        max_depth: Option<usize>,
164    },
165    ReplDump {
166        value: Value,
167    },
168    ReplHelp,
169    ReplClear,
170    /// A named engine mutation. The payload remains composed exclusively of
171    /// script-owned values so no engine type crosses the crate boundary.
172    EngineMutation {
173        operation: String,
174        targets: Vec<ComponentHandle>,
175        args: Vec<Value>,
176    },
177}
178
179impl HostRequest {
180    pub fn operation_name(&self) -> &str {
181        match self {
182            Self::RegisterComponent { .. } => "register_component",
183            Self::Emit { .. } => "emit",
184            Self::Spawn { .. } => "spawn",
185            Self::Register { .. } => "register",
186            Self::Attach { .. } => "attach",
187            Self::Query { .. } => "query",
188            Self::RegisterHandler { .. } => "register_handler",
189            Self::InvokeComponentMethod { .. } => "invoke_component_method",
190            Self::CallApi { api_id, .. } => api_id,
191            Self::AudioClipInstance { .. } => "audio_clip_instance",
192            Self::AudioOperation { operation, .. } | Self::EngineMutation { operation, .. } => {
193                operation
194            }
195            Self::ReplTree { .. } => "repl_tree",
196            Self::ReplDump { .. } => "repl_dump",
197            Self::ReplHelp => "repl_help",
198            Self::ReplClear => "repl_clear",
199        }
200    }
201}
202
203#[derive(Debug, Clone, PartialEq)]
204pub enum HostResponse {
205    Unit,
206    Value(Value),
207    Component {
208        handle: ComponentHandle,
209        component_type: String,
210    },
211    Components(Vec<(ComponentHandle, String)>),
212    Transport(TransportValue),
213}
214
215#[derive(Debug, Clone, PartialEq, Eq)]
216pub enum HostErrorKind {
217    UnsupportedHostOperation,
218    InvalidRequest,
219    HostFailure,
220    ForeignHandle,
221    Conversion,
222}
223
224#[derive(Debug, Clone, PartialEq, Eq)]
225pub struct HostError {
226    pub kind: HostErrorKind,
227    pub operation: String,
228    pub message: String,
229}
230
231impl HostError {
232    pub fn unsupported(operation: impl Into<String>) -> Self {
233        let operation = operation.into();
234        Self {
235            kind: HostErrorKind::UnsupportedHostOperation,
236            message: format!("host operation '{operation}' is unavailable"),
237            operation,
238        }
239    }
240
241    pub fn failure(operation: impl Into<String>, message: impl Into<String>) -> Self {
242        Self {
243            kind: HostErrorKind::HostFailure,
244            operation: operation.into(),
245            message: message.into(),
246        }
247    }
248}
249
250impl fmt::Display for HostError {
251    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252        write!(f, "{}: {}", self.operation, self.message)
253    }
254}
255
256impl Error for HostError {}
257
258/// Synchronous interface through which evaluation requests host capabilities.
259pub trait Host {
260    /// Legacy dispatch entry point. New hosts may implement only
261    /// `dispatch_with_context` and leave this default in place.
262    fn dispatch(&mut self, request: HostRequest) -> Result<HostResponse, HostError> {
263        Err(HostError::unsupported(request.operation_name()))
264    }
265
266    fn capabilities(&self) -> HostCapabilities { HostCapabilities::default() }
267
268    fn dispatch_with_context(
269        &mut self,
270        _context: &mut HostContext,
271        request: HostRequest,
272    ) -> Result<HostResponse, HostError> {
273        self.dispatch(request)
274    }
275}
276
277/// Capability-free host used for deterministic pure-language evaluation.
278#[derive(Debug, Default, Clone, Copy)]
279pub struct Hostless;
280
281impl Host for Hostless {
282    fn dispatch(&mut self, request: HostRequest) -> Result<HostResponse, HostError> {
283        Err(HostError::unsupported(request.operation_name()))
284    }
285}