use std::any::Any;
use std::collections::BTreeMap;
use std::fmt;
use std::fs;
use std::io;
use std::path::Path;
use kinetik_diag::{Diagnostic, SourceFile, SourceId, Span};
use kinetik_parse::parse_module;
use kinetik_runtime::HostId;
pub use kinetik_runtime::{HostHandle, Value};
use kinetik_vm::{HostNativeResult, RuntimeError, Vm};
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Clone, Debug, PartialEq)]
pub struct ReloadReport {
pub value: Value,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Debug)]
pub enum Error {
Io(io::Error),
Parse {
source: Box<SourceFile>,
diagnostics: Vec<Diagnostic>,
},
Runtime {
source: Option<Box<SourceFile>>,
error: Box<RuntimeError>,
},
MissingFunction(String),
Conversion(String),
StaleHostHandle {
id: u64,
},
HostTypeMismatch {
expected: &'static str,
},
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(error) => write!(f, "failed to read script: {error}"),
Self::Parse { diagnostics, .. } => {
write!(
f,
"failed to parse script with {} diagnostic(s)",
diagnostics.len()
)
}
Self::Runtime { error, .. } => write!(f, "runtime error: {}", error.message()),
Self::MissingFunction(name) => write!(f, "function `{name}` is not defined"),
Self::Conversion(message) => f.write_str(message),
Self::StaleHostHandle { id } => write!(f, "stale host handle #{id}"),
Self::HostTypeMismatch { expected } => {
write!(f, "host handle does not contain `{expected}`")
}
}
}
}
impl std::error::Error for Error {}
impl From<io::Error> for Error {
fn from(error: io::Error) -> Self {
Self::Io(error)
}
}
#[derive(Debug)]
pub struct Kinetik {
vm: Vm,
sources: BTreeMap<SourceId, SourceFile>,
hosts: HostRegistry,
next_source_id: u32,
}
impl Default for Kinetik {
fn default() -> Self {
Self::new()
}
}
impl Kinetik {
#[must_use]
pub fn new() -> Self {
Self {
vm: Vm::new(),
sources: BTreeMap::new(),
hosts: HostRegistry::default(),
next_source_id: 1,
}
}
pub fn register_native<F>(&mut self, name: impl Into<String>, function: F)
where
F: Fn(&[Value]) -> HostNativeResult + Send + Sync + 'static,
{
self.vm.define_host_native(name, function);
}
pub fn insert_host<T>(&mut self, value: T) -> Value
where
T: Any + Send + Sync + 'static,
{
self.hosts.insert(value)
}
pub fn remove_host(&mut self, value: &Value) -> Result<()> {
self.hosts.remove(value)
}
pub fn with_host<T, R>(&self, value: &Value, read: impl FnOnce(&T) -> R) -> Result<R>
where
T: Any + Send + Sync + 'static,
{
self.hosts.with(value, read)
}
pub fn with_host_mut<T, R>(
&mut self,
value: &Value,
write: impl FnOnce(&mut T) -> R,
) -> Result<R>
where
T: Any + Send + Sync + 'static,
{
self.hosts.with_mut(value, write)
}
pub fn define_global(&mut self, name: impl Into<String>, value: Value) {
self.vm.define_global(name, value);
}
pub fn load_source(
&mut self,
name: impl Into<String>,
text: impl Into<String>,
) -> Result<Value> {
self.eval_source(name, text)
}
pub fn reload_source(
&mut self,
name: impl Into<String>,
text: impl Into<String>,
) -> Result<ReloadReport> {
let source = self.source_file(name, text);
let parsed = parse_module(&source);
if parsed.has_errors() {
return Err(Error::Parse {
source: Box::new(source),
diagnostics: parsed.diagnostics,
});
}
let Some(module) = parsed.node else {
return Err(Error::Conversion(String::from(
"parser did not produce a module",
)));
};
let previous = self.vm.globals();
let stale_tasks = self.vm.cancel_stale_tasks();
self.sources.insert(source.id(), source.clone());
let value = self
.vm
.eval_module(&module)
.map_err(|error| self.runtime_error(error))?;
let mut diagnostics = preserve_compatible_globals(&mut self.vm, &previous, module.span);
if stale_tasks > 0 {
diagnostics.push(
Diagnostic::warning(format!(
"cancelled {stale_tasks} stale task(s) during reload"
))
.with_span(module.span),
);
}
Ok(ReloadReport { value, diagnostics })
}
fn eval_source(&mut self, name: impl Into<String>, text: impl Into<String>) -> Result<Value> {
let source = self.source_file(name, text);
let parsed = parse_module(&source);
if parsed.has_errors() {
return Err(Error::Parse {
source: Box::new(source),
diagnostics: parsed.diagnostics,
});
}
self.sources.insert(source.id(), source.clone());
let Some(module) = parsed.node else {
return Err(Error::Conversion(String::from(
"parser did not produce a module",
)));
};
let value = self
.vm
.eval_module(&module)
.map_err(|error| self.runtime_error(error))?;
Ok(value)
}
pub fn load_file(&mut self, path: impl AsRef<Path>) -> Result<Value> {
let path = path.as_ref();
let text = fs::read_to_string(path)?;
self.load_source(path.display().to_string(), text)
}
pub fn call_function(&mut self, name: &str, args: &[Value]) -> Result<Vec<Value>> {
let callee = self
.vm
.get(name)
.ok_or_else(|| Error::MissingFunction(name.to_owned()))?;
self.vm
.call(&callee, args, Span::new(SourceId(0), 0, 0))
.map_err(|error| self.runtime_error(error))
}
#[must_use]
pub fn get(&self, name: &str) -> Option<Value> {
self.vm.get(name)
}
pub fn drain_output(&mut self) -> impl Iterator<Item = String> + '_ {
self.vm.drain_output()
}
fn source_file(&mut self, name: impl Into<String>, text: impl Into<String>) -> SourceFile {
let id = SourceId(self.next_source_id);
self.next_source_id += 1;
SourceFile::new(id, name, text)
}
fn runtime_error(&self, error: RuntimeError) -> Error {
Error::Runtime {
source: self
.sources
.get(&error.span().source)
.cloned()
.map(Box::new),
error: Box::new(error),
}
}
}
fn preserve_compatible_globals(
vm: &mut Vm,
previous: &BTreeMap<String, Value>,
span: Span,
) -> Vec<Diagnostic> {
let current = vm.globals();
let mut diagnostics = Vec::new();
for (name, old_value) in previous {
let Some(new_value) = current.get(name) else {
continue;
};
if matches!(new_value, Value::Function(_)) {
continue;
}
if old_value.type_name() == new_value.type_name() {
vm.define_global(name.clone(), old_value.clone());
} else {
diagnostics.push(
Diagnostic::warning(format!(
"reload recreated `{name}` because its type changed from {} to {}",
old_value.type_name(),
new_value.type_name()
))
.with_span(span),
);
}
}
vm.refresh_function_globals();
diagnostics
}
pub trait IntoValue {
fn into_value(self) -> Value;
}
impl IntoValue for Value {
fn into_value(self) -> Value {
self
}
}
impl IntoValue for () {
fn into_value(self) -> Value {
Value::Nil
}
}
impl IntoValue for bool {
fn into_value(self) -> Value {
Value::bool(self)
}
}
impl IntoValue for f64 {
fn into_value(self) -> Value {
Value::number(self)
}
}
impl IntoValue for String {
fn into_value(self) -> Value {
Value::string(self)
}
}
impl IntoValue for &str {
fn into_value(self) -> Value {
Value::string(self)
}
}
impl<T: IntoValue> IntoValue for Vec<T> {
fn into_value(self) -> Value {
Value::array(
self.into_iter()
.map(IntoValue::into_value)
.collect::<Vec<_>>(),
)
}
}
impl<T: IntoValue> IntoValue for BTreeMap<String, T> {
fn into_value(self) -> Value {
Value::object(
self.into_iter()
.map(|(key, value)| (key, value.into_value())),
)
}
}
pub trait FromValue: Sized {
fn from_value(value: Value) -> Result<Self>;
}
impl FromValue for Value {
fn from_value(value: Value) -> Result<Self> {
Ok(value)
}
}
impl FromValue for bool {
fn from_value(value: Value) -> Result<Self> {
match value {
Value::Bool(value) => Ok(value),
other => Err(type_error("bool", &other)),
}
}
}
impl FromValue for f64 {
fn from_value(value: Value) -> Result<Self> {
match value {
Value::Number(value) => Ok(value),
other => Err(type_error("number", &other)),
}
}
}
impl FromValue for String {
fn from_value(value: Value) -> Result<Self> {
match value {
Value::String(value) => Ok(value),
other => Err(type_error("string", &other)),
}
}
}
impl<T: FromValue> FromValue for Vec<T> {
fn from_value(value: Value) -> Result<Self> {
match value {
Value::Array(array) => array
.elements()
.iter()
.cloned()
.map(T::from_value)
.collect::<Result<Vec<_>>>(),
other => Err(type_error("array", &other)),
}
}
}
impl<T: FromValue> FromValue for BTreeMap<String, T> {
fn from_value(value: Value) -> Result<Self> {
match value {
Value::Object(object) => object
.fields()
.iter()
.map(|(key, value)| T::from_value(value.clone()).map(|value| (key.clone(), value)))
.collect::<Result<BTreeMap<_, _>>>(),
other => Err(type_error("object", &other)),
}
}
}
fn type_error(expected: &str, value: &Value) -> Error {
Error::Conversion(format!("expected {expected}, got {}", value.type_name()))
}
#[derive(Default)]
struct HostRegistry {
entries: BTreeMap<HostId, HostEntry>,
next_id: u64,
}
impl fmt::Debug for HostRegistry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HostRegistry")
.field("entry_count", &self.entries.len())
.field("next_id", &self.next_id)
.finish()
}
}
impl HostRegistry {
fn insert<T>(&mut self, value: T) -> Value
where
T: Any + Send + Sync + 'static,
{
self.next_id += 1;
let id = HostId::new(self.next_id);
let generation = 1;
self.entries.insert(
id,
HostEntry {
generation,
value: Box::new(value),
},
);
Value::host(id, generation)
}
fn remove(&mut self, value: &Value) -> Result<()> {
let handle = host_handle(value)?;
let entry = self
.entries
.get(&handle.id())
.ok_or_else(|| stale_handle(handle))?;
if entry.generation != handle.generation() {
return Err(stale_handle(handle));
}
self.entries.remove(&handle.id());
Ok(())
}
fn with<T, R>(&self, value: &Value, read: impl FnOnce(&T) -> R) -> Result<R>
where
T: Any + Send + Sync + 'static,
{
let entry = self.entry(value)?;
let Some(value) = entry.value.downcast_ref::<T>() else {
return Err(Error::HostTypeMismatch {
expected: std::any::type_name::<T>(),
});
};
Ok(read(value))
}
fn with_mut<T, R>(&mut self, value: &Value, write: impl FnOnce(&mut T) -> R) -> Result<R>
where
T: Any + Send + Sync + 'static,
{
let entry = self.entry_mut(value)?;
let Some(value) = entry.value.downcast_mut::<T>() else {
return Err(Error::HostTypeMismatch {
expected: std::any::type_name::<T>(),
});
};
Ok(write(value))
}
fn entry(&self, value: &Value) -> Result<&HostEntry> {
let handle = host_handle(value)?;
let entry = self
.entries
.get(&handle.id())
.ok_or_else(|| stale_handle(handle))?;
if entry.generation == handle.generation() {
Ok(entry)
} else {
Err(stale_handle(handle))
}
}
fn entry_mut(&mut self, value: &Value) -> Result<&mut HostEntry> {
let handle = host_handle(value)?;
let entry = self
.entries
.get_mut(&handle.id())
.ok_or_else(|| stale_handle(handle))?;
if entry.generation == handle.generation() {
Ok(entry)
} else {
Err(stale_handle(handle))
}
}
}
struct HostEntry {
generation: u64,
value: Box<dyn Any + Send + Sync>,
}
fn host_handle(value: &Value) -> Result<HostHandle> {
match value {
Value::Host(handle) => Ok(*handle),
other => Err(type_error("native host handle", other)),
}
}
fn stale_handle(handle: HostHandle) -> Error {
Error::StaleHostHandle {
id: handle.id().get(),
}
}
#[cfg(test)]
mod tests {
use super::{Error, FromValue, IntoValue, Kinetik, Value};
#[test]
fn converts_core_values() {
assert_eq!(true.into_value(), Value::bool(true));
assert_eq!(String::from("hi").into_value(), Value::string("hi"));
let value = vec![1.0, 2.0].into_value();
assert_eq!(
Vec::<f64>::from_value(value).expect("array"),
vec![1.0, 2.0]
);
}
#[test]
fn creates_runtime() {
let mut runtime = Kinetik::new();
runtime
.load_source("test.kn", "let x = 2\n")
.expect("loads source");
assert_eq!(runtime.get("x"), Some(Value::number(2.0)));
}
#[test]
fn stores_and_invalidates_host_handles() {
let mut runtime = Kinetik::new();
let handle = runtime.insert_host(String::from("player"));
let name = runtime
.with_host::<String, _>(&handle, Clone::clone)
.expect("host value");
assert_eq!(name, "player");
runtime
.with_host_mut::<String, _>(&handle, |name| name.push_str("-1"))
.expect("host value mut");
assert_eq!(
runtime
.with_host::<String, _>(&handle, Clone::clone)
.expect("host value"),
"player-1"
);
runtime.remove_host(&handle).expect("removes host value");
assert!(matches!(
runtime.with_host::<String, _>(&handle, Clone::clone),
Err(Error::StaleHostHandle { .. })
));
}
}