Skip to content

Commit

Permalink
Foreign value conversion (#2056)
Browse files Browse the repository at this point in the history
  • Loading branch information
raviqqe authored Jan 29, 2025
1 parent 3cbf640 commit 0c3ed15
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 57 deletions.
16 changes: 16 additions & 0 deletions engine/src/engine.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use crate::{primitive_set::EnginePrimitiveSet, EngineError};
use any_fn::AnyFn;
use stak_module::Module;
#[cfg(doc)]
use stak_native::dynamic::DynamicPrimitiveSet;
use stak_native::dynamic::SchemeValue;
use stak_vm::{Error, Value, Vm};

const DEFAULT_VALUE_COUNT: usize = 1 << 10;
Expand All @@ -23,4 +26,17 @@ impl<'a, 'b, const N: usize> Engine<'a, 'b, N> {
self.vm.initialize(module.bytecode().iter().copied())?;
self.vm.run()
}

/// Registers a type compatible between Scheme and Rust.
///
/// We register all types that this crate implements [`SchemeValue`] for to
/// the engines by default.
///
/// For more information, see [`DynamicPrimitiveSet::register_type`].
pub fn register_type<T: SchemeValue + 'static>(&mut self) {
self.vm
.primitive_set_mut()
.dynamic_mut()
.register_type::<T>()
}
}
4 changes: 4 additions & 0 deletions engine/src/primitive_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ impl<'a, 'b, const N: usize> EnginePrimitiveSet<'a, 'b, N> {
dynamic: DynamicPrimitiveSet::new(functions),
}
}

pub(crate) fn dynamic_mut(&mut self) -> &mut DynamicPrimitiveSet<'a, 'b, N> {
&mut self.dynamic
}
}

impl<const N: usize> PrimitiveSet for EnginePrimitiveSet<'_, '_, N> {
Expand Down
155 changes: 98 additions & 57 deletions native/src/dynamic.rs
Original file line number Diff line number Diff line change
@@ -1,31 +1,71 @@
//! Native functions dynamically defined.
mod error;
mod scheme_value;

pub use self::error::DynamicError;
use alloc::{boxed::Box, string::String, vec, vec::Vec};
use any_fn::AnyFn;
use bitvec::bitvec;
use core::any::TypeId;
use heapless::Vec;
pub use scheme_value::SchemeValue;
use stak_vm::{Cons, Error, Memory, Number, PrimitiveSet, Type, Value};

const MAXIMUM_ARGUMENT_COUNT: usize = 16;

type ArgumentVec<T> = Vec<T, MAXIMUM_ARGUMENT_COUNT>;
type ArgumentVec<T> = heapless::Vec<T, MAXIMUM_ARGUMENT_COUNT>;
type SchemeType = (
TypeId,
Box<dyn Fn(&Memory, Value) -> Option<any_fn::Value>>,
Box<dyn Fn(&mut Memory, any_fn::Value) -> Result<Value, DynamicError>>,
);

/// A dynamic primitive set equipped with native functions in Rust.
pub struct DynamicPrimitiveSet<'a, 'b, const N: usize> {
functions: &'a mut [AnyFn<'b>],
types: Vec<SchemeType>,
values: [Option<any_fn::Value>; N],
}

impl<'a, 'b, const N: usize> DynamicPrimitiveSet<'a, 'b, N> {
/// Creates a primitive set.
pub fn new(functions: &'a mut [AnyFn<'b>]) -> Self {
Self {
let mut set = Self {
functions,
types: vec![],
values: [const { None }; N],
}
};

set.register_type::<bool>();
set.register_type::<i8>();
set.register_type::<u8>();
set.register_type::<i16>();
set.register_type::<u16>();
set.register_type::<i32>();
set.register_type::<u32>();
set.register_type::<i64>();
set.register_type::<u64>();
set.register_type::<f32>();
set.register_type::<f64>();
set.register_type::<isize>();
set.register_type::<usize>();
set.register_type::<String>();

set
}

/// Registers a type compatible between Scheme and Rust.
///
/// Values of such types are automatically marshalled when we pass them from
/// Scheme to Rust, and vice versa. Marshalling values can lead to the loss
/// of information (e.g. floating-point numbers in Scheme marshalled
/// into integers in Rust.)
pub fn register_type<T: SchemeValue + 'static>(&mut self) {
self.types.push((
TypeId::of::<T>(),
Box::new(|memory, value| T::from_scheme(memory, value).map(any_fn::value)),
Box::new(|memory, value| T::into_scheme(value.downcast()?, memory)),
));
}

fn collect_garbages(&mut self, memory: &Memory) -> Result<(), DynamicError> {
Expand Down Expand Up @@ -63,52 +103,47 @@ impl<'a, 'b, const N: usize> DynamicPrimitiveSet<'a, 'b, N> {
self.values.iter().position(Option::is_none)
}

fn convert_from_scheme(value: Value, type_id: TypeId) -> Option<any_fn::Value> {
// TODO Support more types.
if type_id == TypeId::of::<f32>() {
Some(any_fn::value(value.assume_number().to_f64() as f32))
} else if type_id == TypeId::of::<f64>() {
Some(any_fn::value(value.assume_number().to_f64()))
} else if type_id == TypeId::of::<i8>() {
Some(any_fn::value(value.assume_number().to_i64() as i8))
} else if type_id == TypeId::of::<u8>() {
Some(any_fn::value(value.assume_number().to_i64() as u8))
} else if type_id == TypeId::of::<isize>() {
Some(any_fn::value(value.assume_number().to_i64() as isize))
} else if type_id == TypeId::of::<usize>() {
Some(any_fn::value(value.assume_number().to_i64() as usize))
} else {
None
fn convert_from_scheme(
&self,
memory: &Memory,
value: Value,
type_id: TypeId,
) -> Option<any_fn::Value> {
for (id, from, _) in &self.types {
if type_id == *id {
return from(memory, value);
}
}

None
}

fn convert_into_scheme(
&mut self,
memory: &mut Memory,
value: any_fn::Value,
) -> Result<Value, DynamicError> {
// TODO Support more types.
Ok(if value.type_id()? == TypeId::of::<bool>() {
memory.boolean(value.downcast::<bool>()?).into()
} else if value.type_id()? == TypeId::of::<f64>() {
Number::from_f64(value.downcast::<f64>()?).into()
for (id, _, into) in &self.types {
if value.type_id()? == *id {
return into(memory, value);
}
}

let index = if let Some(index) = self.find_free() {
index
} else {
let index = if let Some(index) = self.find_free() {
index
} else {
self.collect_garbages(memory)?;
self.find_free().ok_or(Error::OutOfMemory)?
};
self.collect_garbages(memory)?;
self.find_free().ok_or(Error::OutOfMemory)?
};

self.values[index] = Some(value);
self.values[index] = Some(value);

let cons = memory.allocate(
Ok(memory
.allocate(
Number::from_i64(index as _).into(),
memory.null().set_tag(Type::Foreign as _).into(),
)?;

cons.into()
})
)?
.into())
}
}

Expand All @@ -118,21 +153,23 @@ impl<const N: usize> PrimitiveSet for DynamicPrimitiveSet<'_, '_, N> {
fn operate(&mut self, memory: &mut Memory, primitive: usize) -> Result<(), Self::Error> {
let function = self
.functions
.get_mut(primitive)
.get(primitive)
.ok_or(Error::IllegalPrimitive)?;

let mut arguments = (0..function.arity())
.map(|_| memory.pop())
.collect::<ArgumentVec<_>>();
arguments.reverse();

let cloned_arguments = arguments
.iter()
.enumerate()
.map(|(index, &value)| {
Self::convert_from_scheme(value, function.parameter_types()[index])
})
.collect::<ArgumentVec<_>>();
let cloned_arguments = {
arguments
.iter()
.enumerate()
.map(|(index, &value)| {
self.convert_from_scheme(memory, value, function.parameter_types()[index])
})
.collect::<ArgumentVec<_>>()
};

let mut copied_arguments = ArgumentVec::new();

Expand All @@ -154,18 +191,22 @@ impl<const N: usize> PrimitiveSet for DynamicPrimitiveSet<'_, '_, N> {
.map_err(|_| Error::ArgumentCount)?;
}

let value = function.call(
copied_arguments
.into_iter()
.enumerate()
.map(|(index, value)| {
cloned_arguments[index]
.as_ref()
.map_or_else(|| value.ok_or(DynamicError::ForeignValueExpected), Ok)
})
.collect::<Result<ArgumentVec<_>, DynamicError>>()?
.as_slice(),
)?;
let value = self
.functions
.get_mut(primitive)
.ok_or(Error::IllegalPrimitive)?
.call(
copied_arguments
.into_iter()
.enumerate()
.map(|(index, value)| {
cloned_arguments[index]
.as_ref()
.map_or_else(|| value.ok_or(DynamicError::ForeignValueExpected), Ok)
})
.collect::<Result<ArgumentVec<_>, DynamicError>>()?
.as_slice(),
)?;

let value = self.convert_into_scheme(memory, value)?;
memory.push(value)?;
Expand Down
129 changes: 129 additions & 0 deletions native/src/dynamic/scheme_value.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
use super::DynamicError;
use alloc::string::String;
use stak_vm::{Memory, Number, Type, Value};

/// A trait to convert Rust values from and into Scheme values.
pub trait SchemeValue: Sized {
/// Converts a Scheme value into a Rust value.
fn from_scheme(memory: &Memory, value: Value) -> Option<Self>;

/// Converts a Rust value into a Scheme value.
fn into_scheme(self, memory: &mut Memory) -> Result<Value, DynamicError>;
}

impl SchemeValue for bool {
fn from_scheme(memory: &Memory, value: Value) -> Option<Self> {
Some(value == memory.boolean(false).into())
}

fn into_scheme(self, memory: &mut Memory) -> Result<Value, DynamicError> {
Ok(memory.boolean(self).into())
}
}

macro_rules! implement_integer {
($type:ty) => {
impl SchemeValue for $type {
fn from_scheme(_memory: &Memory, value: Value) -> Option<Self> {
Some(value.assume_number().to_i64() as _)
}

fn into_scheme(self, _memory: &mut Memory) -> Result<Value, DynamicError> {
Ok(Number::from_i64(self as _).into())
}
}
};
}

implement_integer!(i8);
implement_integer!(u8);
implement_integer!(u16);
implement_integer!(i16);
implement_integer!(i32);
implement_integer!(u32);
implement_integer!(i64);
implement_integer!(u64);
implement_integer!(isize);
implement_integer!(usize);

macro_rules! implement_float {
($type:ty) => {
impl SchemeValue for $type {
fn from_scheme(_memory: &Memory, value: Value) -> Option<Self> {
Some(value.assume_number().to_f64() as _)
}

fn into_scheme(self, _memory: &mut Memory) -> Result<Value, DynamicError> {
Ok(Number::from_f64(self as _).into())
}
}
};
}

implement_float!(f32);
implement_float!(f64);

impl SchemeValue for String {
fn from_scheme(memory: &Memory, value: Value) -> Option<Self> {
let cons = value.assume_cons();
let mut string = Self::with_capacity(memory.car(cons).assume_number().to_i64() as _);
let mut cons = memory.cdr(cons).assume_cons();

while cons != memory.null() {
string.push(char::from_u32(
memory.car(cons).assume_number().to_i64() as _
)?);
cons = memory.cdr(cons).assume_cons();
}

Some(string)
}

fn into_scheme(self, memory: &mut Memory) -> Result<Value, DynamicError> {
let mut length = 0;
let mut cons = memory.null();

for character in self.chars().rev() {
cons = memory.cons(Number::from_i64(character as _).into(), cons)?;
length += 1;
}

Ok(memory
.allocate(
Number::from_i64(length).into(),
cons.set_tag(Type::String as _).into(),
)?
.into())
}
}

#[cfg(test)]
mod tests {
use super::*;

mod string {
use super::*;

#[test]
fn ascii() {
let mut heap = [Default::default(); 256];
let mut memory = Memory::new(&mut heap).unwrap();
let string = "tomato";

let value = String::from(string).into_scheme(&mut memory).unwrap();

assert_eq!(&String::from_scheme(&memory, value).unwrap(), string);
}

#[test]
fn unicode() {
let mut heap = [Default::default(); 256];
let mut memory = Memory::new(&mut heap).unwrap();
let string = "あ阿😄";

let value = String::from(string).into_scheme(&mut memory).unwrap();

assert_eq!(&String::from_scheme(&memory, value).unwrap(), string);
}
}
}

0 comments on commit 0c3ed15

Please sign in to comment.