Skip to main content

kinetik_embed/
lib.rs

1//! Rust embedding API for Kinetik.
2
3use std::any::Any;
4use std::collections::BTreeMap;
5use std::fmt;
6use std::fs;
7use std::io;
8use std::path::Path;
9
10use kinetik_diag::{Diagnostic, SourceFile, SourceId, Span};
11use kinetik_parse::parse_module;
12use kinetik_runtime::HostId;
13pub use kinetik_runtime::{HostHandle, Value};
14use kinetik_vm::{HostNativeResult, RuntimeError, Vm};
15
16/// Convenient result type for Kinetik embedding operations.
17pub type Result<T> = std::result::Result<T, Error>;
18
19/// Result of a successful hot reload.
20#[derive(Clone, Debug, PartialEq)]
21pub struct ReloadReport {
22    /// Value produced by evaluating the reloaded source.
23    pub value: Value,
24    /// Non-fatal reload diagnostics such as incompatible preserved values.
25    pub diagnostics: Vec<Diagnostic>,
26}
27
28/// Error returned by the Kinetik embedding API.
29#[derive(Debug)]
30pub enum Error {
31    /// A script file could not be read.
32    Io(io::Error),
33    /// Source text could not be parsed.
34    Parse {
35        /// Source file associated with the diagnostics.
36        source: Box<SourceFile>,
37        /// Diagnostics emitted by lexing or parsing.
38        diagnostics: Vec<Diagnostic>,
39    },
40    /// Runtime evaluation failed.
41    Runtime {
42        /// Source file associated with the runtime span.
43        source: Option<Box<SourceFile>>,
44        /// Runtime error emitted by the VM.
45        error: Box<RuntimeError>,
46    },
47    /// A requested function is not defined.
48    MissingFunction(String),
49    /// A value could not be converted to a requested Rust type.
50    Conversion(String),
51    /// A host handle no longer refers to a live host object.
52    StaleHostHandle {
53        /// Raw host handle id.
54        id: u64,
55    },
56    /// A host handle was accessed as the wrong Rust type.
57    HostTypeMismatch {
58        /// Expected Rust type name.
59        expected: &'static str,
60    },
61}
62
63impl fmt::Display for Error {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        match self {
66            Self::Io(error) => write!(f, "failed to read script: {error}"),
67            Self::Parse { diagnostics, .. } => {
68                write!(
69                    f,
70                    "failed to parse script with {} diagnostic(s)",
71                    diagnostics.len()
72                )
73            }
74            Self::Runtime { error, .. } => write!(f, "runtime error: {}", error.message()),
75            Self::MissingFunction(name) => write!(f, "function `{name}` is not defined"),
76            Self::Conversion(message) => f.write_str(message),
77            Self::StaleHostHandle { id } => write!(f, "stale host handle #{id}"),
78            Self::HostTypeMismatch { expected } => {
79                write!(f, "host handle does not contain `{expected}`")
80            }
81        }
82    }
83}
84
85impl std::error::Error for Error {}
86
87impl From<io::Error> for Error {
88    fn from(error: io::Error) -> Self {
89        Self::Io(error)
90    }
91}
92
93/// Embedded Kinetik runtime.
94#[derive(Debug)]
95pub struct Kinetik {
96    vm: Vm,
97    sources: BTreeMap<SourceId, SourceFile>,
98    hosts: HostRegistry,
99    next_source_id: u32,
100}
101
102impl Default for Kinetik {
103    fn default() -> Self {
104        Self::new()
105    }
106}
107
108impl Kinetik {
109    /// Creates a new embedded runtime.
110    #[must_use]
111    pub fn new() -> Self {
112        Self {
113            vm: Vm::new(),
114            sources: BTreeMap::new(),
115            hosts: HostRegistry::default(),
116            next_source_id: 1,
117        }
118    }
119
120    /// Registers a host-native function in the root environment.
121    pub fn register_native<F>(&mut self, name: impl Into<String>, function: F)
122    where
123        F: Fn(&[Value]) -> HostNativeResult + Send + Sync + 'static,
124    {
125        self.vm.define_host_native(name, function);
126    }
127
128    /// Stores a host-owned object and returns an opaque script value for it.
129    pub fn insert_host<T>(&mut self, value: T) -> Value
130    where
131        T: Any + Send + Sync + 'static,
132    {
133        self.hosts.insert(value)
134    }
135
136    /// Removes a host-owned object, making existing handles stale.
137    ///
138    /// # Errors
139    ///
140    /// Returns a stale handle error when the value is not a live host handle.
141    pub fn remove_host(&mut self, value: &Value) -> Result<()> {
142        self.hosts.remove(value)
143    }
144
145    /// Accesses a host object through a closure without exposing raw references.
146    ///
147    /// # Errors
148    ///
149    /// Returns a stale handle or type mismatch error when the handle is invalid.
150    pub fn with_host<T, R>(&self, value: &Value, read: impl FnOnce(&T) -> R) -> Result<R>
151    where
152        T: Any + Send + Sync + 'static,
153    {
154        self.hosts.with(value, read)
155    }
156
157    /// Mutates a host object through a closure without exposing raw references.
158    ///
159    /// # Errors
160    ///
161    /// Returns a stale handle or type mismatch error when the handle is invalid.
162    pub fn with_host_mut<T, R>(
163        &mut self,
164        value: &Value,
165        write: impl FnOnce(&mut T) -> R,
166    ) -> Result<R>
167    where
168        T: Any + Send + Sync + 'static,
169    {
170        self.hosts.with_mut(value, write)
171    }
172
173    /// Defines a global script value.
174    pub fn define_global(&mut self, name: impl Into<String>, value: Value) {
175        self.vm.define_global(name, value);
176    }
177
178    /// Loads, parses, and evaluates a Kinetik source string.
179    ///
180    /// # Errors
181    ///
182    /// Returns parse diagnostics or a runtime error when script loading fails.
183    pub fn load_source(
184        &mut self,
185        name: impl Into<String>,
186        text: impl Into<String>,
187    ) -> Result<Value> {
188        self.eval_source(name, text)
189    }
190
191    /// Parses and reloads a Kinetik source string into the running runtime.
192    ///
193    /// Parse errors are reported before the current runtime is mutated, so the
194    /// previously loaded script functions remain callable after a bad edit.
195    ///
196    /// # Errors
197    ///
198    /// Returns parse diagnostics or a runtime error when script reloading fails.
199    pub fn reload_source(
200        &mut self,
201        name: impl Into<String>,
202        text: impl Into<String>,
203    ) -> Result<ReloadReport> {
204        let source = self.source_file(name, text);
205        let parsed = parse_module(&source);
206        if parsed.has_errors() {
207            return Err(Error::Parse {
208                source: Box::new(source),
209                diagnostics: parsed.diagnostics,
210            });
211        }
212
213        let Some(module) = parsed.node else {
214            return Err(Error::Conversion(String::from(
215                "parser did not produce a module",
216            )));
217        };
218
219        let previous = self.vm.globals();
220        let stale_tasks = self.vm.cancel_stale_tasks();
221        self.sources.insert(source.id(), source.clone());
222        let value = self
223            .vm
224            .eval_module(&module)
225            .map_err(|error| self.runtime_error(error))?;
226
227        let mut diagnostics = preserve_compatible_globals(&mut self.vm, &previous, module.span);
228        if stale_tasks > 0 {
229            diagnostics.push(
230                Diagnostic::warning(format!(
231                    "cancelled {stale_tasks} stale task(s) during reload"
232                ))
233                .with_span(module.span),
234            );
235        }
236
237        Ok(ReloadReport { value, diagnostics })
238    }
239
240    fn eval_source(&mut self, name: impl Into<String>, text: impl Into<String>) -> Result<Value> {
241        let source = self.source_file(name, text);
242        let parsed = parse_module(&source);
243        if parsed.has_errors() {
244            return Err(Error::Parse {
245                source: Box::new(source),
246                diagnostics: parsed.diagnostics,
247            });
248        }
249
250        self.sources.insert(source.id(), source.clone());
251        let Some(module) = parsed.node else {
252            return Err(Error::Conversion(String::from(
253                "parser did not produce a module",
254            )));
255        };
256        let value = self
257            .vm
258            .eval_module(&module)
259            .map_err(|error| self.runtime_error(error))?;
260        Ok(value)
261    }
262
263    /// Loads, parses, and evaluates a Kinetik script file.
264    ///
265    /// # Errors
266    ///
267    /// Returns I/O, parse, or runtime errors when script loading fails.
268    pub fn load_file(&mut self, path: impl AsRef<Path>) -> Result<Value> {
269        let path = path.as_ref();
270        let text = fs::read_to_string(path)?;
271        self.load_source(path.display().to_string(), text)
272    }
273
274    /// Calls a named script or native function.
275    ///
276    /// # Errors
277    ///
278    /// Returns an error when the function is missing, not callable, or fails.
279    pub fn call_function(&mut self, name: &str, args: &[Value]) -> Result<Vec<Value>> {
280        let callee = self
281            .vm
282            .get(name)
283            .ok_or_else(|| Error::MissingFunction(name.to_owned()))?;
284        self.vm
285            .call(&callee, args, Span::new(SourceId(0), 0, 0))
286            .map_err(|error| self.runtime_error(error))
287    }
288
289    /// Reads a global value from the embedded runtime.
290    #[must_use]
291    pub fn get(&self, name: &str) -> Option<Value> {
292        self.vm.get(name)
293    }
294
295    /// Drains lines produced by the default standard-library `print` function.
296    pub fn drain_output(&mut self) -> impl Iterator<Item = String> + '_ {
297        self.vm.drain_output()
298    }
299
300    fn source_file(&mut self, name: impl Into<String>, text: impl Into<String>) -> SourceFile {
301        let id = SourceId(self.next_source_id);
302        self.next_source_id += 1;
303        SourceFile::new(id, name, text)
304    }
305
306    fn runtime_error(&self, error: RuntimeError) -> Error {
307        Error::Runtime {
308            source: self
309                .sources
310                .get(&error.span().source)
311                .cloned()
312                .map(Box::new),
313            error: Box::new(error),
314        }
315    }
316}
317
318fn preserve_compatible_globals(
319    vm: &mut Vm,
320    previous: &BTreeMap<String, Value>,
321    span: Span,
322) -> Vec<Diagnostic> {
323    let current = vm.globals();
324    let mut diagnostics = Vec::new();
325
326    for (name, old_value) in previous {
327        let Some(new_value) = current.get(name) else {
328            continue;
329        };
330        if matches!(new_value, Value::Function(_)) {
331            continue;
332        }
333        if old_value.type_name() == new_value.type_name() {
334            vm.define_global(name.clone(), old_value.clone());
335        } else {
336            diagnostics.push(
337                Diagnostic::warning(format!(
338                    "reload recreated `{name}` because its type changed from {} to {}",
339                    old_value.type_name(),
340                    new_value.type_name()
341                ))
342                .with_span(span),
343            );
344        }
345    }
346
347    vm.refresh_function_globals();
348    diagnostics
349}
350
351/// Converts Rust values into Kinetik runtime values.
352pub trait IntoValue {
353    /// Converts this value into a Kinetik value.
354    fn into_value(self) -> Value;
355}
356
357impl IntoValue for Value {
358    fn into_value(self) -> Value {
359        self
360    }
361}
362
363impl IntoValue for () {
364    fn into_value(self) -> Value {
365        Value::Nil
366    }
367}
368
369impl IntoValue for bool {
370    fn into_value(self) -> Value {
371        Value::bool(self)
372    }
373}
374
375impl IntoValue for f64 {
376    fn into_value(self) -> Value {
377        Value::number(self)
378    }
379}
380
381impl IntoValue for String {
382    fn into_value(self) -> Value {
383        Value::string(self)
384    }
385}
386
387impl IntoValue for &str {
388    fn into_value(self) -> Value {
389        Value::string(self)
390    }
391}
392
393impl<T: IntoValue> IntoValue for Vec<T> {
394    fn into_value(self) -> Value {
395        Value::array(
396            self.into_iter()
397                .map(IntoValue::into_value)
398                .collect::<Vec<_>>(),
399        )
400    }
401}
402
403impl<T: IntoValue> IntoValue for BTreeMap<String, T> {
404    fn into_value(self) -> Value {
405        Value::object(
406            self.into_iter()
407                .map(|(key, value)| (key, value.into_value())),
408        )
409    }
410}
411
412/// Converts Kinetik runtime values into Rust values.
413pub trait FromValue: Sized {
414    /// Converts a Kinetik value into this Rust type.
415    ///
416    /// # Errors
417    ///
418    /// Returns a conversion error when the runtime value has the wrong type.
419    fn from_value(value: Value) -> Result<Self>;
420}
421
422impl FromValue for Value {
423    fn from_value(value: Value) -> Result<Self> {
424        Ok(value)
425    }
426}
427
428impl FromValue for bool {
429    fn from_value(value: Value) -> Result<Self> {
430        match value {
431            Value::Bool(value) => Ok(value),
432            other => Err(type_error("bool", &other)),
433        }
434    }
435}
436
437impl FromValue for f64 {
438    fn from_value(value: Value) -> Result<Self> {
439        match value {
440            Value::Number(value) => Ok(value),
441            other => Err(type_error("number", &other)),
442        }
443    }
444}
445
446impl FromValue for String {
447    fn from_value(value: Value) -> Result<Self> {
448        match value {
449            Value::String(value) => Ok(value),
450            other => Err(type_error("string", &other)),
451        }
452    }
453}
454
455impl<T: FromValue> FromValue for Vec<T> {
456    fn from_value(value: Value) -> Result<Self> {
457        match value {
458            Value::Array(array) => array
459                .elements()
460                .iter()
461                .cloned()
462                .map(T::from_value)
463                .collect::<Result<Vec<_>>>(),
464            other => Err(type_error("array", &other)),
465        }
466    }
467}
468
469impl<T: FromValue> FromValue for BTreeMap<String, T> {
470    fn from_value(value: Value) -> Result<Self> {
471        match value {
472            Value::Object(object) => object
473                .fields()
474                .iter()
475                .map(|(key, value)| T::from_value(value.clone()).map(|value| (key.clone(), value)))
476                .collect::<Result<BTreeMap<_, _>>>(),
477            other => Err(type_error("object", &other)),
478        }
479    }
480}
481
482fn type_error(expected: &str, value: &Value) -> Error {
483    Error::Conversion(format!("expected {expected}, got {}", value.type_name()))
484}
485
486#[derive(Default)]
487struct HostRegistry {
488    entries: BTreeMap<HostId, HostEntry>,
489    next_id: u64,
490}
491
492impl fmt::Debug for HostRegistry {
493    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
494        f.debug_struct("HostRegistry")
495            .field("entry_count", &self.entries.len())
496            .field("next_id", &self.next_id)
497            .finish()
498    }
499}
500
501impl HostRegistry {
502    fn insert<T>(&mut self, value: T) -> Value
503    where
504        T: Any + Send + Sync + 'static,
505    {
506        self.next_id += 1;
507        let id = HostId::new(self.next_id);
508        let generation = 1;
509        self.entries.insert(
510            id,
511            HostEntry {
512                generation,
513                value: Box::new(value),
514            },
515        );
516        Value::host(id, generation)
517    }
518
519    fn remove(&mut self, value: &Value) -> Result<()> {
520        let handle = host_handle(value)?;
521        let entry = self
522            .entries
523            .get(&handle.id())
524            .ok_or_else(|| stale_handle(handle))?;
525        if entry.generation != handle.generation() {
526            return Err(stale_handle(handle));
527        }
528        self.entries.remove(&handle.id());
529        Ok(())
530    }
531
532    fn with<T, R>(&self, value: &Value, read: impl FnOnce(&T) -> R) -> Result<R>
533    where
534        T: Any + Send + Sync + 'static,
535    {
536        let entry = self.entry(value)?;
537        let Some(value) = entry.value.downcast_ref::<T>() else {
538            return Err(Error::HostTypeMismatch {
539                expected: std::any::type_name::<T>(),
540            });
541        };
542        Ok(read(value))
543    }
544
545    fn with_mut<T, R>(&mut self, value: &Value, write: impl FnOnce(&mut T) -> R) -> Result<R>
546    where
547        T: Any + Send + Sync + 'static,
548    {
549        let entry = self.entry_mut(value)?;
550        let Some(value) = entry.value.downcast_mut::<T>() else {
551            return Err(Error::HostTypeMismatch {
552                expected: std::any::type_name::<T>(),
553            });
554        };
555        Ok(write(value))
556    }
557
558    fn entry(&self, value: &Value) -> Result<&HostEntry> {
559        let handle = host_handle(value)?;
560        let entry = self
561            .entries
562            .get(&handle.id())
563            .ok_or_else(|| stale_handle(handle))?;
564        if entry.generation == handle.generation() {
565            Ok(entry)
566        } else {
567            Err(stale_handle(handle))
568        }
569    }
570
571    fn entry_mut(&mut self, value: &Value) -> Result<&mut HostEntry> {
572        let handle = host_handle(value)?;
573        let entry = self
574            .entries
575            .get_mut(&handle.id())
576            .ok_or_else(|| stale_handle(handle))?;
577        if entry.generation == handle.generation() {
578            Ok(entry)
579        } else {
580            Err(stale_handle(handle))
581        }
582    }
583}
584
585struct HostEntry {
586    generation: u64,
587    value: Box<dyn Any + Send + Sync>,
588}
589
590fn host_handle(value: &Value) -> Result<HostHandle> {
591    match value {
592        Value::Host(handle) => Ok(*handle),
593        other => Err(type_error("native host handle", other)),
594    }
595}
596
597fn stale_handle(handle: HostHandle) -> Error {
598    Error::StaleHostHandle {
599        id: handle.id().get(),
600    }
601}
602
603#[cfg(test)]
604mod tests {
605    use super::{Error, FromValue, IntoValue, Kinetik, Value};
606
607    #[test]
608    fn converts_core_values() {
609        assert_eq!(true.into_value(), Value::bool(true));
610        assert_eq!(String::from("hi").into_value(), Value::string("hi"));
611
612        let value = vec![1.0, 2.0].into_value();
613        assert_eq!(
614            Vec::<f64>::from_value(value).expect("array"),
615            vec![1.0, 2.0]
616        );
617    }
618
619    #[test]
620    fn creates_runtime() {
621        let mut runtime = Kinetik::new();
622        runtime
623            .load_source("test.kn", "let x = 2\n")
624            .expect("loads source");
625
626        assert_eq!(runtime.get("x"), Some(Value::number(2.0)));
627    }
628
629    #[test]
630    fn stores_and_invalidates_host_handles() {
631        let mut runtime = Kinetik::new();
632        let handle = runtime.insert_host(String::from("player"));
633
634        let name = runtime
635            .with_host::<String, _>(&handle, Clone::clone)
636            .expect("host value");
637        assert_eq!(name, "player");
638
639        runtime
640            .with_host_mut::<String, _>(&handle, |name| name.push_str("-1"))
641            .expect("host value mut");
642        assert_eq!(
643            runtime
644                .with_host::<String, _>(&handle, Clone::clone)
645                .expect("host value"),
646            "player-1"
647        );
648
649        runtime.remove_host(&handle).expect("removes host value");
650        assert!(matches!(
651            runtime.with_host::<String, _>(&handle, Clone::clone),
652            Err(Error::StaleHostHandle { .. })
653        ));
654    }
655}