Skip to main content

hara_native/vm/
prepared.rs

1//! Prepare-once callable dispatch through a stable namespace Var.
2
3use std::rc::Rc;
4
5use crate::core::Value;
6use crate::kernel::{NamespaceRegistry, Var};
7use crate::lang::data::Symbol;
8
9use super::fiber::VmFiber;
10use super::opcode::Instruction;
11use super::program::{FunctionPrototype, Program, MAX_PRIMITIVE_ARGUMENTS};
12use super::source_map::SourceMap;
13use super::validate::validate;
14
15/// A stable global function cell plus a validated argument-call stub.
16///
17/// The Var is dereferenced for every call, so redefining its root takes effect
18/// without rebuilding the handle. Arguments enter lexical slots directly;
19/// no request binding is interned into a namespace.
20#[derive(Debug, Clone)]
21pub struct PreparedCall {
22    symbol: String,
23    arity: u16,
24    var: Var<Value>,
25    program: Rc<Program>,
26}
27
28impl PreparedCall {
29    pub fn symbol(&self) -> &str {
30        &self.symbol
31    }
32
33    pub fn arity(&self) -> u16 {
34        self.arity
35    }
36
37    /// Starts one resumable invocation with the current root of the Var.
38    pub fn start(&self, arguments: Vec<Value>) -> Result<VmFiber, String> {
39        if arguments.len() != usize::from(self.arity) {
40            return Err(format!("{} expects {} arguments", self.symbol, self.arity));
41        }
42        let callable = self.var.deref_value();
43        if !matches!(callable, Value::Function(_)) {
44            return Err(format!("prepared Var is not callable: {}", self.symbol));
45        }
46        let mut locals = Vec::with_capacity(arguments.len() + 1);
47        locals.push(callable);
48        locals.extend(arguments);
49        Ok(VmFiber::start_call(
50            self.program.clone(),
51            0,
52            locals,
53            Vec::new(),
54        ))
55    }
56
57    /// Invokes the prepared Var without the synthetic outer call machine.
58    ///
59    /// Compiled closures run their own machine and return a Promise only when
60    /// execution actually suspends. Embedders that already own continuation
61    /// scheduling can use this path to avoid wrapping every synchronous call
62    /// in a second VM.
63    pub fn invoke(&self, arguments: Vec<Value>) -> Result<Value, String> {
64        if arguments.len() != usize::from(self.arity) {
65            return Err(format!("{} expects {} arguments", self.symbol, self.arity));
66        }
67        let callable = self.var.deref_value();
68        let Value::Function(function) = callable else {
69            return Err(format!("prepared Var is not callable: {}", self.symbol));
70        };
71        crate::core::call_function(&function, arguments)
72    }
73}
74
75/// Resolves a callable Var once and prepares a validated direct-argument stub.
76pub fn prepare_call(
77    registry: &NamespaceRegistry<Value>,
78    symbol: &str,
79    arity: u16,
80) -> Result<PreparedCall, String> {
81    if usize::from(arity) > MAX_PRIMITIVE_ARGUMENTS {
82        return Err(format!(
83            "prepared call arity exceeds {MAX_PRIMITIVE_ARGUMENTS}"
84        ));
85    }
86    let parsed = Symbol::parse(symbol);
87    let var = registry
88        .resolve(&parsed)
89        .ok_or_else(|| format!("unbound handler Var: {symbol}"))?;
90    if !matches!(var.deref_value(), Value::Function(_)) {
91        return Err(format!("prepared Var is not callable: {symbol}"));
92    }
93
94    let mut code = Vec::with_capacity(usize::from(arity) + 2);
95    code.push(Instruction::LoadLocal(0));
96    for argument in 0..arity {
97        code.push(Instruction::LoadLocal(argument + 1));
98    }
99    code.push(Instruction::Call { argc: arity as u8 });
100    code.push(Instruction::Return);
101    let mut source_map = SourceMap::default();
102    for _ in &code {
103        source_map.record(None);
104    }
105    let program = Program {
106        namespace: None,
107        constants: Vec::new(),
108        var_metadata: Vec::new(),
109        schema_types: Default::default(),
110        function_types: Default::default(),
111        inferred_function_types: Default::default(),
112        functions: vec![FunctionPrototype {
113            name: Some(format!("prepared:{symbol}")),
114            async_function: false,
115            arity: arity + 1,
116            variadic: false,
117            capture_count: 0,
118            local_count: arity + 1,
119            max_stack: arity + 1,
120            code,
121            source_map,
122            handlers: Vec::new(),
123        }],
124        entry: 0,
125    };
126    validate(&program).map_err(|error| error.to_string())?;
127    Ok(PreparedCall {
128        symbol: var.symbol().as_str().to_owned(),
129        arity,
130        var,
131        program: Rc::new(program),
132    })
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use crate::core::native_function;
139
140    #[test]
141    fn prepared_calls_pass_arguments_without_globals_and_observe_redefinition() {
142        let registry = NamespaceRegistry::new("app");
143        let function = registry.current().intern(
144            "handler",
145            native_function("handler", 1, |args| Ok(args[0].clone())),
146        );
147        let prepared = prepare_call(&registry, "app/handler", 1).unwrap();
148
149        assert_eq!(
150            prepared
151                .start(vec![Value::Number(42)])
152                .unwrap()
153                .drive_sync()
154                .unwrap(),
155            Value::Number(42)
156        );
157        assert_eq!(
158            prepared.invoke(vec![Value::Number(42)]).unwrap(),
159            Value::Number(42)
160        );
161
162        function.reset_value(native_function("handler", 1, |_| Ok(Value::Number(7))));
163        assert_eq!(
164            prepared
165                .start(vec![Value::Number(42)])
166                .unwrap()
167                .drive_sync()
168                .unwrap(),
169            Value::Number(7)
170        );
171        assert!(registry
172            .resolve(&Symbol::parse("__hoplite_request"))
173            .is_none());
174    }
175
176    #[test]
177    fn prepared_calls_validate_target_and_arity() {
178        let registry = NamespaceRegistry::new("app");
179        registry.current().intern("value", Value::Number(1));
180        assert!(prepare_call(&registry, "app/missing", 1)
181            .unwrap_err()
182            .contains("unbound handler Var"));
183        assert!(prepare_call(&registry, "app/value", 1)
184            .unwrap_err()
185            .contains("not callable"));
186
187        registry.current().intern(
188            "handler",
189            native_function("handler", 1, |args| Ok(args[0].clone())),
190        );
191        let prepared = prepare_call(&registry, "app/handler", 1).unwrap();
192        assert!(matches!(
193            prepared.start(Vec::new()),
194            Err(message) if message.contains("expects 1")
195        ));
196    }
197}