extern crate alloc;
use alloc::format;
use alloc::sync::Arc;
use alloc::vec::Vec;
use crate::bytecode::GenericValue;
use crate::float::Float;
use crate::opaque::HostOpaque;
use crate::vm::VmError;
use crate::word::Word;
pub fn stale_flat_decode() -> VmError {
VmError::TypeError(alloc::string::String::from(
"flat composite body read after the arena was reset; decode a yielded or \
returned composite before the next resume (read-before-resume)",
))
}
pub struct RefContext<'a> {
pub arena: &'a keleusma_arena::Arena,
pub opaques: &'a [Arc<dyn HostOpaque>],
pub word_bytes: usize,
pub float_bytes: usize,
pub ref_epoch: u64,
}
pub trait KeleusmaType<W: Word, F: Float>: Sized {
fn from_value(v: &GenericValue<W, F>) -> Result<Self, VmError>;
fn into_value(self) -> GenericValue<W, F>;
fn into_value_ctx(self, ctx: &RefContext<'_>) -> Result<GenericValue<W, F>, VmError> {
self.into_value().into_arena_body(ctx.arena).map_err(|_| {
VmError::OutOfArena(alloc::string::String::from(
"arena exhausted building a native result composite body",
))
})
}
fn flat_field_kind() -> Option<crate::value_layout::ScalarKind> {
None
}
fn flat_byte_size(word_bytes: usize, float_bytes: usize) -> Option<usize> {
Self::flat_field_kind().map(|k| k.size_in_bytes(word_bytes, float_bytes))
}
fn from_flat_bytes(
bytes: &[u8],
word_bytes: usize,
float_bytes: usize,
) -> Result<Self, VmError> {
match Self::flat_field_kind() {
Some(kind) => {
let v = GenericValue::read_scalar_le(bytes, 0, kind, word_bytes, float_bytes)?;
Self::from_value(&v)
}
None => Err(VmError::TypeError(alloc::string::String::from(
"type has no flat byte representation",
))),
}
}
fn from_value_ctx(v: &GenericValue<W, F>, ctx: &RefContext<'_>) -> Result<Self, VmError> {
let _ = ctx;
Self::from_value(v)
}
fn from_flat_bytes_ctx(
bytes: &[u8],
word_bytes: usize,
float_bytes: usize,
ctx: &RefContext<'_>,
) -> Result<Self, VmError> {
let _ = ctx;
Self::from_flat_bytes(bytes, word_bytes, float_bytes)
}
fn to_flat_bytes(
self,
dst: &mut [u8],
word_bytes: usize,
float_bytes: usize,
) -> Result<(), VmError>
where
Self: Sized,
{
use crate::value_layout::ScalarKind;
match Self::flat_field_kind() {
Some(ScalarKind::Text) | Some(ScalarKind::Opaque) => {
Err(VmError::TypeError(alloc::string::String::from(
"reference field (Text or Opaque) cannot be written to a flat host buffer",
)))
}
Some(_) => {
self.into_value()
.write_scalar_le(dst, 0, word_bytes, float_bytes)?;
Ok(())
}
None => Err(VmError::TypeError(alloc::string::String::from(
"type has no flat byte representation",
))),
}
}
}
impl<W: Word, F: Float> KeleusmaType<W, F> for i64 {
fn from_value(v: &GenericValue<W, F>) -> Result<Self, VmError> {
match v {
GenericValue::Int(n) => Ok(W::to_i64(*n)),
other => Err(VmError::TypeError(format!(
"expected Word, got {}",
other.type_name()
))),
}
}
fn into_value(self) -> GenericValue<W, F> {
GenericValue::Int(W::from_i64_wrap(self))
}
fn flat_field_kind() -> Option<crate::value_layout::ScalarKind> {
Some(crate::value_layout::ScalarKind::Int)
}
}
impl<W: Word, F: Float> KeleusmaType<W, F> for u8 {
fn from_value(v: &GenericValue<W, F>) -> Result<Self, VmError> {
match v {
GenericValue::Byte(b) => Ok(*b),
other => Err(VmError::TypeError(format!(
"expected Byte, got {}",
other.type_name()
))),
}
}
fn into_value(self) -> GenericValue<W, F> {
GenericValue::Byte(self)
}
fn flat_field_kind() -> Option<crate::value_layout::ScalarKind> {
Some(crate::value_layout::ScalarKind::Byte)
}
}
#[cfg(feature = "floats")]
impl<W: Word, F: Float> KeleusmaType<W, F> for f64 {
fn from_value(v: &GenericValue<W, F>) -> Result<Self, VmError> {
match v {
GenericValue::Float(f) => Ok(F::to_f64(*f)),
GenericValue::Int(n) => {
const F64_SAFE_INT: i64 = 1 << 53;
let n = W::to_i64(*n);
if (-F64_SAFE_INT..=F64_SAFE_INT).contains(&n) {
Ok(n as f64)
} else {
Err(VmError::TypeError(format!(
"Int {} exceeds the f64 safe-integer range (±2^53); coercion to Float would lose precision",
n
)))
}
}
other => Err(VmError::TypeError(format!(
"expected Float, got {}",
other.type_name()
))),
}
}
fn into_value(self) -> GenericValue<W, F> {
GenericValue::Float(F::from_f64(self))
}
fn flat_field_kind() -> Option<crate::value_layout::ScalarKind> {
Some(crate::value_layout::ScalarKind::Float)
}
}
impl<W: Word, F: Float> KeleusmaType<W, F> for bool {
fn from_value(v: &GenericValue<W, F>) -> Result<Self, VmError> {
match v {
GenericValue::Bool(b) => Ok(*b),
other => Err(VmError::TypeError(format!(
"expected bool, got {}",
other.type_name()
))),
}
}
fn into_value(self) -> GenericValue<W, F> {
GenericValue::Bool(self)
}
fn flat_field_kind() -> Option<crate::value_layout::ScalarKind> {
Some(crate::value_layout::ScalarKind::Bool)
}
}
impl<W: Word, F: Float> KeleusmaType<W, F> for () {
fn from_value(v: &GenericValue<W, F>) -> Result<Self, VmError> {
match v {
GenericValue::Unit => Ok(()),
other => Err(VmError::TypeError(format!(
"expected unit, got {}",
other.type_name()
))),
}
}
fn into_value(self) -> GenericValue<W, F> {
GenericValue::Unit
}
fn flat_field_kind() -> Option<crate::value_layout::ScalarKind> {
Some(crate::value_layout::ScalarKind::Unit)
}
}
impl<W: Word, F: Float> KeleusmaType<W, F> for alloc::string::String {
fn from_value(v: &GenericValue<W, F>) -> Result<Self, VmError> {
match v {
GenericValue::StaticStr(s) => Ok(s.clone()),
GenericValue::KStr(_) => Err(VmError::TypeError(alloc::string::String::from(
"dynamic string requires a resolution context; decode through Vm::decode",
))),
other => Err(VmError::TypeError(format!(
"expected Text, got {}",
other.type_name()
))),
}
}
fn into_value(self) -> GenericValue<W, F> {
GenericValue::StaticStr(self)
}
fn flat_field_kind() -> Option<crate::value_layout::ScalarKind> {
Some(crate::value_layout::ScalarKind::Text)
}
fn from_flat_bytes(
_bytes: &[u8],
_word_bytes: usize,
_float_bytes: usize,
) -> Result<Self, VmError> {
Err(VmError::TypeError(alloc::string::String::from(
"flat Text field requires a resolution context; decode through Vm::decode",
)))
}
fn from_value_ctx(v: &GenericValue<W, F>, ctx: &RefContext<'_>) -> Result<Self, VmError> {
match v {
GenericValue::StaticStr(s) => Ok(s.clone()),
GenericValue::KStr(ks) => {
ks.get(ctx.arena)
.map(alloc::string::String::from)
.map_err(|_| {
VmError::TypeError(alloc::string::String::from(
"dynamic string is stale (arena reset since it was produced)",
))
})
}
other => Err(VmError::TypeError(format!(
"expected Text, got {}",
other.type_name()
))),
}
}
fn from_flat_bytes_ctx(
bytes: &[u8],
word_bytes: usize,
_float_bytes: usize,
ctx: &RefContext<'_>,
) -> Result<Self, VmError> {
let read_word = |o: usize| -> Result<usize, VmError> {
let mut buf = [0u8; 8];
buf[..word_bytes].copy_from_slice(flat_subslice(bytes, o, word_bytes)?);
Ok(u64::from_le_bytes(buf) as usize)
};
let ptr = read_word(0)?;
let len = read_word(word_bytes)?;
if ptr == 0 {
return Ok(alloc::string::String::new());
}
let ks = unsafe { crate::kstring::KString::from_raw_parts(ptr, len, ctx.ref_epoch) };
ks.get(ctx.arena)
.map(alloc::string::String::from)
.map_err(|_| {
VmError::TypeError(alloc::string::String::from(
"flat Text field is stale (arena reset since it was produced)",
))
})
}
}
impl<W: Word, F: Float> KeleusmaType<W, F> for Arc<dyn HostOpaque> {
fn from_value(v: &GenericValue<W, F>) -> Result<Self, VmError> {
match v {
GenericValue::Opaque(o) => Ok(Arc::clone(o)),
other => Err(VmError::TypeError(format!(
"expected opaque, got {}",
other.type_name()
))),
}
}
fn from_value_ctx(v: &GenericValue<W, F>, ctx: &RefContext<'_>) -> Result<Self, VmError> {
match v {
GenericValue::Opaque(o) => Ok(Arc::clone(o)),
GenericValue::OpaqueRef(i) => {
ctx.opaques.get(*i as usize).map(Arc::clone).ok_or_else(|| {
VmError::InvalidBytecode(alloc::string::String::from(
"opaque index does not resolve (stale or out of range)",
))
})
}
other => Err(VmError::TypeError(format!(
"expected opaque, got {}",
other.type_name()
))),
}
}
fn into_value(self) -> GenericValue<W, F> {
GenericValue::Opaque(self)
}
fn flat_field_kind() -> Option<crate::value_layout::ScalarKind> {
Some(crate::value_layout::ScalarKind::Opaque)
}
fn from_flat_bytes(
_bytes: &[u8],
_word_bytes: usize,
_float_bytes: usize,
) -> Result<Self, VmError> {
Err(VmError::TypeError(alloc::string::String::from(
"flat opaque field requires a resolution context; decode through Vm::decode",
)))
}
fn from_flat_bytes_ctx(
bytes: &[u8],
word_bytes: usize,
_float_bytes: usize,
ctx: &RefContext<'_>,
) -> Result<Self, VmError> {
let mut buf = [0u8; 8];
buf[..word_bytes].copy_from_slice(flat_subslice(bytes, 0, word_bytes)?);
let index = u64::from_le_bytes(buf) as usize;
ctx.opaques.get(index).map(Arc::clone).ok_or_else(|| {
VmError::InvalidBytecode(alloc::string::String::from(
"opaque field index does not resolve (stale or out of range)",
))
})
}
}
impl<W: Word, F: Float, T: KeleusmaType<W, F>> KeleusmaType<W, F> for Option<T> {
fn from_value(v: &GenericValue<W, F>) -> Result<Self, VmError> {
match v {
GenericValue::None => Ok(Option::None),
GenericValue::Enum(crate::bytecode::EnumBody::Boxed(be))
if be.type_name == "Option" && be.variant == "Some" && be.fields.len() == 1 =>
{
Ok(Some(T::from_value(&be.fields[0])?))
}
other => T::from_value(other).map(Some),
}
}
fn into_value(self) -> GenericValue<W, F> {
match self {
Some(t) => GenericValue::Enum(crate::bytecode::EnumBody::boxed_with_disc(
alloc::string::String::from("Option"),
alloc::string::String::from("Some"),
1,
alloc::vec![t.into_value()],
)),
Option::None => GenericValue::None,
}
}
fn from_value_ctx(v: &GenericValue<W, F>, ctx: &RefContext<'_>) -> Result<Self, VmError> {
match v {
GenericValue::None => Ok(Option::None),
GenericValue::Enum(crate::bytecode::EnumBody::Flat(fc)) => {
let bytes = fc.resolve(ctx.arena).map_err(|_| stale_flat_decode())?;
Self::from_flat_bytes_ctx(bytes, ctx.word_bytes, ctx.float_bytes, ctx)
}
GenericValue::Enum(crate::bytecode::EnumBody::Boxed(be))
if be.type_name == "Option" && be.variant == "Some" && be.fields.len() == 1 =>
{
Ok(Some(T::from_value_ctx(&be.fields[0], ctx)?))
}
other => T::from_value_ctx(other, ctx).map(Some),
}
}
fn into_value_ctx(self, ctx: &RefContext<'_>) -> Result<GenericValue<W, F>, VmError> {
let wb = ctx.word_bytes;
let fb = ctx.float_bytes;
if matches!(self, Option::None) {
return Ok(GenericValue::None);
}
match Self::flat_byte_size(wb, fb) {
Some(size) => {
let fc = crate::flat_value::FlatComposite::build_in_arena(ctx.arena, size, |dst| {
self.to_flat_bytes(dst, wb, fb).map_err(|_| ())
})
.map_err(|_| {
VmError::OutOfArena(alloc::string::String::from(
"arena exhausted building a flat Option native result",
))
})?
.ok_or_else(|| {
VmError::TypeError(alloc::string::String::from(
"flat Option payload is not flat-eligible",
))
})?;
Ok(GenericValue::Enum(crate::bytecode::EnumBody::Flat(fc)))
}
None => match self {
Some(t) => Ok(GenericValue::Enum(
crate::bytecode::EnumBody::boxed_with_disc(
alloc::string::String::from("Option"),
alloc::string::String::from("Some"),
1,
alloc::vec![t.into_value_ctx(ctx)?],
),
)),
Option::None => unreachable!("None handled above"),
},
}
}
fn flat_byte_size(word_bytes: usize, float_bytes: usize) -> Option<usize> {
Some(word_bytes + T::flat_byte_size(word_bytes, float_bytes)?)
}
fn from_flat_bytes(
bytes: &[u8],
word_bytes: usize,
float_bytes: usize,
) -> Result<Self, VmError> {
match read_flat_disc::<W, F>(bytes, word_bytes, float_bytes)? {
0 => Ok(Option::None),
1 => {
let psize = option_payload_size::<W, F, T>(word_bytes, float_bytes)?;
Ok(Some(T::from_flat_bytes(
flat_subslice(bytes, word_bytes, psize)?,
word_bytes,
float_bytes,
)?))
}
other => Err(VmError::TypeError(format!(
"unknown Option discriminant {}",
other
))),
}
}
fn to_flat_bytes(
self,
dst: &mut [u8],
word_bytes: usize,
float_bytes: usize,
) -> Result<(), VmError> {
match self {
Option::None => {
write_flat_disc::<W, F>(dst, 0, word_bytes, float_bytes)?;
for b in dst[word_bytes..].iter_mut() {
*b = 0;
}
Ok(())
}
Some(t) => {
write_flat_disc::<W, F>(dst, 1, word_bytes, float_bytes)?;
let psize = option_payload_size::<W, F, T>(word_bytes, float_bytes)?;
t.to_flat_bytes(
&mut dst[word_bytes..word_bytes + psize],
word_bytes,
float_bytes,
)?;
for b in dst[word_bytes + psize..].iter_mut() {
*b = 0;
}
Ok(())
}
}
}
fn from_flat_bytes_ctx(
bytes: &[u8],
word_bytes: usize,
float_bytes: usize,
ctx: &RefContext<'_>,
) -> Result<Self, VmError> {
match read_flat_disc::<W, F>(bytes, word_bytes, float_bytes)? {
0 => Ok(Option::None),
1 => {
let psize = option_payload_size::<W, F, T>(word_bytes, float_bytes)?;
Ok(Some(T::from_flat_bytes_ctx(
flat_subslice(bytes, word_bytes, psize)?,
word_bytes,
float_bytes,
ctx,
)?))
}
other => Err(VmError::TypeError(format!(
"unknown Option discriminant {}",
other
))),
}
}
}
#[doc(hidden)]
pub fn flat_subslice(bytes: &[u8], lo: usize, len: usize) -> Result<&[u8], VmError> {
lo.checked_add(len)
.and_then(|hi| bytes.get(lo..hi))
.ok_or_else(|| {
VmError::TypeError(alloc::string::String::from(
"flat composite body is shorter than its declared layout",
))
})
}
fn read_flat_disc<W: Word, F: Float>(
bytes: &[u8],
word_bytes: usize,
float_bytes: usize,
) -> Result<i64, VmError> {
match GenericValue::<W, F>::read_scalar_le(
bytes,
0,
crate::value_layout::ScalarKind::Int,
word_bytes,
float_bytes,
)? {
GenericValue::Int(w) => Ok(W::to_i64(w)),
_ => Err(VmError::TypeError(alloc::string::String::from(
"flat enum discriminant is not an Int",
))),
}
}
fn write_flat_disc<W: Word, F: Float>(
dst: &mut [u8],
disc: i64,
word_bytes: usize,
float_bytes: usize,
) -> Result<(), VmError> {
GenericValue::<W, F>::Int(W::from_i64_wrap(disc))
.write_scalar_le(dst, 0, word_bytes, float_bytes)
.map_err(VmError::from)
}
fn option_payload_size<W: Word, F: Float, T: KeleusmaType<W, F>>(
word_bytes: usize,
float_bytes: usize,
) -> Result<usize, VmError> {
T::flat_byte_size(word_bytes, float_bytes).ok_or_else(|| {
VmError::TypeError(alloc::string::String::from(
"Option payload type is not flat-eligible",
))
})
}
impl<W: Word, F: Float, T: KeleusmaType<W, F> + Clone, const N: usize> KeleusmaType<W, F>
for [T; N]
{
fn from_value(v: &GenericValue<W, F>) -> Result<Self, VmError> {
use crate::bytecode::ArrayBody;
match v {
GenericValue::Array(ArrayBody::Boxed(items)) => {
if items.len() != N {
return Err(VmError::TypeError(format!(
"expected array of length {}, got {}",
N,
items.len()
)));
}
let mut converted: Vec<T> = Vec::with_capacity(N);
for item in items.iter() {
converted.push(T::from_value(item)?);
}
converted.try_into().map_err(|_| {
VmError::TypeError(format!("failed to convert array of length {}", N))
})
}
GenericValue::Array(ArrayBody::Flat(_)) => {
Err(VmError::TypeError(alloc::string::String::from(
"cannot decode a flat (arena) array without an arena; use from_value_ctx",
)))
}
other => Err(VmError::TypeError(format!(
"expected array, got {}",
other.type_name()
))),
}
}
fn into_value(self) -> GenericValue<W, F> {
GenericValue::array_with_widths(
self.into_iter().map(|t| t.into_value()).collect(),
(1usize << <W as Word>::BITS_LOG2) / 8,
(1usize << <F as Float>::BITS_LOG2) / 8,
)
}
fn into_value_ctx(self, ctx: &RefContext<'_>) -> Result<GenericValue<W, F>, VmError> {
let elems = self
.into_iter()
.map(|t| t.into_value_ctx(ctx))
.collect::<Result<Vec<GenericValue<W, F>>, VmError>>()?;
GenericValue::array_in_arena(elems, ctx.word_bytes, ctx.float_bytes, ctx.arena).map_err(
|_| {
VmError::OutOfArena(alloc::string::String::from(
"arena exhausted building a native array result",
))
},
)
}
fn flat_byte_size(word_bytes: usize, float_bytes: usize) -> Option<usize> {
Some(N * <T as KeleusmaType<W, F>>::flat_byte_size(word_bytes, float_bytes)?)
}
fn from_flat_bytes(
bytes: &[u8],
word_bytes: usize,
float_bytes: usize,
) -> Result<Self, VmError> {
let esize = <T as KeleusmaType<W, F>>::flat_byte_size(word_bytes, float_bytes).ok_or_else(
|| {
VmError::TypeError(alloc::string::String::from(
"flat array element type is not flat-eligible",
))
},
)?;
let len = bytes.len().checked_div(esize).unwrap_or(N);
if len != N {
return Err(VmError::TypeError(format!(
"expected array of length {}, got {}",
N, len
)));
}
let mut converted: Vec<T> = Vec::with_capacity(N);
for i in 0..N {
let lo = i * esize;
converted.push(<T as KeleusmaType<W, F>>::from_flat_bytes(
&bytes[lo..lo + esize],
word_bytes,
float_bytes,
)?);
}
converted
.try_into()
.map_err(|_| VmError::TypeError(format!("failed to convert array of length {}", N)))
}
fn to_flat_bytes(
self,
dst: &mut [u8],
word_bytes: usize,
float_bytes: usize,
) -> Result<(), VmError> {
let esize = <T as KeleusmaType<W, F>>::flat_byte_size(word_bytes, float_bytes).ok_or_else(
|| {
VmError::TypeError(alloc::string::String::from(
"flat array element type is not flat-eligible",
))
},
)?;
for (i, t) in self.into_iter().enumerate() {
let lo = i * esize;
t.to_flat_bytes(&mut dst[lo..lo + esize], word_bytes, float_bytes)?;
}
Ok(())
}
fn from_value_ctx(v: &GenericValue<W, F>, ctx: &RefContext<'_>) -> Result<Self, VmError> {
use crate::bytecode::ArrayBody;
match v {
GenericValue::Array(ArrayBody::Boxed(items)) => {
if items.len() != N {
return Err(VmError::TypeError(format!(
"expected array of length {}, got {}",
N,
items.len()
)));
}
let mut converted: Vec<T> = Vec::with_capacity(N);
for item in items.iter() {
converted.push(T::from_value_ctx(item, ctx)?);
}
converted.try_into().map_err(|_| {
VmError::TypeError(format!("failed to convert array of length {}", N))
})
}
GenericValue::Array(ArrayBody::Flat(fc)) => {
let bytes = fc.resolve(ctx.arena).map_err(|_| stale_flat_decode())?;
Self::from_flat_bytes_ctx(bytes, ctx.word_bytes, ctx.float_bytes, ctx)
}
other => Err(VmError::TypeError(format!(
"expected array, got {}",
other.type_name()
))),
}
}
fn from_flat_bytes_ctx(
bytes: &[u8],
word_bytes: usize,
float_bytes: usize,
ctx: &RefContext<'_>,
) -> Result<Self, VmError> {
let esize = <T as KeleusmaType<W, F>>::flat_byte_size(word_bytes, float_bytes).ok_or_else(
|| {
VmError::TypeError(alloc::string::String::from(
"flat array element type is not flat-eligible",
))
},
)?;
let len = bytes.len().checked_div(esize).unwrap_or(N);
if len != N {
return Err(VmError::TypeError(format!(
"expected array of length {}, got {}",
N, len
)));
}
let mut converted: Vec<T> = Vec::with_capacity(N);
for i in 0..N {
let lo = i * esize;
converted.push(<T as KeleusmaType<W, F>>::from_flat_bytes_ctx(
&bytes[lo..lo + esize],
word_bytes,
float_bytes,
ctx,
)?);
}
converted
.try_into()
.map_err(|_| VmError::TypeError(format!("failed to convert array of length {}", N)))
}
}
macro_rules! impl_tuple {
($($name:ident: $idx:tt),*) => {
impl<W: Word, FloatT: Float, $($name: KeleusmaType<W, FloatT>),*>
KeleusmaType<W, FloatT> for ($($name,)*)
{
#[allow(clippy::unused_unit, unused_assignments, non_snake_case)]
fn from_value(v: &GenericValue<W, FloatT>) -> Result<Self, VmError> {
let expected = [$(stringify!($name),)*].len();
match v {
GenericValue::Tuple(crate::bytecode::TupleBody::Boxed(items)) => {
if items.len() != expected {
return Err(VmError::TypeError(format!(
"expected tuple of arity {}, got {}",
expected,
items.len()
)));
}
Ok(($($name::from_value(&items[$idx])?,)*))
}
GenericValue::Tuple(crate::bytecode::TupleBody::Flat(_)) => {
Err(VmError::TypeError(alloc::string::String::from(
"cannot decode a flat (arena) tuple without an arena; use from_value_ctx",
)))
}
other => Err(VmError::TypeError(format!(
"expected tuple, got {}",
other.type_name()
))),
}
}
#[allow(non_snake_case)]
fn into_value(self) -> GenericValue<W, FloatT> {
let ($($name,)*) = self;
GenericValue::tuple_with_widths(
::alloc::vec![$($name.into_value(),)*],
(1usize << <W as Word>::BITS_LOG2) / 8,
(1usize << <FloatT as Float>::BITS_LOG2) / 8,
)
}
#[allow(non_snake_case)]
fn into_value_ctx(self, __ctx: &RefContext<'_>)
-> Result<GenericValue<W, FloatT>, VmError>
{
let ($($name,)*) = self;
GenericValue::tuple_in_arena(
::alloc::vec![$($name.into_value_ctx(__ctx)?,)*],
__ctx.word_bytes,
__ctx.float_bytes,
__ctx.arena,
)
.map_err(|_| {
VmError::OutOfArena(::alloc::string::String::from(
"arena exhausted building a native tuple result",
))
})
}
#[allow(unused_assignments, unused_mut, unused_variables)]
fn flat_byte_size(word_bytes: usize, float_bytes: usize) -> Option<usize> {
let mut total = 0usize;
$(
total += <$name as KeleusmaType<W, FloatT>>::flat_byte_size(word_bytes, float_bytes)?;
)*
Some(total)
}
#[allow(unused_assignments, unused_mut, unused_variables, non_snake_case)]
fn from_flat_bytes(bytes: &[u8], word_bytes: usize, float_bytes: usize)
-> Result<Self, VmError>
{
let mut offset = 0usize;
Ok(($(
{
let size = <$name as KeleusmaType<W, FloatT>>::flat_byte_size(word_bytes, float_bytes)
.ok_or_else(|| VmError::TypeError(::alloc::string::String::from(
"flat tuple field is not flat-eligible",
)))?;
let val = <$name as KeleusmaType<W, FloatT>>::from_flat_bytes(
flat_subslice(bytes, offset, size)?, word_bytes, float_bytes,
)?;
offset += size;
val
},
)*))
}
#[allow(unused_assignments, unused_mut, unused_variables, non_snake_case)]
fn to_flat_bytes(self, dst: &mut [u8], word_bytes: usize, float_bytes: usize)
-> Result<(), VmError>
{
let ($($name,)*) = self;
let mut offset = 0usize;
$(
{
let size = <$name as KeleusmaType<W, FloatT>>::flat_byte_size(word_bytes, float_bytes)
.ok_or_else(|| VmError::TypeError(::alloc::string::String::from(
"flat tuple field is not flat-eligible",
)))?;
$name.to_flat_bytes(&mut dst[offset..offset + size], word_bytes, float_bytes)?;
offset += size;
}
)*
Ok(())
}
#[allow(clippy::unused_unit, unused_assignments, non_snake_case)]
fn from_value_ctx(v: &GenericValue<W, FloatT>, __ctx: &RefContext<'_>) -> Result<Self, VmError> {
let expected = [$(stringify!($name),)*].len();
match v {
GenericValue::Tuple(crate::bytecode::TupleBody::Boxed(items)) => {
if items.len() != expected {
return Err(VmError::TypeError(format!(
"expected tuple of arity {}, got {}",
expected,
items.len()
)));
}
Ok(($($name::from_value_ctx(&items[$idx], __ctx)?,)*))
}
GenericValue::Tuple(crate::bytecode::TupleBody::Flat(fc)) => {
let bytes = fc.resolve(__ctx.arena).map_err(|_| crate::marshall::stale_flat_decode())?;
Self::from_flat_bytes_ctx(bytes, __ctx.word_bytes, __ctx.float_bytes, __ctx)
}
other => Err(VmError::TypeError(format!(
"expected tuple, got {}",
other.type_name()
))),
}
}
#[allow(unused_assignments, unused_mut, unused_variables, non_snake_case)]
fn from_flat_bytes_ctx(bytes: &[u8], word_bytes: usize, float_bytes: usize, __ctx: &RefContext<'_>)
-> Result<Self, VmError>
{
let mut offset = 0usize;
Ok(($(
{
let size = <$name as KeleusmaType<W, FloatT>>::flat_byte_size(word_bytes, float_bytes)
.ok_or_else(|| VmError::TypeError(::alloc::string::String::from(
"flat tuple field is not flat-eligible",
)))?;
let val = <$name as KeleusmaType<W, FloatT>>::from_flat_bytes_ctx(
flat_subslice(bytes, offset, size)?, word_bytes, float_bytes, __ctx,
)?;
offset += size;
val
},
)*))
}
}
};
}
impl_tuple!(A: 0, B: 1);
impl_tuple!(A: 0, B: 1, C: 2);
impl_tuple!(A: 0, B: 1, C: 2, D: 3);
impl_tuple!(A: 0, B: 1, C: 2, D: 3, E: 4);
pub type BoxedNativeFn<W, F> = alloc::boxed::Box<
dyn for<'a> Fn(
&crate::vm::NativeCtx<'a>,
&[GenericValue<W, F>],
) -> Result<GenericValue<W, F>, VmError>,
>;
pub trait IntoNativeFn<W: Word, F: Float, Args, R> {
fn into_native_fn(self) -> BoxedNativeFn<W, F>;
}
pub trait IntoFallibleNativeFn<W: Word, F: Float, Args, R> {
fn into_native_fn(self) -> BoxedNativeFn<W, F>;
}
macro_rules! impl_into_native_fn {
($arity:expr; $($name:ident: $idx:tt),*) => {
impl<W: Word, FloatT: Float, Func, $($name,)* R>
IntoNativeFn<W, FloatT, ($($name,)*), R> for Func
where
Func: Fn($($name,)*) -> R + 'static,
$($name: KeleusmaType<W, FloatT>,)*
R: KeleusmaType<W, FloatT>,
{
#[allow(unused_variables, clippy::let_unit_value, non_snake_case)]
fn into_native_fn(self) -> BoxedNativeFn<W, FloatT> {
alloc::boxed::Box::new(
move |__ctx: &crate::vm::NativeCtx<'_>, args: &[GenericValue<W, FloatT>]|
-> Result<GenericValue<W, FloatT>, VmError> {
if args.len() != $arity {
return Err(VmError::NativeError(format!(
"native function expected {} argument(s), got {}",
$arity,
args.len()
)));
}
let __rc = __ctx.ref_context();
let _ = &__rc;
$(
let $name = <$name as KeleusmaType<W, FloatT>>::from_value_ctx(&args[$idx], &__rc)?;
)*
<R as KeleusmaType<W, FloatT>>::into_value_ctx(self($($name,)*), &__rc)
},
)
}
}
impl<W: Word, FloatT: Float, Func, $($name,)* R>
IntoFallibleNativeFn<W, FloatT, ($($name,)*), R> for Func
where
Func: Fn($($name,)*) -> Result<R, VmError> + 'static,
$($name: KeleusmaType<W, FloatT>,)*
R: KeleusmaType<W, FloatT>,
{
#[allow(unused_variables, clippy::let_unit_value, non_snake_case)]
fn into_native_fn(self) -> BoxedNativeFn<W, FloatT> {
alloc::boxed::Box::new(
move |__ctx: &crate::vm::NativeCtx<'_>, args: &[GenericValue<W, FloatT>]|
-> Result<GenericValue<W, FloatT>, VmError> {
if args.len() != $arity {
return Err(VmError::NativeError(format!(
"native function expected {} argument(s), got {}",
$arity,
args.len()
)));
}
let __rc = __ctx.ref_context();
let _ = &__rc;
$(
let $name = <$name as KeleusmaType<W, FloatT>>::from_value_ctx(&args[$idx], &__rc)?;
)*
self($($name,)*)
.and_then(|__r| {
<R as KeleusmaType<W, FloatT>>::into_value_ctx(__r, &__rc)
})
},
)
}
}
};
}
impl_into_native_fn!(0;);
impl_into_native_fn!(1; A: 0);
impl_into_native_fn!(2; A: 0, B: 1);
impl_into_native_fn!(3; A: 0, B: 1, C: 2);
impl_into_native_fn!(4; A: 0, B: 1, C: 2, D: 3);
#[cfg(all(test, feature = "floats"))]
mod tests {
use super::*;
use crate::bytecode::Value;
#[test]
fn primitive_roundtrip() {
assert_eq!(
<i64 as KeleusmaType<i64, f64>>::from_value(&Value::Int(42)).unwrap(),
42
);
assert_eq!(
<f64 as KeleusmaType<i64, f64>>::from_value(&Value::Float(2.5)).unwrap(),
2.5
);
assert!(<bool as KeleusmaType<i64, f64>>::from_value(&Value::Bool(true)).unwrap());
<() as KeleusmaType<i64, f64>>::from_value(&Value::Unit).unwrap();
assert_eq!(
<i64 as KeleusmaType<i64, f64>>::into_value(42i64),
Value::Int(42)
);
assert_eq!(
<f64 as KeleusmaType<i64, f64>>::into_value(2.5f64),
Value::Float(2.5)
);
assert_eq!(
<bool as KeleusmaType<i64, f64>>::into_value(true),
Value::Bool(true)
);
assert_eq!(<() as KeleusmaType<i64, f64>>::into_value(()), Value::Unit);
}
#[test]
fn i64_to_f64_widening() {
assert_eq!(
<f64 as KeleusmaType<i64, f64>>::from_value(&Value::Int(7)).unwrap(),
7.0
);
}
#[test]
fn type_mismatch_errors() {
let err = <i64 as KeleusmaType<i64, f64>>::from_value(&Value::Bool(true)).unwrap_err();
match err {
VmError::TypeError(msg) => assert!(msg.contains("expected Word")),
other => panic!("expected TypeError, got {:?}", other),
}
}
#[test]
fn option_roundtrip() {
let some = <Option<i64> as KeleusmaType<i64, f64>>::into_value(Some(42i64));
assert_ne!(some, Value::None);
assert_eq!(
<Option<i64> as KeleusmaType<i64, f64>>::from_value(&some).unwrap(),
Some(42)
);
let none = <Option<i64> as KeleusmaType<i64, f64>>::into_value(Option::<i64>::None);
assert_eq!(none, Value::None);
let recovered: Option<i64> =
<Option<i64> as KeleusmaType<i64, f64>>::from_value(&Value::Int(42)).unwrap();
assert_eq!(recovered, Some(42));
let recovered_none: Option<i64> =
<Option<i64> as KeleusmaType<i64, f64>>::from_value(&Value::None).unwrap();
assert_eq!(recovered_none, Option::None);
}
#[test]
fn nested_option_round_trips_some_none() {
type Oo = Option<Option<i64>>;
let some_none = <Oo as KeleusmaType<i64, f64>>::into_value(Some(None));
let none = <Oo as KeleusmaType<i64, f64>>::into_value(None);
assert_ne!(some_none, none, "Some(None) must differ from None");
assert_eq!(
<Oo as KeleusmaType<i64, f64>>::from_value(&some_none).unwrap(),
Some(None)
);
assert_eq!(
<Oo as KeleusmaType<i64, f64>>::from_value(&none).unwrap(),
None
);
let some_some = <Oo as KeleusmaType<i64, f64>>::into_value(Some(Some(7)));
assert_eq!(
<Oo as KeleusmaType<i64, f64>>::from_value(&some_some).unwrap(),
Some(Some(7))
);
}
#[test]
fn tuple_roundtrip() {
let t = (1i64, 2.0f64, true);
let v = <(i64, f64, bool) as KeleusmaType<i64, f64>>::into_value(t);
assert!(matches!(
&v,
Value::Tuple(crate::bytecode::TupleBody::Boxed(_))
));
let r: (i64, f64, bool) =
<(i64, f64, bool) as KeleusmaType<i64, f64>>::from_value(&v).unwrap();
assert_eq!(r, (1, 2.0, true));
}
#[test]
fn array_roundtrip() {
let a: [i64; 3] = [10, 20, 30];
let v = <[i64; 3] as KeleusmaType<i64, f64>>::into_value(a);
let r: [i64; 3] = <[i64; 3] as KeleusmaType<i64, f64>>::from_value(&v).unwrap();
assert_eq!(r, [10, 20, 30]);
}
#[test]
fn scalar_array_into_value_uses_boxed_body() {
use crate::bytecode::ArrayBody;
let v = <[i64; 3] as KeleusmaType<i64, f64>>::into_value([1, 2, 3]);
assert!(matches!(v, Value::Array(ArrayBody::Boxed(_))));
}
#[test]
fn byte_array_roundtrips_through_boxed_body() {
use crate::bytecode::ArrayBody;
let a: [u8; 4] = [1, 2, 250, 255];
let v = <[u8; 4] as KeleusmaType<i64, f64>>::into_value(a);
assert!(matches!(v, Value::Array(ArrayBody::Boxed(_))));
let r: [u8; 4] = <[u8; 4] as KeleusmaType<i64, f64>>::from_value(&v).unwrap();
assert_eq!(r, [1, 2, 250, 255]);
}
#[test]
fn struct_value_uses_boxed_body_without_arena() {
use crate::bytecode::StructBody;
let scalar = Value::struct_value(
::alloc::string::String::from("P"),
::alloc::vec![
(::alloc::string::String::from("a"), Value::Int(1)),
(::alloc::string::String::from("b"), Value::Int(2)),
],
);
assert!(matches!(scalar, Value::Struct(StructBody::Boxed { .. })));
let reference = Value::struct_value(
::alloc::string::String::from("Q"),
::alloc::vec![(
::alloc::string::String::from("s"),
Value::StaticStr(::alloc::string::String::from("x")),
)],
);
assert!(matches!(reference, Value::Struct(StructBody::Boxed { .. })));
}
#[test]
fn reference_element_array_uses_boxed_body() {
use crate::bytecode::ArrayBody;
let v = Value::array(::alloc::vec![
Value::StaticStr(::alloc::string::String::from("a")),
Value::StaticStr(::alloc::string::String::from("b")),
]);
assert!(matches!(v, Value::Array(ArrayBody::Boxed(_))));
}
#[test]
fn array_length_mismatch() {
let v = Value::array(::alloc::vec![Value::Int(1), Value::Int(2)]);
let err = <[i64; 3] as KeleusmaType<i64, f64>>::from_value(&v).unwrap_err();
match err {
VmError::TypeError(msg) => assert!(msg.contains("length")),
other => panic!("expected TypeError, got {:?}", other),
}
}
fn ctx(arena: &keleusma_arena::Arena) -> crate::vm::NativeCtx<'_> {
crate::vm::NativeCtx {
arena,
opaques: &[],
word_bytes: 8,
float_bytes: 8,
}
}
#[test]
fn into_native_fn_arity_zero() {
let f = || 42i64;
let native = <_ as IntoNativeFn<i64, f64, (), i64>>::into_native_fn(f);
let arena = keleusma_arena::Arena::with_capacity(64);
let r = native(&ctx(&arena), &[]).unwrap();
assert_eq!(r, Value::Int(42));
}
#[test]
fn into_native_fn_arity_one() {
let f = |x: i64| x * 2;
let native = <_ as IntoNativeFn<i64, f64, (i64,), i64>>::into_native_fn(f);
let arena = keleusma_arena::Arena::with_capacity(64);
let r = native(&ctx(&arena), &[Value::Int(7)]).unwrap();
assert_eq!(r, Value::Int(14));
}
#[test]
fn into_native_fn_arity_two() {
let f = |a: i64, b: i64| a + b;
let native = <_ as IntoNativeFn<i64, f64, (i64, i64), i64>>::into_native_fn(f);
let arena = keleusma_arena::Arena::with_capacity(64);
let r = native(&ctx(&arena), &[Value::Int(3), Value::Int(4)]).unwrap();
assert_eq!(r, Value::Int(7));
}
#[test]
fn into_native_fn_arity_mismatch_errors() {
let f = |x: i64| x;
let native = <_ as IntoNativeFn<i64, f64, (i64,), i64>>::into_native_fn(f);
let arena = keleusma_arena::Arena::with_capacity(64);
let err = native(&ctx(&arena), &[Value::Int(1), Value::Int(2)]).unwrap_err();
match err {
VmError::NativeError(msg) => assert!(msg.contains("expected 1 argument")),
other => panic!("expected NativeError, got {:?}", other),
}
}
#[test]
fn into_fallible_native_fn_propagates_error() {
let f = |x: i64| -> Result<i64, VmError> {
if x == 0 {
Err(VmError::DivisionByZero)
} else {
Ok(100 / x)
}
};
let native = <_ as IntoFallibleNativeFn<i64, f64, (i64,), i64>>::into_native_fn(f);
let arena = keleusma_arena::Arena::with_capacity(64);
let r = native(&ctx(&arena), &[Value::Int(5)]).unwrap();
assert_eq!(r, Value::Int(20));
let err = native(&ctx(&arena), &[Value::Int(0)]).unwrap_err();
match err {
VmError::DivisionByZero => {}
other => panic!("expected DivisionByZero, got {:?}", other),
}
}
#[test]
fn type_error_message_contains_typename() {
let err = <i64 as KeleusmaType<i64, f64>>::from_value(&Value::Float(1.5)).unwrap_err();
match err {
VmError::TypeError(msg) => {
assert!(msg.contains("Float"), "got message: {}", msg)
}
other => panic!("expected TypeError, got {:?}", other),
}
}
}