meow-meow-script 0.6.0

A host-neutral parser, runtime, and evaluator for Meow Meow Script
Documentation
//! Configurable MMS runtime and persistent sessions.

use std::collections::{HashMap, HashSet};
use std::fmt;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;

use crate::{CallbackHandle, EvalError, Evaluation, Evaluator, Expression, HeapHandle, Host,
    HostCapabilities, HostContext, Hostless, MaterializedCE, MeowMeowParser, MeowMeowTokenizer,
    Statement, Value};

static NEXT_SESSION_TAG: AtomicU32 = AtomicU32::new(1);

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ValueType {
    Any, Null, Bool, Number, String, Array, Table, Component, Callback,
}

impl ValueType {
    pub fn accepts(&self, value: &Value) -> bool {
        matches!(self, Self::Any)
            || matches!((self, value),
                (Self::Null, Value::Null) | (Self::Bool, Value::Bool(_))
                | (Self::Number, Value::Number(_)) | (Self::String, Value::String(_))
                | (Self::Array, Value::Array(_)) | (Self::Table, Value::Map(_) | Value::Object(_))
                | (Self::Component, Value::ComponentObject { .. })
                | (Self::Callback, Value::Function { .. }))
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValueSignature {
    pub arguments: Vec<ValueType>,
    pub result: ValueType,
    pub variadic: bool,
}

impl ValueSignature {
    pub fn new(arguments: impl Into<Vec<ValueType>>, result: ValueType) -> Self {
        Self { arguments: arguments.into(), result, variadic: false }
    }
    pub fn any() -> Self { Self { arguments: vec![], result: ValueType::Any, variadic: true } }
}

pub type ComponentCallback = Arc<dyn Fn(&mut MaterializedCE) -> Result<(), String> + Send + Sync>;

#[derive(Clone)]
pub struct ComponentSpec {
    pub name: String,
    pub aliases: Vec<String>,
    pub constructors: HashMap<String, ValueSignature>,
    pub builder_calls: HashMap<String, ValueSignature>,
    pub properties: HashMap<String, ValueType>,
    pub positional: Vec<ValueType>,
    pub methods: HashMap<String, ValueSignature>,
    pub required_capability: Option<String>,
    pub normalize: Option<ComponentCallback>,
    pub validate: Option<ComponentCallback>,
}

impl fmt::Debug for ComponentSpec {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("ComponentSpec").field("name", &self.name).field("aliases", &self.aliases).finish_non_exhaustive()
    }
}

impl ComponentSpec {
    pub fn new(name: impl Into<String>) -> Self {
        Self { name: name.into(), aliases: vec![], constructors: HashMap::new(),
            builder_calls: HashMap::new(), properties: HashMap::new(), positional: vec![],
            methods: HashMap::new(), required_capability: None, normalize: None, validate: None }
    }
    pub fn alias(mut self, alias: impl Into<String>) -> Self { self.aliases.push(alias.into()); self }
    pub fn constructor(mut self, name: impl Into<String>, signature: ValueSignature) -> Self { self.constructors.insert(name.into(), signature); self }
    pub fn builder_call(mut self, name: impl Into<String>, signature: ValueSignature) -> Self { self.builder_calls.insert(name.into(), signature); self }
    pub fn property(mut self, name: impl Into<String>, ty: ValueType) -> Self { self.properties.insert(name.into(), ty); self }
    pub fn positional(mut self, ty: ValueType) -> Self { self.positional.push(ty); self }
    pub fn method(mut self, name: impl Into<String>, signature: ValueSignature) -> Self { self.methods.insert(name.into(), signature); self }
    pub fn requires(mut self, capability: impl Into<String>) -> Self { self.required_capability = Some(capability.into()); self }
    pub fn normalize_with(mut self, callback: impl Fn(&mut MaterializedCE) -> Result<(), String> + Send + Sync + 'static) -> Self { self.normalize = Some(Arc::new(callback)); self }
    pub fn validate_with(mut self, callback: impl Fn(&mut MaterializedCE) -> Result<(), String> + Send + Sync + 'static) -> Self { self.validate = Some(Arc::new(callback)); self }
}

#[derive(Debug, Clone)]
pub struct HostApiSpec {
    pub id: String,
    pub namespace: Option<String>,
    pub name: String,
    pub signature: ValueSignature,
    pub required_capability: String,
}

impl HostApiSpec {
    pub fn function(name: impl Into<String>, signature: ValueSignature) -> Self {
        let name = name.into(); Self { id: name.clone(), namespace: None, name,
            signature, required_capability: String::new() }
    }
    pub fn method(namespace: impl Into<String>, name: impl Into<String>, signature: ValueSignature) -> Self {
        let namespace = namespace.into(); let name = name.into();
        Self { id: format!("{namespace}.{name}"), namespace: Some(namespace), name, signature,
            required_capability: String::new() }
    }
    pub fn id(mut self, id: impl Into<String>) -> Self { self.id = id.into(); self }
    pub fn requires(mut self, capability: impl Into<String>) -> Self { self.required_capability = capability.into(); self }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CatalogErrorKind { DuplicateName, NameConflict, CapabilityMismatch }

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CatalogError { pub kind: CatalogErrorKind, pub name: String, pub message: String }
impl fmt::Display for CatalogError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.message) } }
impl std::error::Error for CatalogError {}

#[derive(Debug, Clone)]
pub(crate) struct Catalog {
    pub components: HashMap<String, Arc<ComponentSpec>>,
    pub canonical_components: Vec<Arc<ComponentSpec>>,
    pub apis: HashMap<String, Arc<HostApiSpec>>,
    pub namespaces: HashSet<String>,
    pub builtins: HashSet<String>,
}

impl Catalog {
    pub(crate) fn has_namespace(&self, name: &str) -> bool {
        self.namespaces.iter().any(|namespace| namespace.eq_ignore_ascii_case(name))
    }
}

#[derive(Debug, Clone)]
pub struct RuntimeBuilder { catalog: Catalog }

impl Default for RuntimeBuilder {
    fn default() -> Self {
        Self { catalog: Catalog { components: HashMap::new(), canonical_components: vec![], apis: HashMap::new(),
            namespaces: HashSet::new(), builtins: ["null", "range", "len", "query", "query_all", "Math", "MusicNote"].into_iter().map(str::to_owned).collect() } }
    }
}

impl RuntimeBuilder {
    pub fn new() -> Self { Self::default() }
    pub fn register_builtin(&mut self, name: impl Into<String>) -> Result<&mut Self, CatalogError> {
        let name = name.into(); self.ensure_free(&name)?; self.catalog.builtins.insert(name); Ok(self)
    }
    pub fn register_component(&mut self, spec: ComponentSpec) -> Result<&mut Self, CatalogError> {
        let spec = Arc::new(spec);
        for name in std::iter::once(&spec.name).chain(&spec.aliases) { self.ensure_free(name)?; }
        for name in std::iter::once(&spec.name).chain(&spec.aliases) {
            self.catalog.components.insert(name.to_lowercase(), spec.clone());
        }
        self.catalog.canonical_components.push(spec); Ok(self)
    }
    pub fn register_host_api(&mut self, spec: HostApiSpec) -> Result<&mut Self, CatalogError> {
        if let Some(namespace) = &spec.namespace {
            if !self.catalog.namespaces.contains(namespace) { self.ensure_free(namespace)?; }
            self.catalog.namespaces.insert(namespace.clone());
        } else { self.ensure_free(&spec.name)?; }
        let key = api_key(spec.namespace.as_deref(), &spec.name);
        if self.catalog.apis.contains_key(&key) { return Err(duplicate(&key)); }
        self.catalog.apis.insert(key, Arc::new(spec)); Ok(self)
    }
    fn ensure_free(&self, name: &str) -> Result<(), CatalogError> {
        let lower = name.to_lowercase();
        if self.catalog.components.contains_key(&lower) || self.catalog.builtins.iter().any(|v| v.eq_ignore_ascii_case(name))
            || self.catalog.namespaces.iter().any(|v| v.eq_ignore_ascii_case(name))
            || self.catalog.apis.values().any(|api| api.namespace.is_none() && api.name.eq_ignore_ascii_case(name)) {
            return Err(duplicate(name));
        } Ok(())
    }
    pub fn build(self) -> Runtime { Runtime { catalog: Arc::new(self.catalog) } }
}

fn duplicate(name: &str) -> CatalogError { CatalogError { kind: CatalogErrorKind::DuplicateName, name: name.into(), message: format!("catalog name '{name}' is already registered") } }
pub(crate) fn api_key(namespace: Option<&str>, name: &str) -> String { namespace.map_or_else(|| name.to_lowercase(), |ns| format!("{}.{}", ns.to_lowercase(), name.to_lowercase())) }

#[derive(Debug, Clone)]
pub struct Runtime { pub(crate) catalog: Arc<Catalog> }
impl Runtime {
    pub fn builder() -> RuntimeBuilder { RuntimeBuilder::new() }
    pub fn component_names(&self) -> impl Iterator<Item = &str> { self.catalog.components.keys().map(String::as_str) }
    pub fn materialize_component(&self, source: &str) -> Result<MaterializedCE, EvalError> {
        let tokens = MeowMeowTokenizer::new(source)
            .tokenize()
            .map_err(|e| EvalError::Tokenize(format!("{e:?}")))?;
        let statements = MeowMeowParser::with_component_names(tokens, self.catalog.components.keys().cloned())
            .parse_program()
            .map_err(|e| EvalError::Parse(e.message))?;
        let [Statement::Expression(Expression::Component(component))] = statements.as_slice() else {
            return Err(EvalError::Runtime("expected exactly one component expression".into()));
        };
        let mut host = Hostless;
        let tag = NEXT_SESSION_TAG.fetch_add(1, Ordering::Relaxed);
        let mut context = HostContext::new(tag);
        let mut evaluator = Evaluator::for_session(
            &mut host,
            vec![HashMap::from([("null".into(), Value::Null)])],
            HeapHandle::new(),
            HashMap::new(),
            self.catalog.clone(),
            &mut context,
        );
        evaluator.materialize(component)
    }
    pub fn session<H: Host>(&self, host: H) -> Result<Session<H>, CatalogError> {
        check_capabilities(&self.catalog, &host.capabilities())?;
        let tag = NEXT_SESSION_TAG.fetch_add(1, Ordering::Relaxed);
        Ok(Session { runtime: self.clone(), host, scopes: vec![HashMap::from([("null".into(), Value::Null)])],
            heap: HeapHandle::new(), callbacks: HashMap::new(), context: HostContext::new(tag) })
    }
}

fn check_capabilities(catalog: &Catalog, host: &HostCapabilities) -> Result<(), CatalogError> {
    for component in &catalog.canonical_components {
        if !host.components.contains(&component.name.to_lowercase()) {
            return Err(CatalogError { kind: CatalogErrorKind::CapabilityMismatch, name: component.name.clone(),
                message: format!("host does not support component '{}'", component.name) });
        }
        if let Some(capability) = &component.required_capability {
            if !host.component_operations.contains(capability) { return Err(CatalogError { kind: CatalogErrorKind::CapabilityMismatch,
                name: capability.clone(), message: format!("host is missing component capability '{capability}'") }); }
        }
    }
    for api in catalog.apis.values() {
        let required = if api.required_capability.is_empty() { &api.id } else { &api.required_capability };
        if !host.api_ids.contains(required) { return Err(CatalogError { kind: CatalogErrorKind::CapabilityMismatch,
            name: required.clone(), message: format!("host is missing API capability '{required}'") }); }
    }
    Ok(())
}

pub struct Session<H: Host> {
    runtime: Runtime,
    host: H,
    pub(crate) scopes: Vec<HashMap<String, Value>>,
    pub(crate) heap: HeapHandle,
    pub(crate) callbacks: HashMap<CallbackHandle, Value>,
    pub(crate) context: HostContext,
}

impl<H: Host> Session<H> {
    pub fn eval(&mut self, source: &str) -> Result<Evaluation, EvalError> {
        let scopes = std::mem::take(&mut self.scopes);
        let callbacks = std::mem::take(&mut self.callbacks);
        let mut evaluator = Evaluator::for_session(&mut self.host, scopes, self.heap.clone(), callbacks,
            self.runtime.catalog.clone(), &mut self.context);
        let result = evaluator.evaluate(source);
        let (scopes, callbacks) = evaluator.into_session_state();
        self.scopes = scopes; self.callbacks = callbacks; result
    }
    pub fn invoke_callback(&mut self, handle: CallbackHandle, args: Vec<Value>) -> Result<Value, EvalError> {
        if !self.context.owns_callback(handle) { return Err(EvalError::Runtime(format!("stale or foreign callback {handle:?}"))); }
        let callback = self.callbacks.get(&handle).cloned().ok_or_else(|| EvalError::Runtime(format!("unknown callback {handle:?}")))?;
        let scopes = std::mem::take(&mut self.scopes); let callbacks = std::mem::take(&mut self.callbacks);
        let mut evaluator = Evaluator::for_session(&mut self.host, scopes, self.heap.clone(), callbacks,
            self.runtime.catalog.clone(), &mut self.context);
        let result = evaluator.invoke_value(callback, args);
        let (scopes, callbacks) = evaluator.into_session_state(); self.scopes = scopes; self.callbacks = callbacks; result
    }
    pub fn host(&self) -> &H { &self.host }
    pub fn host_mut(&mut self) -> &mut H { &mut self.host }
    pub fn context(&self) -> &HostContext { &self.context }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{EventStreamHost, HostError, HostRequest, HostResponse, TransportValue};

    struct FixedHandleHost {
        capabilities: HostCapabilities,
        handle: crate::ComponentHandle,
        requests: Vec<HostRequest>,
    }

    impl crate::Host for FixedHandleHost {
        fn capabilities(&self) -> HostCapabilities { self.capabilities.clone() }
        fn dispatch_with_context(&mut self, _context: &mut HostContext, request: HostRequest) -> Result<HostResponse, HostError> {
            self.requests.push(request.clone());
            match request {
                HostRequest::Emit { tree } | HostRequest::RegisterComponent { tree } => {
                    Ok(HostResponse::Component { handle: self.handle, component_type: tree.component_type })
                }
                HostRequest::InvokeComponentMethod { .. } => Ok(HostResponse::Unit),
                other => Err(HostError::unsupported(other.operation_name())),
            }
        }
    }

    fn runtime() -> Runtime {
        let mut builder = Runtime::builder();
        builder.register_component(ComponentSpec::new("Panel").alias("panel")
            .constructor("new", ValueSignature::new(vec![ValueType::Number], ValueType::Component))
            .property("title", ValueType::String)
            .method("show", ValueSignature::new(vec![], ValueType::Null))
            .normalize_with(|tree| { tree.component_type = "Panel".into(); Ok(()) })).unwrap();
        builder.register_host_api(HostApiSpec::method("log", "write",
            ValueSignature::new(vec![ValueType::String], ValueType::Null)).requires("log.write")).unwrap();
        builder.register_host_api(HostApiSpec::method("sink", "write",
            ValueSignature::new(vec![ValueType::Any], ValueType::Null)).requires("sink.write")).unwrap();
        builder.build()
    }

    fn capabilities() -> HostCapabilities {
        HostCapabilities::default().supports_component("Panel").supports_api("log.write").supports_api("sink.write")
    }

    #[test]
    fn catalog_parses_lowercase_aliases_and_issues_handles() {
        let mut session = runtime().session(EventStreamHost::new(capabilities())).unwrap();
        session.eval("panel.new(2) { title = \"hello\" }").unwrap();
        let crate::HostEvent::Emit { handle, tree } = &session.host().events[0] else { panic!() };
        assert!(session.context().owns_component(*handle));
        assert_eq!(tree.component_type, "Panel");
    }

    #[test]
    fn emitted_component_identity_comes_from_host_response() {
        let handle = crate::ComponentHandle::from_raw(0xfeed_face);
        let host = FixedHandleHost { capabilities: capabilities(), handle, requests: vec![] };
        let mut session = runtime().session(host).unwrap();
        let result = session.eval("panel.new(2) { title = \"hello\" }").unwrap();
        assert_eq!(result.value, Some(Value::ComponentObject { id: handle, component_type: "Panel".into() }));
        assert!(session.context().owns_component(handle));
        assert!(matches!(session.host().requests[0], HostRequest::Emit { .. }));
    }

    #[test]
    fn materializes_component_without_host_session() {
        let tree = runtime().materialize_component("panel.new(2) { title = \"hello\" }").unwrap();
        assert_eq!(tree.component_type, "Panel");
        assert_eq!(tree.ctor_method, Some("new".into()));
        assert_eq!(tree.ctor_args, vec![Value::Number(2.0)]);
        assert_eq!(tree.named, vec![("title".into(), Value::String("hello".into()))]);
    }

    #[test]
    fn bindings_and_table_identity_persist_between_evaluations() {
        let mut session = runtime().session(EventStreamHost::new(capabilities())).unwrap();
        session.eval("let table = { value = 1 }; let alias = table").unwrap();
        session.eval("alias[\"value\"] = 9").unwrap();
        let result = session.eval("table[\"value\"]").unwrap();
        assert_eq!(result.value, Some(Value::Number(9.0)));
    }

    #[test]
    fn namespace_api_is_transport_safe() {
        let mut session = runtime().session(EventStreamHost::new(capabilities())).unwrap();
        session.eval("log.write(\"hello\")").unwrap();
        assert!(matches!(&session.host().events[0], crate::HostEvent::Api { id, args }
            if id == "log.write" && args == &vec![TransportValue::String("hello".into())]));
    }

    #[test]
    fn duplicate_and_capability_failures_are_typed() {
        let mut builder = Runtime::builder();
        builder.register_component(ComponentSpec::new("Panel")).unwrap();
        assert_eq!(builder.register_component(ComponentSpec::new("panel")).unwrap_err().kind, CatalogErrorKind::DuplicateName);
        let error = match builder.build().session(EventStreamHost::new(HostCapabilities::default())) {
            Err(error) => error,
            Ok(_) => panic!("expected capability mismatch"),
        };
        assert_eq!(error.kind, CatalogErrorKind::CapabilityMismatch);
    }

    #[test]
    fn suggestions_and_schema_validation_are_reported() {
        let mut session = runtime().session(EventStreamHost::new(capabilities())).unwrap();
        let error = session.eval("panel.neew(2)").unwrap_err().to_string();
        assert!(error.contains("did you mean 'new'"), "{error}");
        let error = session.eval("panel.new(\"bad\")").unwrap_err().to_string();
        assert!(error.contains("wrong type"), "{error}");
    }

    #[test]
    fn host_boundary_rejects_cyclic_tables() {
        let mut session = runtime().session(EventStreamHost::new(capabilities())).unwrap();
        session.eval("let table = { label = \"root\" }; table[\"self\"] = table").unwrap();
        let error = session.eval("sink.write(table)").unwrap_err().to_string();
        assert!(error.contains("cyclic table"), "{error}");
    }

    #[test]
    fn dotted_unknown_component_suggests_registered_names() {
        let mut session = runtime().session(EventStreamHost::new(capabilities())).unwrap();
        let error = session.eval("panal.new(2)").unwrap_err().to_string();
        assert!(error.contains("did you mean 'panel'"), "{error}");
    }
}