#![allow(missing_docs)]
extern crate alloc;
use alloc::string::String;
use alloc::vec::Vec;
use rkyv::{Archive, Deserialize, Serialize};
use crate::kstring::KString;
#[derive(Debug, Clone, Archive, Serialize, Deserialize)]
#[rkyv(
serialize_bounds(__S: rkyv::ser::Writer + rkyv::ser::Allocator, __S::Error: rkyv::rancor::Source),
deserialize_bounds(__D::Error: rkyv::rancor::Source),
bytecheck(bounds(__C: rkyv::validation::ArchiveContext, <__C as rkyv::rancor::Fallible>::Error: rkyv::rancor::Source)),
attr(allow(missing_docs))
)]
pub enum ConstValue {
Unit,
Bool(bool),
Int(i64),
Byte(u8),
Fixed(i64),
#[cfg(feature = "floats")]
Float(f64),
StaticStr(String),
Tuple(#[rkyv(omit_bounds)] Vec<ConstValue>),
Array(#[rkyv(omit_bounds)] Vec<ConstValue>),
Struct {
type_name: String,
#[rkyv(omit_bounds)]
fields: Vec<(String, ConstValue)>,
},
Enum {
type_name: String,
variant: String,
discriminant: Option<i64>,
#[rkyv(omit_bounds)]
fields: Vec<ConstValue>,
},
None,
}
pub type Value = GenericValue<i64, f64>;
#[cfg(target_pointer_width = "64")]
const _: () = assert!(
core::mem::size_of::<Value>() == 32,
"Value slot must be 32 bytes (B28 item 2 step 6B); a layout change regressed it"
);
#[derive(Debug, Clone, PartialEq)]
pub enum TupleBody<W: crate::word::Word, F: crate::float::Float> {
Flat(crate::flat_value::FlatComposite),
Boxed(alloc::boxed::Box<alloc::vec::Vec<GenericValue<W, F>>>),
}
impl<W: crate::word::Word, F: crate::float::Float> TupleBody<W, F> {
pub fn boxed(elements: alloc::vec::Vec<GenericValue<W, F>>) -> Self {
Self::Boxed(alloc::boxed::Box::new(elements))
}
pub fn elements(&self) -> &[GenericValue<W, F>] {
match self {
Self::Boxed(v) => v,
Self::Flat(_) => {
unreachable!("flat tuple body has no element values; read via GetTupleField")
}
}
}
pub fn into_elements(self) -> alloc::vec::Vec<GenericValue<W, F>> {
match self {
Self::Boxed(v) => *v,
Self::Flat(_) => {
unreachable!("flat tuple body has no element values; read via GetTupleField")
}
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ArrayBody<W: crate::word::Word, F: crate::float::Float> {
Flat(crate::flat_value::FlatComposite),
Boxed(alloc::boxed::Box<alloc::vec::Vec<GenericValue<W, F>>>),
}
impl<W: crate::word::Word, F: crate::float::Float> ArrayBody<W, F> {
pub fn boxed(elements: alloc::vec::Vec<GenericValue<W, F>>) -> Self {
Self::Boxed(alloc::boxed::Box::new(elements))
}
pub fn elements(&self) -> &[GenericValue<W, F>] {
match self {
Self::Boxed(v) => v,
Self::Flat(_) => {
unreachable!(
"flat array body has no element kind; read via GetIndex or marshalling"
)
}
}
}
pub fn into_elements(self) -> alloc::vec::Vec<GenericValue<W, F>> {
match self {
Self::Boxed(v) => *v,
Self::Flat(_) => {
unreachable!(
"flat array body has no element kind; read via GetIndex or marshalling"
)
}
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum StructBody<W: crate::word::Word, F: crate::float::Float> {
Flat(crate::flat_value::FlatComposite),
Boxed(alloc::boxed::Box<BoxedStruct<W, F>>),
}
#[derive(Debug, Clone, PartialEq)]
pub struct BoxedStruct<W: crate::word::Word, F: crate::float::Float> {
pub type_name: alloc::string::String,
pub fields: alloc::vec::Vec<(alloc::string::String, GenericValue<W, F>)>,
}
impl<W: crate::word::Word, F: crate::float::Float> StructBody<W, F> {
pub fn boxed(
type_name: alloc::string::String,
fields: alloc::vec::Vec<(alloc::string::String, GenericValue<W, F>)>,
) -> Self {
Self::Boxed(alloc::boxed::Box::new(BoxedStruct { type_name, fields }))
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum EnumBody<W: crate::word::Word, F: crate::float::Float> {
Flat(crate::flat_value::FlatComposite),
Boxed(alloc::boxed::Box<BoxedEnum<W, F>>),
}
#[derive(Debug, Clone)]
pub struct BoxedEnum<W: crate::word::Word, F: crate::float::Float> {
pub type_name: alloc::string::String,
pub variant: alloc::string::String,
pub disc: i64,
pub min_payload: usize,
pub fields: alloc::vec::Vec<GenericValue<W, F>>,
}
impl<W: crate::word::Word, F: crate::float::Float> PartialEq for BoxedEnum<W, F> {
fn eq(&self, other: &Self) -> bool {
self.type_name == other.type_name
&& self.variant == other.variant
&& self.fields == other.fields
}
}
impl<W: crate::word::Word, F: crate::float::Float> EnumBody<W, F> {
pub fn boxed(
type_name: alloc::string::String,
variant: alloc::string::String,
fields: alloc::vec::Vec<GenericValue<W, F>>,
) -> Self {
Self::boxed_with_layout(type_name, variant, 0, 0, fields)
}
pub fn boxed_with_disc(
type_name: alloc::string::String,
variant: alloc::string::String,
disc: i64,
fields: alloc::vec::Vec<GenericValue<W, F>>,
) -> Self {
Self::boxed_with_layout(type_name, variant, disc, 0, fields)
}
pub fn boxed_with_layout(
type_name: alloc::string::String,
variant: alloc::string::String,
disc: i64,
min_payload: usize,
fields: alloc::vec::Vec<GenericValue<W, F>>,
) -> Self {
Self::Boxed(alloc::boxed::Box::new(BoxedEnum {
type_name,
variant,
disc,
min_payload,
fields,
}))
}
}
#[derive(Debug, Clone)]
pub enum GenericValue<W: crate::word::Word, F: crate::float::Float> {
Unit,
Bool(bool),
Int(W),
Byte(u8),
Fixed(W),
#[cfg(feature = "floats")]
Float(F),
StaticStr(String),
KStr(KString),
Tuple(TupleBody<W, F>),
Array(ArrayBody<W, F>),
Struct(StructBody<W, F>),
Enum(EnumBody<W, F>),
None,
Opaque(alloc::sync::Arc<dyn crate::opaque::HostOpaque>),
#[doc(hidden)]
OpaqueRef(u32),
#[cfg(not(feature = "floats"))]
#[doc(hidden)]
_PhantomFloat(core::marker::PhantomData<F>),
}
impl<W: crate::word::Word, F: crate::float::Float> PartialEq for GenericValue<W, F> {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Unit, Self::Unit) | (Self::None, Self::None) => true,
(Self::Bool(a), Self::Bool(b)) => a == b,
(Self::Int(a), Self::Int(b)) => a == b,
(Self::Byte(a), Self::Byte(b)) => a == b,
(Self::Fixed(a), Self::Fixed(b)) => a == b,
#[cfg(feature = "floats")]
(Self::Float(a), Self::Float(b)) => a == b,
(Self::StaticStr(a), Self::StaticStr(b)) => a == b,
(Self::KStr(a), Self::KStr(b)) => a.epoch() == b.epoch(),
(Self::Tuple(a), Self::Tuple(b)) => a == b,
(Self::Array(a), Self::Array(b)) => a == b,
(Self::Struct(a), Self::Struct(b)) => a == b,
(Self::Enum(EnumBody::Flat(a)), Self::Enum(EnumBody::Flat(b))) => {
match (a.inline_bytes(), b.inline_bytes()) {
(Some(x), Some(y)) => flat_enum_bytes_eq(x, y),
_ => false,
}
}
(Self::Enum(a), Self::Enum(b)) => a == b,
(Self::Opaque(a), Self::Opaque(b)) => alloc::sync::Arc::ptr_eq(a, b),
(Self::OpaqueRef(a), Self::OpaqueRef(b)) => a == b,
_ => false,
}
}
}
pub(crate) fn flat_tuple_scalar_kind<W: crate::word::Word, F: crate::float::Float>(
v: &GenericValue<W, F>,
) -> Option<crate::value_layout::ScalarKind> {
use crate::value_layout::ScalarKind as K;
match v {
GenericValue::Unit => Some(K::Unit),
GenericValue::Bool(_) => Some(K::Bool),
GenericValue::Byte(_) => Some(K::Byte),
GenericValue::Int(_) => Some(K::Int),
GenericValue::Fixed(_) => Some(K::Fixed),
#[cfg(feature = "floats")]
GenericValue::Float(_) => Some(K::Float),
GenericValue::KStr(_) => Some(K::Text),
_ => None,
}
}
pub(crate) fn flat_field_size<W: crate::word::Word, F: crate::float::Float>(
v: &GenericValue<W, F>,
word_bytes: usize,
float_bytes: usize,
) -> Option<usize> {
if let Some(kind) = flat_tuple_scalar_kind(v) {
return Some(kind.size_in_bytes(word_bytes, float_bytes));
}
flat_composite_ref(v).map(|fc| fc.byte_len())
}
pub(crate) fn flat_tuple_element_with_refs<W: crate::word::Word, F: crate::float::Float>(
v: &GenericValue<W, F>,
word_bytes: usize,
float_bytes: usize,
) -> bool {
match v {
GenericValue::Opaque(_) | GenericValue::OpaqueRef(_) => true,
GenericValue::StaticStr(_) | GenericValue::KStr(_) => {
word_bytes >= core::mem::size_of::<usize>()
}
_ => flat_field_size(v, word_bytes, float_bytes).is_some(),
}
}
fn flat_enum_bytes_eq(a: &[u8], b: &[u8]) -> bool {
let m = core::cmp::min(a.len(), b.len());
a[..m] == b[..m] && a[m..].iter().all(|&x| x == 0) && b[m..].iter().all(|&x| x == 0)
}
pub(crate) fn flat_composite_ref<W: crate::word::Word, F: crate::float::Float>(
v: &GenericValue<W, F>,
) -> Option<&crate::flat_value::FlatComposite> {
match v {
GenericValue::Tuple(TupleBody::Flat(fc))
| GenericValue::Array(ArrayBody::Flat(fc))
| GenericValue::Struct(StructBody::Flat(fc))
| GenericValue::Enum(EnumBody::Flat(fc)) => Some(fc),
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScalarError {
OutOfBounds,
ReferenceKind,
UnsupportedWidth,
}
impl<W: crate::word::Word, F: crate::float::Float> GenericValue<W, F> {
pub fn tuple(elements: alloc::vec::Vec<Self>) -> Self {
let word_bytes = (1usize << <W as crate::word::Word>::BITS_LOG2) / 8;
let float_bytes = (1usize << <F as crate::float::Float>::BITS_LOG2) / 8;
Self::tuple_with_widths(elements, word_bytes, float_bytes)
}
pub fn tuple_with_widths(
elements: alloc::vec::Vec<Self>,
word_bytes: usize,
float_bytes: usize,
) -> Self {
let _ = (word_bytes, float_bytes);
Self::Tuple(TupleBody::boxed(elements))
}
pub fn array(elements: alloc::vec::Vec<Self>) -> Self {
let word_bytes = (1usize << <W as crate::word::Word>::BITS_LOG2) / 8;
let float_bytes = (1usize << <F as crate::float::Float>::BITS_LOG2) / 8;
Self::array_with_widths(elements, word_bytes, float_bytes)
}
pub fn array_with_widths(
elements: alloc::vec::Vec<Self>,
word_bytes: usize,
float_bytes: usize,
) -> Self {
let _ = (word_bytes, float_bytes);
Self::Array(ArrayBody::boxed(elements))
}
pub fn struct_value(
type_name: alloc::string::String,
fields: alloc::vec::Vec<(alloc::string::String, Self)>,
) -> Self {
let word_bytes = (1usize << <W as crate::word::Word>::BITS_LOG2) / 8;
let float_bytes = (1usize << <F as crate::float::Float>::BITS_LOG2) / 8;
Self::struct_with_widths(type_name, fields, word_bytes, float_bytes)
}
pub fn struct_with_widths(
type_name: alloc::string::String,
fields: alloc::vec::Vec<(alloc::string::String, Self)>,
word_bytes: usize,
float_bytes: usize,
) -> Self {
let _ = (word_bytes, float_bytes);
Self::Struct(StructBody::boxed(type_name, fields))
}
pub fn enum_value(
type_name: alloc::string::String,
variant: alloc::string::String,
disc: i64,
fields: alloc::vec::Vec<Self>,
) -> Self {
let word_bytes = (1usize << <W as crate::word::Word>::BITS_LOG2) / 8;
let float_bytes = (1usize << <F as crate::float::Float>::BITS_LOG2) / 8;
Self::enum_with_widths(type_name, variant, disc, fields, 0, word_bytes, float_bytes)
}
pub fn enum_with_widths(
type_name: alloc::string::String,
variant: alloc::string::String,
disc: i64,
fields: alloc::vec::Vec<Self>,
min_payload: usize,
word_bytes: usize,
float_bytes: usize,
) -> Self {
let _ = (word_bytes, float_bytes);
Self::Enum(EnumBody::boxed_with_layout(
type_name,
variant,
disc,
min_payload,
fields,
))
}
pub fn tuple_in_arena(
elements: alloc::vec::Vec<Self>,
word_bytes: usize,
float_bytes: usize,
arena: &keleusma_arena::Arena,
) -> Result<Self, allocator_api2::alloc::AllocError> {
match Self::pack_flat_in_arena(&elements, 0, word_bytes, float_bytes, arena)? {
Some(body) => Ok(Self::Tuple(TupleBody::Flat(body))),
None => Ok(Self::Tuple(TupleBody::boxed(elements))),
}
}
pub fn array_in_arena(
elements: alloc::vec::Vec<Self>,
word_bytes: usize,
float_bytes: usize,
arena: &keleusma_arena::Arena,
) -> Result<Self, allocator_api2::alloc::AllocError> {
match Self::pack_flat_in_arena(&elements, 0, word_bytes, float_bytes, arena)? {
Some(body) => Ok(Self::Array(ArrayBody::Flat(body))),
None => Ok(Self::Array(ArrayBody::boxed(elements))),
}
}
pub fn struct_in_arena(
type_name: alloc::string::String,
fields: alloc::vec::Vec<(alloc::string::String, Self)>,
word_bytes: usize,
float_bytes: usize,
arena: &keleusma_arena::Arena,
) -> Result<Self, allocator_api2::alloc::AllocError> {
let (names, values): (
alloc::vec::Vec<alloc::string::String>,
alloc::vec::Vec<Self>,
) = fields.into_iter().unzip();
match Self::pack_flat_in_arena(&values, 0, word_bytes, float_bytes, arena)? {
Some(body) => Ok(Self::Struct(StructBody::Flat(body))),
None => Ok(Self::Struct(StructBody::boxed(
type_name,
names.into_iter().zip(values).collect(),
))),
}
}
#[allow(clippy::too_many_arguments)]
pub fn enum_in_arena(
type_name: alloc::string::String,
variant: alloc::string::String,
disc: i64,
min_payload: usize,
fields: alloc::vec::Vec<Self>,
word_bytes: usize,
float_bytes: usize,
arena: &keleusma_arena::Arena,
) -> Result<Self, allocator_api2::alloc::AllocError> {
let disc_value = Self::Int(<W as crate::word::Word>::from_i64_wrap(disc));
let mut values: alloc::vec::Vec<Self> = alloc::vec::Vec::with_capacity(fields.len() + 1);
values.push(disc_value);
values.extend(fields);
let min_bytes = word_bytes + min_payload;
match Self::pack_flat_in_arena(&values, min_bytes, word_bytes, float_bytes, arena)? {
Some(body) => Ok(Self::Enum(EnumBody::Flat(body))),
None => {
let mut it = values.into_iter();
it.next();
let fields: alloc::vec::Vec<Self> = it.collect();
Ok(Self::Enum(EnumBody::boxed_with_layout(
type_name,
variant,
disc,
min_payload,
fields,
)))
}
}
}
pub fn into_arena_canonical(
self,
word_bytes: usize,
float_bytes: usize,
arena: &keleusma_arena::Arena,
) -> Result<Self, allocator_api2::alloc::AllocError> {
match self {
Self::Tuple(TupleBody::Boxed(elems)) => {
let elems = (*elems)
.into_iter()
.map(|v| v.into_arena_canonical_field(word_bytes, float_bytes, arena))
.collect::<Result<alloc::vec::Vec<_>, _>>()?;
Self::tuple_in_arena(elems, word_bytes, float_bytes, arena)
}
Self::Array(ArrayBody::Boxed(elems)) => {
let elems = (*elems)
.into_iter()
.map(|v| v.into_arena_canonical_field(word_bytes, float_bytes, arena))
.collect::<Result<alloc::vec::Vec<_>, _>>()?;
Self::array_in_arena(elems, word_bytes, float_bytes, arena)
}
Self::Struct(StructBody::Boxed(b)) => {
let BoxedStruct { type_name, fields } = *b;
let fields = fields
.into_iter()
.map(|(k, v)| {
Ok::<_, allocator_api2::alloc::AllocError>((
k,
v.into_arena_canonical_field(word_bytes, float_bytes, arena)?,
))
})
.collect::<Result<alloc::vec::Vec<_>, _>>()?;
Self::struct_in_arena(type_name, fields, word_bytes, float_bytes, arena)
}
Self::Enum(EnumBody::Boxed(b)) => {
let BoxedEnum {
type_name,
variant,
disc,
min_payload,
fields,
} = *b;
if type_name == "Option" {
let fields = fields
.into_iter()
.map(|v| v.into_arena_canonical_field(word_bytes, float_bytes, arena))
.collect::<Result<alloc::vec::Vec<_>, _>>()?;
return Self::enum_in_arena(
type_name,
variant,
1,
min_payload,
fields,
word_bytes,
float_bytes,
arena,
);
}
let fields = fields
.into_iter()
.map(|v| v.into_arena_canonical_field(word_bytes, float_bytes, arena))
.collect::<Result<alloc::vec::Vec<_>, _>>()?;
Self::enum_in_arena(
type_name,
variant,
disc,
min_payload,
fields,
word_bytes,
float_bytes,
arena,
)
}
other => Ok(other),
}
}
fn into_arena_canonical_field(
self,
word_bytes: usize,
float_bytes: usize,
arena: &keleusma_arena::Arena,
) -> Result<Self, allocator_api2::alloc::AllocError> {
match self.into_arena_canonical(word_bytes, float_bytes, arena)? {
Self::StaticStr(s) if word_bytes >= core::mem::size_of::<usize>() => {
let ks = crate::kstring::KString::alloc(arena, &s)?;
Ok(Self::KStr(ks))
}
other => Ok(other),
}
}
#[cfg_attr(not(feature = "floats"), allow(unused_variables))]
pub fn write_scalar_le(
&self,
dst: &mut [u8],
offset: usize,
word_bytes: usize,
float_bytes: usize,
) -> Result<(), ScalarError> {
fn slice_mut(dst: &mut [u8], offset: usize, n: usize) -> Result<&mut [u8], ScalarError> {
offset
.checked_add(n)
.and_then(|end| dst.get_mut(offset..end))
.ok_or(ScalarError::OutOfBounds)
}
match self {
Self::Unit | Self::None => {}
Self::Bool(b) => *dst.get_mut(offset).ok_or(ScalarError::OutOfBounds)? = u8::from(*b),
Self::Byte(b) => *dst.get_mut(offset).ok_or(ScalarError::OutOfBounds)? = *b,
Self::Int(w) | Self::Fixed(w) => {
if word_bytes > 8 {
return Err(ScalarError::UnsupportedWidth);
}
let le = w.to_i64().to_le_bytes();
slice_mut(dst, offset, word_bytes)?.copy_from_slice(&le[..word_bytes]);
}
#[cfg(feature = "floats")]
Self::Float(f) => {
let v = f.to_f64();
match float_bytes {
8 => slice_mut(dst, offset, 8)?.copy_from_slice(&v.to_le_bytes()),
4 => slice_mut(dst, offset, 4)?.copy_from_slice(&(v as f32).to_le_bytes()),
_ => return Err(ScalarError::UnsupportedWidth),
}
}
Self::KStr(ks) => {
let (ptr, len) = ks.raw_parts();
if word_bytes < core::mem::size_of::<usize>() {
return Err(ScalarError::UnsupportedWidth);
}
let pe = (ptr as u64).to_le_bytes();
slice_mut(dst, offset, word_bytes)?.copy_from_slice(&pe[..word_bytes]);
let le = (len as u64).to_le_bytes();
slice_mut(dst, offset + word_bytes, word_bytes)?.copy_from_slice(&le[..word_bytes]);
}
_ => return Err(ScalarError::UnsupportedWidth),
}
Ok(())
}
pub fn new_composite_boxed(
kind: crate::value_layout::CompositeKind,
type_name: alloc::string::String,
names: alloc::vec::Vec<alloc::string::String>,
values: alloc::vec::Vec<Self>,
) -> Self {
use crate::value_layout::CompositeKind as C;
match kind {
C::Tuple => Self::Tuple(TupleBody::boxed(values)),
C::Array => Self::Array(ArrayBody::boxed(values)),
C::Struct => Self::Struct(StructBody::boxed(
type_name,
names.into_iter().zip(values).collect(),
)),
C::Enum => {
let variant = names.into_iter().next().unwrap_or_default();
Self::Enum(EnumBody::boxed(type_name, variant, values))
}
}
}
pub fn flat_nested_field(
parent: &crate::flat_value::FlatComposite,
offset: usize,
size: usize,
variant: crate::value_layout::CompositeKind,
arena: &keleusma_arena::Arena,
) -> Result<Self, keleusma_arena::Stale> {
use crate::value_layout::CompositeKind as C;
let fc = parent.nested_view(offset, size, arena)?;
Ok(match variant {
C::Tuple => Self::Tuple(TupleBody::Flat(fc)),
C::Array => Self::Array(ArrayBody::Flat(fc)),
C::Struct => Self::Struct(StructBody::Flat(fc)),
C::Enum => Self::Enum(EnumBody::Flat(fc)),
})
}
pub fn into_arena_body(
self,
arena: &keleusma_arena::Arena,
) -> Result<Self, allocator_api2::alloc::AllocError> {
Ok(match self {
Self::Tuple(TupleBody::Flat(fc)) => Self::Tuple(TupleBody::Flat(fc.in_arena(arena)?)),
Self::Array(ArrayBody::Flat(fc)) => Self::Array(ArrayBody::Flat(fc.in_arena(arena)?)),
Self::Struct(StructBody::Flat(fc)) => {
Self::Struct(StructBody::Flat(fc.in_arena(arena)?))
}
Self::Enum(EnumBody::Flat(fc)) => Self::Enum(EnumBody::Flat(fc.in_arena(arena)?)),
other => other,
})
}
pub fn flat_ref_epoch(&self) -> Option<u64> {
match self {
Self::Tuple(TupleBody::Flat(fc)) => Some(fc.ref_epoch()),
Self::Array(ArrayBody::Flat(fc)) => Some(fc.ref_epoch()),
Self::Struct(StructBody::Flat(fc)) => Some(fc.ref_epoch()),
Self::Enum(EnumBody::Flat(fc)) => Some(fc.ref_epoch()),
_ => None,
}
}
pub fn pack_flat_in_arena(
values: &[Self],
min_bytes: usize,
word_bytes: usize,
float_bytes: usize,
arena: &keleusma_arena::Arena,
) -> Result<Option<crate::flat_value::FlatComposite>, allocator_api2::alloc::AllocError> {
let mut size = 0usize;
for v in values {
match flat_field_size(v, word_bytes, float_bytes) {
Some(n) => size += n,
None => return Ok(None),
}
}
if size < min_bytes {
size = min_bytes;
}
if size > u16::MAX as usize {
return Ok(None);
}
crate::flat_value::FlatComposite::build_in_arena(arena, size, |dst| {
let mut off = 0usize;
for v in values {
if let Some(kind) = flat_tuple_scalar_kind(v) {
let field = kind.size_in_bytes(word_bytes, float_bytes);
v.write_scalar_le(dst, off, word_bytes, float_bytes)
.map_err(|_| ())?;
off += field;
} else if let Some(fc) = flat_composite_ref(v) {
let bytes = fc.resolve(arena).map_err(|_| ())?;
dst[off..off + bytes.len()].copy_from_slice(bytes);
off += bytes.len();
} else {
return Err(());
}
}
for b in dst[off..size].iter_mut() {
*b = 0;
}
Ok(())
})
}
#[cfg_attr(not(feature = "floats"), allow(unused_variables))]
pub fn read_scalar_le(
src: &[u8],
offset: usize,
kind: crate::value_layout::ScalarKind,
word_bytes: usize,
float_bytes: usize,
) -> Result<Self, ScalarError> {
use crate::value_layout::ScalarKind;
let slice = |n: usize| -> Result<&[u8], ScalarError> {
offset
.checked_add(n)
.and_then(|end| src.get(offset..end))
.ok_or(ScalarError::OutOfBounds)
};
match kind {
ScalarKind::Unit => Ok(Self::Unit),
ScalarKind::Bool => Ok(Self::Bool(
*src.get(offset).ok_or(ScalarError::OutOfBounds)? != 0,
)),
ScalarKind::Byte => Ok(Self::Byte(
*src.get(offset).ok_or(ScalarError::OutOfBounds)?,
)),
ScalarKind::Int | ScalarKind::Fixed => {
if word_bytes > 8 {
return Err(ScalarError::UnsupportedWidth);
}
let mut buf = [0u8; 8];
buf[..word_bytes].copy_from_slice(slice(word_bytes)?);
let mut n = i64::from_le_bytes(buf);
if word_bytes < 8 {
let bits = word_bytes * 8;
let sign = 1i64 << (bits - 1);
if n & sign != 0 {
n |= !((1i64 << bits) - 1);
}
}
let w = W::from_i64_wrap(n);
Ok(if matches!(kind, ScalarKind::Fixed) {
Self::Fixed(w)
} else {
Self::Int(w)
})
}
#[cfg(feature = "floats")]
ScalarKind::Float => {
let v = match float_bytes {
8 => {
let mut buf = [0u8; 8];
buf.copy_from_slice(slice(8)?);
f64::from_le_bytes(buf)
}
4 => {
let mut buf = [0u8; 4];
buf.copy_from_slice(slice(4)?);
f32::from_le_bytes(buf) as f64
}
_ => return Err(ScalarError::UnsupportedWidth),
};
Ok(Self::Float(F::from_f64(v)))
}
ScalarKind::Text | ScalarKind::Opaque => Err(ScalarError::ReferenceKind),
}
}
pub fn materialise_kstrings(&self, arena: &keleusma_arena::Arena) -> Self {
match self {
Self::KStr(handle) => match handle.get(arena) {
Ok(s) => Self::StaticStr(alloc::string::String::from(s)),
Err(_) => Self::StaticStr(alloc::string::String::new()),
},
Self::Tuple(TupleBody::Flat(_)) => self.clone(),
Self::Tuple(TupleBody::Boxed(items)) => Self::tuple(
items
.iter()
.map(|v| v.materialise_kstrings(arena))
.collect(),
),
Self::Array(ArrayBody::Flat(_)) => self.clone(),
Self::Array(ArrayBody::Boxed(items)) => Self::array(
items
.iter()
.map(|v| v.materialise_kstrings(arena))
.collect(),
),
Self::Struct(StructBody::Flat(_)) => self.clone(),
Self::Struct(StructBody::Boxed(b)) => Self::struct_value(
b.type_name.clone(),
b.fields
.iter()
.map(|(k, v)| (k.clone(), v.materialise_kstrings(arena)))
.collect(),
),
Self::Enum(EnumBody::Flat(_)) => self.clone(),
Self::Enum(EnumBody::Boxed(b)) => Self::Enum(EnumBody::boxed_with_layout(
b.type_name.clone(),
b.variant.clone(),
b.disc,
b.min_payload,
b.fields
.iter()
.map(|v| v.materialise_kstrings(arena))
.collect(),
)),
other => other.clone(),
}
}
pub fn type_name(&self) -> &'static str {
match self {
Self::Unit => "Unit",
Self::Bool(_) => "Bool",
Self::Int(_) => "Int",
Self::Byte(_) => "Byte",
Self::Fixed(_) => "Fixed",
#[cfg(feature = "floats")]
Self::Float(_) => "Float",
Self::StaticStr(_) => "StaticStr",
Self::KStr(_) => "KStr",
Self::Tuple(_) => "Tuple",
Self::Array(_) => "Array",
Self::Struct { .. } => "Struct",
Self::Enum(_) => "Enum",
Self::None => "None",
Self::Opaque(_) => "Opaque",
Self::OpaqueRef(_) => "Opaque",
#[cfg(not(feature = "floats"))]
Self::_PhantomFloat(_) => unreachable!("_PhantomFloat is never constructed"),
}
}
pub fn opaque_type_name(&self) -> Option<&'static str> {
match self {
Self::Opaque(o) => Some(o.type_name()),
_ => None,
}
}
pub fn as_str(&self) -> Option<&str> {
match self {
Self::StaticStr(s) => Some(s.as_str()),
_ => Option::None,
}
}
pub fn as_str_with_arena<'a>(
&'a self,
arena: &'a keleusma_arena::Arena,
) -> Result<Option<&'a str>, keleusma_arena::Stale> {
match self {
Self::StaticStr(s) => Ok(Some(s.as_str())),
Self::KStr(h) => h.get(arena).map(Some),
_ => Ok(Option::None),
}
}
pub fn contains_dynstr(&self) -> bool {
match self {
Self::KStr(_) => true,
Self::Tuple(TupleBody::Flat(_)) => false,
Self::Tuple(TupleBody::Boxed(items)) => items.iter().any(Self::contains_dynstr),
Self::Array(ArrayBody::Flat(_)) => false,
Self::Array(ArrayBody::Boxed(items)) => items.iter().any(Self::contains_dynstr),
Self::Struct(StructBody::Flat(_)) => false,
Self::Struct(StructBody::Boxed(b)) => b.fields.iter().any(|(_, v)| v.contains_dynstr()),
Self::Enum(EnumBody::Flat(_)) => false,
Self::Enum(EnumBody::Boxed(b)) => b.fields.iter().any(Self::contains_dynstr),
_ => false,
}
}
pub fn from_const_archived(
c: &ArchivedConstValue,
word_bytes: usize,
float_bytes: usize,
) -> Self {
match c {
ArchivedConstValue::Unit => Self::Unit,
ArchivedConstValue::Bool(b) => Self::Bool(*b),
ArchivedConstValue::Int(i) => Self::Int(W::from_i64_wrap(i.to_native())),
ArchivedConstValue::Byte(b) => Self::Byte(*b),
ArchivedConstValue::Fixed(i) => Self::Fixed(W::from_i64_wrap(i.to_native())),
#[cfg(feature = "floats")]
ArchivedConstValue::Float(f) => Self::Float(F::from_f64(f.to_native())),
ArchivedConstValue::StaticStr(s) => {
use alloc::string::ToString;
Self::StaticStr(s.as_str().to_string())
}
ArchivedConstValue::Tuple(items) => Self::tuple_with_widths(
items
.iter()
.map(|c| Self::from_const_archived(c, word_bytes, float_bytes))
.collect(),
word_bytes,
float_bytes,
),
ArchivedConstValue::Array(items) => Self::array_with_widths(
items
.iter()
.map(|c| Self::from_const_archived(c, word_bytes, float_bytes))
.collect(),
word_bytes,
float_bytes,
),
ArchivedConstValue::Struct { type_name, fields } => {
use alloc::string::ToString;
Self::struct_with_widths(
type_name.as_str().to_string(),
fields
.iter()
.map(|kv| {
(
kv.0.as_str().to_string(),
Self::from_const_archived(&kv.1, word_bytes, float_bytes),
)
})
.collect(),
word_bytes,
float_bytes,
)
}
ArchivedConstValue::Enum {
type_name,
variant,
discriminant,
fields,
} => {
use alloc::string::ToString;
let materialised: alloc::vec::Vec<Self> = fields
.iter()
.map(|c| Self::from_const_archived(c, word_bytes, float_bytes))
.collect();
match discriminant.as_ref().map(|d| d.to_native()) {
Some(disc) => Self::enum_with_widths(
type_name.as_str().to_string(),
variant.as_str().to_string(),
disc,
materialised,
0,
word_bytes,
float_bytes,
),
None => Self::Enum(EnumBody::boxed(
type_name.as_str().to_string(),
variant.as_str().to_string(),
materialised,
)),
}
}
ArchivedConstValue::None => Self::None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Archive, Serialize, Deserialize)]
pub enum BlockType {
Func,
Reentrant,
Stream,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TrapKind {
RefinementFailed,
NoMatchingHead,
NoMatchingArm,
CheckedArithNoArm,
EnumVariantUnmapped,
ZeroDivisor,
AssertionFailed,
}
impl TrapKind {
pub fn code(self) -> u16 {
match self {
TrapKind::RefinementFailed => 0,
TrapKind::NoMatchingHead => 1,
TrapKind::NoMatchingArm => 2,
TrapKind::CheckedArithNoArm => 3,
TrapKind::EnumVariantUnmapped => 4,
TrapKind::ZeroDivisor => 5,
TrapKind::AssertionFailed => 6,
}
}
pub fn from_code(code: u16) -> Option<TrapKind> {
match code {
0 => Some(TrapKind::RefinementFailed),
1 => Some(TrapKind::NoMatchingHead),
2 => Some(TrapKind::NoMatchingArm),
3 => Some(TrapKind::CheckedArithNoArm),
4 => Some(TrapKind::EnumVariantUnmapped),
5 => Some(TrapKind::ZeroDivisor),
6 => Some(TrapKind::AssertionFailed),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TupleField {
Flat {
offset: u16,
kind: crate::value_layout::ScalarKind,
},
FlatNested {
offset: u16,
size: u16,
variant: crate::value_layout::CompositeKind,
},
Boxed {
index: u8,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StructField {
Flat {
offset: u16,
kind: crate::value_layout::ScalarKind,
},
FlatNested {
offset: u16,
size: u16,
variant: crate::value_layout::CompositeKind,
},
Boxed {
name_const: u16,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EnumField {
Flat {
offset: u16,
kind: crate::value_layout::ScalarKind,
},
FlatNested {
offset: u16,
size: u16,
variant: crate::value_layout::CompositeKind,
},
Boxed {
index: u8,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArrayElem {
Flat {
kind: crate::value_layout::ScalarKind,
},
FlatNested {
size: u16,
variant: crate::value_layout::CompositeKind,
},
Boxed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NewCompositeOperand {
Flat {
kind: crate::value_layout::CompositeKind,
count: u16,
byte_size: u16,
},
Boxed {
kind: crate::value_layout::CompositeKind,
count: u16,
meta: u16,
},
}
impl NewCompositeOperand {
pub fn kind(&self) -> crate::value_layout::CompositeKind {
match self {
Self::Flat { kind, .. } | Self::Boxed { kind, .. } => *kind,
}
}
pub fn count(&self) -> u16 {
match self {
Self::Flat { count, .. } | Self::Boxed { count, .. } => *count,
}
}
pub fn alloc_bytes(&self) -> u32 {
match self {
Self::Flat { byte_size, .. } => *byte_size as u32,
Self::Boxed { .. } => 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Op {
Const(u16),
GetLocal(u16),
SetLocal(u16),
GetData(u16),
SetData(u16),
GetDataIndexed(u16, u16),
SetDataIndexed(u16, u16),
BoundsCheck(u16),
Add,
Sub,
Mul,
Div,
Mod,
Neg,
CmpEq,
CmpNe,
CmpLt,
CmpGt,
CmpLe,
CmpGe,
Not,
If(u16),
Else(u16),
EndIf,
Loop(u16),
EndLoop(u16),
Break(u16),
BreakIf(u16),
Stream,
Reset,
Call(u16, u8),
Return,
Yield,
Dup,
NewComposite(NewCompositeOperand),
GetField(StructField),
GetIndex(ArrayElem),
GetTupleField(TupleField),
GetEnumField(EnumField),
Len,
IsEnum(u16, u16, u16),
IsStruct(u16),
IntToFloat,
FloatToInt,
WordToByte,
ByteToWord,
WordToFixed(u8),
FixedToWord(u8),
FixedMul(u8),
FixedDiv(u8),
Trap(u16),
CheckedAdd,
CheckedSub,
CheckedMul(u8),
CheckedNeg,
CheckedDiv(u8),
CheckedMod,
PushImmediate(u8),
PopN(u8),
BitAnd,
BitOr,
BitXor,
Shl,
Shr,
CallVerifiedNative(u16, u8),
CallExternalNative(u16, u8),
}
pub const VALUE_SLOT_SIZE_BYTES: u32 = core::mem::size_of::<Value>() as u32;
#[derive(Clone, Copy, Debug, Default)]
pub struct OpCostContext {
pub lhs_text_len: u32,
pub rhs_text_len: u32,
}
#[derive(Clone, Copy)]
pub enum OpCost {
Fixed(u32),
Dynamic(fn(&OpCostContext) -> u32),
}
impl OpCost {
pub fn evaluate(&self, ctx: &OpCostContext) -> u32 {
match self {
OpCost::Fixed(n) => *n,
OpCost::Dynamic(f) => f(ctx),
}
}
}
#[derive(Clone, Copy)]
pub struct CostModel {
pub value_slot_bytes: u32,
pub op_cycles: fn(&Op) -> u32,
pub text_byte_cycles: u32,
}
impl CostModel {
pub fn cycles(&self, op: &Op) -> u32 {
(self.op_cycles)(op)
}
pub fn slots_to_bytes(&self, slots: u32) -> u32 {
slots.saturating_mul(self.value_slot_bytes)
}
pub fn heap_alloc_bytes(&self, op: &Op, chunk: &Chunk) -> u32 {
match self.heap_alloc_cost(op, chunk) {
OpCost::Fixed(n) => n,
OpCost::Dynamic(_) => 0,
}
}
pub fn heap_alloc_cost(&self, op: &Op, _chunk: &Chunk) -> OpCost {
match op {
Op::NewComposite(op) => OpCost::Fixed(op.alloc_bytes()),
Op::Add => OpCost::Dynamic(add_text_heap_alloc_bytes),
_ => OpCost::Fixed(0),
}
}
}
fn add_text_heap_alloc_bytes(ctx: &OpCostContext) -> u32 {
ctx.lhs_text_len.saturating_add(ctx.rhs_text_len)
}
pub const NOMINAL_COST_MODEL: CostModel = CostModel {
value_slot_bytes: VALUE_SLOT_SIZE_BYTES,
op_cycles: nominal_op_cycles,
text_byte_cycles: 1,
};
pub fn nominal_op_cycles(op: &Op) -> u32 {
match op {
Op::Const(_)
| Op::GetLocal(_)
| Op::SetLocal(_)
| Op::GetData(_)
| Op::SetData(_)
| Op::Dup
| Op::Not => 1,
Op::If(_)
| Op::Else(_)
| Op::EndIf
| Op::Loop(_)
| Op::EndLoop(_)
| Op::Break(_)
| Op::BreakIf(_)
| Op::Stream
| Op::Reset
| Op::Yield
| Op::Trap(_) => 1,
Op::Add
| Op::Sub
| Op::CheckedAdd
| Op::CheckedSub
| Op::CheckedMul(_)
| Op::CheckedNeg
| Op::CheckedDiv(_)
| Op::CheckedMod
| Op::Mul
| Op::Neg
| Op::CmpEq
| Op::CmpNe
| Op::CmpLt
| Op::CmpGt
| Op::CmpLe
| Op::CmpGe
| Op::GetIndex(_)
| Op::GetTupleField(_)
| Op::GetEnumField(_)
| Op::Len
| Op::IntToFloat
| Op::FloatToInt
| Op::WordToByte
| Op::ByteToWord
| Op::WordToFixed(_)
| Op::FixedToWord(_)
| Op::FixedMul(_)
| Op::FixedDiv(_)
| Op::Return
| Op::GetDataIndexed(_, _)
| Op::SetDataIndexed(_, _)
| Op::BoundsCheck(_) => 2,
Op::Div | Op::Mod | Op::GetField(_) | Op::IsEnum(_, _, _) | Op::IsStruct(_) => 3,
Op::NewComposite(_) => 5,
Op::Call(_, _) => 10,
Op::PushImmediate(_) | Op::PopN(_) => 1,
Op::BitAnd | Op::BitOr | Op::BitXor | Op::Shl | Op::Shr => 2,
Op::CallVerifiedNative(_, _) | Op::CallExternalNative(_, _) => 10,
}
}
impl Op {
pub fn cost(&self) -> u32 {
NOMINAL_COST_MODEL.cycles(self)
}
pub fn stack_growth(&self) -> u32 {
match self {
Op::Const(_) | Op::GetLocal(_) | Op::GetData(_) | Op::Dup => 1,
Op::Not | Op::Neg => 0,
Op::CheckedAdd
| Op::CheckedSub
| Op::CheckedMul(_)
| Op::CheckedDiv(_)
| Op::CheckedMod => 1,
Op::CheckedNeg => 2,
Op::Add
| Op::Sub
| Op::Mul
| Op::Div
| Op::Mod
| Op::CmpEq
| Op::CmpNe
| Op::CmpLt
| Op::CmpGt
| Op::CmpLe
| Op::CmpGe => 0,
Op::SetLocal(_) | Op::SetData(_) => 0,
Op::GetDataIndexed(_, _) => 1,
Op::SetDataIndexed(_, _) => 0,
Op::BoundsCheck(_) => 0,
Op::If(_) | Op::BreakIf(_) => 0,
Op::Else(_) | Op::EndIf | Op::Loop(_) | Op::EndLoop(_) | Op::Break(_) => 0,
Op::Stream | Op::Reset => 0,
Op::Yield => 0,
Op::Call(_, _) => 1,
Op::Return => 0,
Op::NewComposite(_) => 1,
Op::GetField(_)
| Op::GetIndex(_)
| Op::GetTupleField(_)
| Op::GetEnumField(_)
| Op::Len => 0,
Op::IsEnum(_, _, _) | Op::IsStruct(_) => 1,
Op::IntToFloat
| Op::FloatToInt
| Op::WordToByte
| Op::ByteToWord
| Op::WordToFixed(_)
| Op::FixedToWord(_) => 0,
Op::FixedMul(_) | Op::FixedDiv(_) => 0,
Op::Trap(_) => 0,
Op::PushImmediate(_) => 1,
Op::PopN(_) => 0,
Op::BitAnd | Op::BitOr | Op::BitXor | Op::Shl | Op::Shr => 0,
Op::CallVerifiedNative(_, n) | Op::CallExternalNative(_, n) => {
if n & 0x80 != 0 {
2
} else {
1
}
}
}
}
pub fn stack_shrink(&self) -> u32 {
match self {
Op::Const(_) | Op::GetLocal(_) | Op::GetData(_) | Op::Dup => 0,
Op::Not | Op::Neg => 0,
Op::CheckedAdd
| Op::CheckedSub
| Op::CheckedMul(_)
| Op::CheckedNeg
| Op::CheckedDiv(_)
| Op::CheckedMod => 0,
Op::Add
| Op::Sub
| Op::Mul
| Op::Div
| Op::Mod
| Op::CmpEq
| Op::CmpNe
| Op::CmpLt
| Op::CmpGt
| Op::CmpLe
| Op::CmpGe => 1,
Op::SetLocal(_) | Op::SetData(_) => 1,
Op::GetDataIndexed(_, _) => 1,
Op::SetDataIndexed(_, _) => 2,
Op::BoundsCheck(_) => 0,
Op::If(_) | Op::BreakIf(_) => 1,
Op::Else(_) | Op::EndIf | Op::Loop(_) | Op::EndLoop(_) | Op::Break(_) => 0,
Op::Stream | Op::Reset => 0,
Op::Yield => 1,
Op::Call(_, n) => *n as u32,
Op::Return => 0,
Op::NewComposite(c) => c.count() as u32,
Op::GetField(_) | Op::GetIndex(_) | Op::GetTupleField(_) | Op::GetEnumField(_) => 1,
Op::Len => 0,
Op::IsEnum(_, _, _) | Op::IsStruct(_) => 0,
Op::IntToFloat
| Op::FloatToInt
| Op::WordToByte
| Op::ByteToWord
| Op::WordToFixed(_)
| Op::FixedToWord(_) => 0,
Op::FixedMul(_) | Op::FixedDiv(_) => 0,
Op::Trap(_) => 0,
Op::PushImmediate(_) => 0,
Op::PopN(n) => *n as u32,
Op::BitAnd | Op::BitOr | Op::BitXor | Op::Shl | Op::Shr => 1,
Op::CallVerifiedNative(_, n) | Op::CallExternalNative(_, n) => (*n & 0x7F) as u32,
}
}
pub fn heap_alloc(&self, chunk: &Chunk) -> u32 {
NOMINAL_COST_MODEL.heap_alloc_bytes(self, chunk)
}
}
#[derive(Debug, Clone, Archive, Serialize, Deserialize)]
pub struct StructTemplate {
pub type_name: String,
pub field_names: Vec<String>,
}
#[derive(Debug, Clone, Archive, Serialize, Deserialize)]
pub struct EnumVariantDisc {
pub name: String,
pub disc: i64,
}
#[derive(Debug, Clone, Archive, Serialize, Deserialize)]
pub struct EnumLayout {
pub type_name: String,
pub variants: Vec<EnumVariantDisc>,
pub min_payload: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Archive, Serialize, Deserialize)]
pub enum WireShape {
Top,
Scalar {
kind: u8,
},
Flat {
kind: u8,
size: u32,
},
}
#[derive(Debug, Clone, Archive, Serialize, Deserialize)]
pub struct ChunkSignature {
pub params: Vec<WireShape>,
pub ret: WireShape,
pub resume: WireShape,
}
impl Default for ChunkSignature {
fn default() -> Self {
ChunkSignature {
params: Vec::new(),
ret: WireShape::Top,
resume: WireShape::Top,
}
}
}
#[derive(Debug, Clone, Archive, Serialize, Deserialize)]
pub struct DataSlot {
pub name: String,
pub visibility: SlotVisibility,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Archive, Serialize, Deserialize)]
pub enum SlotVisibility {
Shared,
Private,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Archive, Serialize, Deserialize)]
pub struct SharedSlotLayout {
pub offset: u16,
pub kind: u8,
pub len: u16,
}
pub const SHARED_SLOT_COMPOSITE_FLAG: u8 = 0x80;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Archive, Serialize, Deserialize)]
pub struct PrivateCompositeSlot {
pub slot: u16,
pub offset: u32,
}
#[derive(Debug, Clone, Archive, Serialize, Deserialize)]
pub struct DataLayout {
pub slots: Vec<DataSlot>,
pub shared_layout: Vec<SharedSlotLayout>,
pub private_composite_layout: Vec<PrivateCompositeSlot>,
}
#[derive(Debug, Clone)]
pub struct Chunk {
pub name: String,
pub ops: Vec<Op>,
pub constants: Vec<ConstValue>,
pub struct_templates: Vec<StructTemplate>,
pub local_count: u16,
pub param_count: u8,
pub block_type: BlockType,
pub param_types: Vec<TypeTag>,
pub debug_pool: Option<crate::debug_meta::DebugPool>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Archive, Serialize, Deserialize)]
pub enum TypeTag {
Composite,
Byte,
Word,
Fixed,
Float,
Bool,
Unit,
Text,
}
impl TypeTag {
pub fn from_archived(archived: &ArchivedTypeTag) -> Self {
match archived {
ArchivedTypeTag::Composite => TypeTag::Composite,
ArchivedTypeTag::Byte => TypeTag::Byte,
ArchivedTypeTag::Word => TypeTag::Word,
ArchivedTypeTag::Fixed => TypeTag::Fixed,
ArchivedTypeTag::Float => TypeTag::Float,
ArchivedTypeTag::Bool => TypeTag::Bool,
ArchivedTypeTag::Unit => TypeTag::Unit,
ArchivedTypeTag::Text => TypeTag::Text,
}
}
pub fn admits<W: crate::word::Word, F: crate::float::Float>(
&self,
value: &GenericValue<W, F>,
) -> bool {
match self {
TypeTag::Composite => true,
TypeTag::Byte => matches!(value, GenericValue::Byte(_)),
TypeTag::Word => matches!(value, GenericValue::Int(_)),
TypeTag::Fixed => matches!(value, GenericValue::Fixed(_)),
#[cfg(feature = "floats")]
TypeTag::Float => matches!(value, GenericValue::Float(_)),
#[cfg(not(feature = "floats"))]
TypeTag::Float => false,
TypeTag::Bool => matches!(value, GenericValue::Bool(_)),
TypeTag::Unit => matches!(value, GenericValue::Unit),
TypeTag::Text => {
matches!(value, GenericValue::StaticStr(_) | GenericValue::KStr(_))
}
}
}
pub fn name(&self) -> &'static str {
match self {
TypeTag::Composite => "Composite",
TypeTag::Byte => "Byte",
TypeTag::Word => "Word",
TypeTag::Fixed => "Fixed",
TypeTag::Float => "Float",
TypeTag::Bool => "Bool",
TypeTag::Unit => "Unit",
TypeTag::Text => "Text",
}
}
}
#[derive(Debug, Clone)]
pub struct Module {
pub chunks: Vec<Chunk>,
pub native_names: Vec<String>,
pub entry_point: Option<usize>,
pub data_layout: Option<DataLayout>,
pub word_bits_log2: u8,
pub addr_bits_log2: u8,
pub float_bits_log2: u8,
pub wcet_cycles: u32,
pub wcmu_bytes: u32,
pub aux_arena_bytes: u32,
pub persistent_composite_bytes: u32,
pub flags: u8,
pub shared_data_bytes: u32,
pub private_data_bytes: u32,
pub schema_hash: u32,
pub enum_layouts: Vec<EnumLayout>,
pub signatures: Vec<ChunkSignature>,
pub native_return_shapes: Vec<WireShape>,
}
pub const FLAG_EPHEMERAL: u8 = 0x01;
pub const BYTECODE_MAGIC: [u8; 4] = *b"KELE";
pub const BYTECODE_VERSION: u16 = 1;
#[cfg(feature = "narrow-word-8")]
pub const RUNTIME_WORD_BITS_LOG2: u8 = 3;
#[cfg(all(feature = "narrow-word-16", not(feature = "narrow-word-8")))]
pub const RUNTIME_WORD_BITS_LOG2: u8 = 4;
#[cfg(all(
feature = "narrow-word-32",
not(any(feature = "narrow-word-8", feature = "narrow-word-16"))
))]
pub const RUNTIME_WORD_BITS_LOG2: u8 = 5;
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
pub const RUNTIME_WORD_BITS_LOG2: u8 = 6;
#[cfg(feature = "narrow-address-8")]
pub const RUNTIME_ADDRESS_BITS_LOG2: u8 = 3;
#[cfg(all(feature = "narrow-address-16", not(feature = "narrow-address-8")))]
pub const RUNTIME_ADDRESS_BITS_LOG2: u8 = 4;
#[cfg(all(
feature = "narrow-address-32",
not(any(feature = "narrow-address-8", feature = "narrow-address-16"))
))]
pub const RUNTIME_ADDRESS_BITS_LOG2: u8 = 5;
#[cfg(not(any(
feature = "narrow-address-8",
feature = "narrow-address-16",
feature = "narrow-address-32"
)))]
pub const RUNTIME_ADDRESS_BITS_LOG2: u8 = 6;
#[cfg(feature = "narrow-float-32")]
pub const RUNTIME_FLOAT_BITS_LOG2: u8 = 5;
#[cfg(not(feature = "narrow-float-32"))]
pub const RUNTIME_FLOAT_BITS_LOG2: u8 = 6;
const CRC32_POLY: u32 = 0xEDB88320;
pub fn compute_schema_hash(layout: Option<&DataLayout>) -> u32 {
let layout = match layout {
Some(l) => l,
None => return 0,
};
if layout.slots.is_empty() {
return 0;
}
let mut buf: Vec<u8> = Vec::new();
for slot in &layout.slots {
buf.extend_from_slice(slot.name.as_bytes());
buf.push(0x00);
let vis_tag = match slot.visibility {
SlotVisibility::Shared => b'S',
SlotVisibility::Private => b'P',
};
buf.push(vis_tag);
buf.push(b'\n');
}
crc32(&buf)
}
pub(crate) fn crc32(bytes: &[u8]) -> u32 {
let mut crc: u32 = 0xFFFFFFFF;
for &byte in bytes {
crc ^= byte as u32;
for _ in 0..8 {
crc = if crc & 1 != 0 {
(crc >> 1) ^ CRC32_POLY
} else {
crc >> 1
};
}
}
crc ^ 0xFFFFFFFF
}
#[derive(Debug, Clone)]
pub enum LoadError {
BadMagic,
Truncated,
UnsupportedVersion {
got: u16,
expected: u16,
},
WordSizeMismatch {
got: u8,
max_supported: u8,
},
AddressSizeMismatch {
got: u8,
max_supported: u8,
},
FloatSizeMismatch {
got: u8,
max_supported: u8,
},
BadChecksum,
WcetOverflow,
WcmuOverflow,
Codec(String),
InvalidSignature,
SignaturesUnsupported,
}
impl core::fmt::Display for LoadError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
LoadError::BadMagic => f.write_str("bytecode header missing magic 'KELE'"),
LoadError::Truncated => f.write_str(
"bytecode truncated, recorded length exceeds slice, or below minimum framing",
),
LoadError::UnsupportedVersion { got, expected } => {
write!(
f,
"bytecode version {} not supported, expected {}",
got, expected
)
}
LoadError::WordSizeMismatch { got, max_supported } => {
write!(
f,
"bytecode requires {}-bit words, runtime supports up to {}-bit",
1u32 << got,
1u32 << max_supported
)
}
LoadError::AddressSizeMismatch { got, max_supported } => {
write!(
f,
"bytecode requires {}-bit addresses, runtime supports up to {}-bit",
1u32 << got,
1u32 << max_supported
)
}
LoadError::FloatSizeMismatch { got, max_supported } => {
write!(
f,
"bytecode requires {}-bit floats, runtime supports up to {}-bit",
1u32 << got,
1u32 << max_supported
)
}
LoadError::BadChecksum => f.write_str("bytecode CRC-32 residue check failed"),
LoadError::WcetOverflow => {
f.write_str("declared WCET is u32::MAX (overflow); no representable bound")
}
LoadError::WcmuOverflow => {
f.write_str("declared WCMU is u32::MAX (overflow); no representable bound")
}
LoadError::Codec(msg) => write!(f, "bytecode codec error: {}", msg),
LoadError::InvalidSignature => {
f.write_str("bytecode signature did not verify against any registered key")
}
LoadError::SignaturesUnsupported => f.write_str(
"bytecode is signed but the runtime build does not include the `signatures` feature",
),
}
}
}
impl core::error::Error for LoadError {}
impl Module {
pub fn to_bytes(&self) -> Result<Vec<u8>, LoadError> {
crate::wire_format::module_to_wire_bytes(self)
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, LoadError> {
crate::wire_format::module_from_wire_bytes(bytes)
}
pub fn access_bytes(
bytes: &[u8],
) -> Result<&crate::wire_format::ArchivedWireAuxBody, LoadError> {
use alloc::format;
let header = crate::wire_format::read_header_fields(bytes)?;
if header.word_bits_log2 > RUNTIME_WORD_BITS_LOG2 {
return Err(LoadError::WordSizeMismatch {
got: header.word_bits_log2,
max_supported: RUNTIME_WORD_BITS_LOG2,
});
}
if header.addr_bits_log2 > RUNTIME_ADDRESS_BITS_LOG2 {
return Err(LoadError::AddressSizeMismatch {
got: header.addr_bits_log2,
max_supported: RUNTIME_ADDRESS_BITS_LOG2,
});
}
if header.float_bits_log2 > RUNTIME_FLOAT_BITS_LOG2 {
return Err(LoadError::FloatSizeMismatch {
got: header.float_bits_log2,
max_supported: RUNTIME_FLOAT_BITS_LOG2,
});
}
if header.wcet_cycles == u32::MAX {
return Err(LoadError::WcetOverflow);
}
if header.wcmu_bytes == u32::MAX {
return Err(LoadError::WcmuOverflow);
}
let sections = crate::wire_format::parse_wire_sections(bytes)?;
if !(sections.aux_body.as_ptr() as usize).is_multiple_of(8) {
return Err(LoadError::Codec(format!(
"auxiliary body not 8-byte aligned (slice base 0x{:x}); use Module::from_bytes for unaligned input",
bytes.as_ptr() as usize
)));
}
rkyv::access::<crate::wire_format::ArchivedWireAuxBody, rkyv::rancor::Error>(
sections.aux_body,
)
.map_err(|e| LoadError::Codec(format!("rkyv access failed: {}", e)))
}
pub fn view_bytes(bytes: &[u8]) -> Result<Module, LoadError> {
crate::wire_format::module_from_wire_bytes(bytes)
}
}
impl ConstValue {
pub fn try_from_value(value: Value) -> Result<Self, &'static str> {
match value {
Value::Unit => Ok(ConstValue::Unit),
Value::Bool(b) => Ok(ConstValue::Bool(b)),
Value::Int(i) => Ok(ConstValue::Int(i)),
Value::Byte(b) => Ok(ConstValue::Byte(b)),
Value::Fixed(i) => Ok(ConstValue::Fixed(i)),
#[cfg(feature = "floats")]
Value::Float(f) => Ok(ConstValue::Float(f)),
Value::StaticStr(s) => Ok(ConstValue::StaticStr(s)),
Value::KStr(_) => Err("KStr cannot be a compile-time constant"),
Value::Opaque(_) => Err("Opaque cannot be a compile-time constant"),
Value::OpaqueRef(_) => Err("Opaque cannot be a compile-time constant"),
Value::Tuple(items) => items
.into_elements()
.into_iter()
.map(ConstValue::try_from_value)
.collect::<Result<Vec<_>, _>>()
.map(ConstValue::Tuple),
Value::Array(items) => items
.into_elements()
.into_iter()
.map(ConstValue::try_from_value)
.collect::<Result<Vec<_>, _>>()
.map(ConstValue::Array),
Value::Struct(StructBody::Boxed(b)) => {
let BoxedStruct { type_name, fields } = *b;
let cfields: Result<Vec<_>, _> = fields
.into_iter()
.map(|(n, v)| ConstValue::try_from_value(v).map(|cv| (n, cv)))
.collect();
Ok(ConstValue::Struct {
type_name,
fields: cfields?,
})
}
Value::Struct(StructBody::Flat(_)) => {
Err("a flat struct cannot be converted to a compile-time constant")
}
Value::Enum(EnumBody::Boxed(b)) => {
let BoxedEnum {
type_name,
variant,
fields,
..
} = *b;
let cfields: Result<Vec<_>, _> =
fields.into_iter().map(ConstValue::try_from_value).collect();
Ok(ConstValue::Enum {
type_name,
variant,
discriminant: None,
fields: cfields?,
})
}
Value::Enum(EnumBody::Flat(_)) => {
Err("a flat enum cannot be converted to a compile-time constant")
}
Value::None => Ok(ConstValue::None),
#[cfg(not(feature = "floats"))]
Value::_PhantomFloat(_) => unreachable!("_PhantomFloat is never constructed"),
}
}
pub fn into_value(self) -> Value {
match self {
ConstValue::Unit => Value::Unit,
ConstValue::Bool(b) => Value::Bool(b),
ConstValue::Int(i) => Value::Int(i),
ConstValue::Byte(b) => Value::Byte(b),
ConstValue::Fixed(i) => Value::Fixed(i),
#[cfg(feature = "floats")]
ConstValue::Float(f) => Value::Float(f),
ConstValue::StaticStr(s) => Value::StaticStr(s),
ConstValue::Tuple(items) => Value::tuple_with_widths(
items.into_iter().map(ConstValue::into_value).collect(),
8,
8,
),
ConstValue::Array(items) => Value::array_with_widths(
items.into_iter().map(ConstValue::into_value).collect(),
8,
8,
),
ConstValue::Struct { type_name, fields } => Value::struct_with_widths(
type_name,
fields
.into_iter()
.map(|(n, v)| (n, v.into_value()))
.collect(),
8,
8,
),
ConstValue::Enum {
type_name,
variant,
discriminant,
fields,
} => {
let vals: Vec<Value> = fields.into_iter().map(ConstValue::into_value).collect();
match discriminant {
Some(disc) => Value::enum_with_widths(type_name, variant, disc, vals, 0, 8, 8),
None => Value::Enum(EnumBody::boxed(type_name, variant, vals)),
}
}
ConstValue::None => Value::None,
}
}
}
impl PartialEq for ConstValue {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(ConstValue::Unit, ConstValue::Unit) | (ConstValue::None, ConstValue::None) => true,
(ConstValue::Bool(a), ConstValue::Bool(b)) => a == b,
(ConstValue::Int(a), ConstValue::Int(b)) => a == b,
(ConstValue::Byte(a), ConstValue::Byte(b)) => a == b,
(ConstValue::Fixed(a), ConstValue::Fixed(b)) => a == b,
#[cfg(feature = "floats")]
(ConstValue::Float(a), ConstValue::Float(b)) => a == b,
(ConstValue::StaticStr(a), ConstValue::StaticStr(b)) => a == b,
(ConstValue::Tuple(a), ConstValue::Tuple(b))
| (ConstValue::Array(a), ConstValue::Array(b)) => a == b,
(
ConstValue::Struct {
type_name: na,
fields: fa,
},
ConstValue::Struct {
type_name: nb,
fields: fb,
},
) => na == nb && fa == fb,
(
ConstValue::Enum {
type_name: na,
variant: va,
fields: fa,
..
},
ConstValue::Enum {
type_name: nb,
variant: vb,
fields: fb,
..
},
) => na == nb && va == vb && fa == fb,
_ => false,
}
}
}
pub(crate) fn const_flat_bytes(
c: &ArchivedConstValue,
word_bytes: usize,
float_bytes: usize,
) -> Option<alloc::vec::Vec<u8>> {
let mut buf = alloc::vec::Vec::new();
const_flat_bytes_into(c, word_bytes, float_bytes, &mut buf)?;
Some(buf)
}
fn const_flat_bytes_into(
c: &ArchivedConstValue,
word_bytes: usize,
float_bytes: usize,
buf: &mut alloc::vec::Vec<u8>,
) -> Option<()> {
use ArchivedConstValue as A;
match c {
A::Unit => {}
A::Bool(b) => buf.push(u8::from(*b)),
A::Byte(b) => buf.push(*b),
A::Int(i) | A::Fixed(i) => {
let le = i.to_native().to_le_bytes();
buf.extend_from_slice(&le[..word_bytes]);
}
#[cfg(feature = "floats")]
A::Float(f) => {
let v = f.to_native();
match float_bytes {
8 => buf.extend_from_slice(&v.to_le_bytes()),
4 => buf.extend_from_slice(&(v as f32).to_le_bytes()),
_ => return None,
}
}
A::StaticStr(_) => return None,
A::None => return None,
A::Tuple(items) | A::Array(items) => {
for it in items.iter() {
const_flat_bytes_into(it, word_bytes, float_bytes, buf)?;
}
}
A::Struct { fields, .. } => {
for kv in fields.iter() {
const_flat_bytes_into(&kv.1, word_bytes, float_bytes, buf)?;
}
}
A::Enum {
discriminant,
fields,
..
} => {
let disc = discriminant.as_ref()?.to_native();
let le = disc.to_le_bytes();
buf.extend_from_slice(&le[..word_bytes]);
for f in fields.iter() {
const_flat_bytes_into(f, word_bytes, float_bytes, buf)?;
}
}
}
Some(())
}
pub fn value_from_archived<W: crate::word::Word, F: crate::float::Float>(
archived: &ArchivedConstValue,
word_bytes: usize,
float_bytes: usize,
) -> GenericValue<W, F> {
GenericValue::<W, F>::from_const_archived(archived, word_bytes, float_bytes)
}
pub(crate) fn truncate_int_to_declared_width(value: i64, word_bits_log2: u8) -> i64 {
if word_bits_log2 >= 6 {
return value;
}
let bits = 1u32 << word_bits_log2;
let shift = 64 - bits;
(value << shift) >> shift
}
#[cfg(test)]
mod cost_model_tests {
use super::*;
#[test]
fn nominal_cost_model_value_slot_bytes_matches_constant() {
assert_eq!(NOMINAL_COST_MODEL.value_slot_bytes, VALUE_SLOT_SIZE_BYTES);
}
#[test]
fn runtime_width_constants_track_narrowing_features() {
#[cfg(feature = "narrow-word-8")]
assert_eq!(RUNTIME_WORD_BITS_LOG2, 3);
#[cfg(all(feature = "narrow-word-16", not(feature = "narrow-word-8")))]
assert_eq!(RUNTIME_WORD_BITS_LOG2, 4);
#[cfg(all(
feature = "narrow-word-32",
not(any(feature = "narrow-word-8", feature = "narrow-word-16"))
))]
assert_eq!(RUNTIME_WORD_BITS_LOG2, 5);
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
assert_eq!(RUNTIME_WORD_BITS_LOG2, 6);
#[cfg(feature = "narrow-address-8")]
assert_eq!(RUNTIME_ADDRESS_BITS_LOG2, 3);
#[cfg(all(feature = "narrow-address-16", not(feature = "narrow-address-8")))]
assert_eq!(RUNTIME_ADDRESS_BITS_LOG2, 4);
#[cfg(all(
feature = "narrow-address-32",
not(any(feature = "narrow-address-8", feature = "narrow-address-16"))
))]
assert_eq!(RUNTIME_ADDRESS_BITS_LOG2, 5);
#[cfg(not(any(
feature = "narrow-address-8",
feature = "narrow-address-16",
feature = "narrow-address-32"
)))]
assert_eq!(RUNTIME_ADDRESS_BITS_LOG2, 6);
#[cfg(feature = "narrow-float-32")]
assert_eq!(RUNTIME_FLOAT_BITS_LOG2, 5);
#[cfg(not(feature = "narrow-float-32"))]
assert_eq!(RUNTIME_FLOAT_BITS_LOG2, 6);
}
#[test]
fn nominal_cost_model_cycles_match_op_cost_method() {
let ops: alloc::vec::Vec<Op> = alloc::vec![
Op::Const(0),
Op::PushImmediate(0),
Op::Add,
Op::Mul,
Op::Div,
Op::NewComposite(NewCompositeOperand::Flat {
kind: crate::value_layout::CompositeKind::Array,
count: 2,
byte_size: 16,
}),
Op::Call(0, 0),
Op::Yield,
];
for op in &ops {
assert_eq!(NOMINAL_COST_MODEL.cycles(op), op.cost());
}
}
#[test]
fn cost_model_slots_to_bytes_uses_slot_size() {
let model = CostModel {
value_slot_bytes: 8,
op_cycles: nominal_op_cycles,
text_byte_cycles: 1,
};
assert_eq!(model.slots_to_bytes(0), 0);
assert_eq!(model.slots_to_bytes(1), 8);
assert_eq!(model.slots_to_bytes(4), 32);
}
#[test]
fn cost_model_heap_alloc_bytes_is_operand_exact_not_slot_scaled() {
let nominal = NOMINAL_COST_MODEL;
let custom = CostModel {
value_slot_bytes: VALUE_SLOT_SIZE_BYTES / 2,
op_cycles: nominal_op_cycles,
text_byte_cycles: 1,
};
let chunk = Chunk {
name: alloc::string::String::from("test"),
ops: alloc::vec::Vec::new(),
constants: alloc::vec::Vec::new(),
struct_templates: alloc::vec::Vec::new(),
local_count: 0,
param_count: 0,
block_type: BlockType::Func,
param_types: alloc::vec::Vec::new(),
debug_pool: None,
};
let op = Op::NewComposite(NewCompositeOperand::Flat {
kind: crate::value_layout::CompositeKind::Array,
count: 4,
byte_size: 32,
});
let nominal_bytes = nominal.heap_alloc_bytes(&op, &chunk);
let custom_bytes = custom.heap_alloc_bytes(&op, &chunk);
assert_eq!(nominal_bytes, 32);
assert_eq!(custom_bytes, 32);
assert_eq!(custom_bytes, nominal_bytes);
}
#[test]
fn custom_cost_model_returns_custom_cycles() {
fn flat_hundred(_op: &Op) -> u32 {
100
}
let custom = CostModel {
value_slot_bytes: VALUE_SLOT_SIZE_BYTES,
op_cycles: flat_hundred,
text_byte_cycles: 1,
};
assert_eq!(custom.cycles(&Op::Add), 100);
assert_eq!(custom.cycles(&Op::PushImmediate(0)), 100);
assert_eq!(custom.cycles(&Op::Call(0, 0)), 100);
}
#[test]
fn op_cost_fixed_evaluates_to_inner_value() {
let ctx = OpCostContext::default();
assert_eq!(OpCost::Fixed(42).evaluate(&ctx), 42);
assert_eq!(OpCost::Fixed(0).evaluate(&ctx), 0);
}
#[test]
fn op_cost_dynamic_invokes_function_with_context() {
fn sum_lengths(ctx: &OpCostContext) -> u32 {
ctx.lhs_text_len.saturating_add(ctx.rhs_text_len)
}
let cost = OpCost::Dynamic(sum_lengths);
let ctx = OpCostContext {
lhs_text_len: 100,
rhs_text_len: 200,
};
assert_eq!(cost.evaluate(&ctx), 300);
}
#[test]
fn op_cost_dynamic_saturates_at_u32_max_for_unbounded_operand() {
fn sum_lengths(ctx: &OpCostContext) -> u32 {
ctx.lhs_text_len.saturating_add(ctx.rhs_text_len)
}
let cost = OpCost::Dynamic(sum_lengths);
let ctx = OpCostContext {
lhs_text_len: u32::MAX,
rhs_text_len: 100,
};
assert_eq!(cost.evaluate(&ctx), u32::MAX);
}
#[test]
fn heap_alloc_cost_text_add_is_dynamic() {
let chunk = Chunk {
name: alloc::string::String::from("test"),
ops: alloc::vec::Vec::new(),
constants: alloc::vec::Vec::new(),
struct_templates: alloc::vec::Vec::new(),
local_count: 0,
param_count: 0,
block_type: BlockType::Func,
param_types: alloc::vec::Vec::new(),
debug_pool: None,
};
let cost = NOMINAL_COST_MODEL.heap_alloc_cost(&Op::Add, &chunk);
assert!(matches!(cost, OpCost::Dynamic(_)));
let ctx = OpCostContext {
lhs_text_len: 5,
rhs_text_len: 6,
};
assert_eq!(cost.evaluate(&ctx), 11);
}
#[test]
fn heap_alloc_cost_composite_is_fixed() {
let chunk = Chunk {
name: alloc::string::String::from("test"),
ops: alloc::vec::Vec::new(),
constants: alloc::vec::Vec::new(),
struct_templates: alloc::vec::Vec::new(),
local_count: 0,
param_count: 0,
block_type: BlockType::Func,
param_types: alloc::vec::Vec::new(),
debug_pool: None,
};
let op = Op::NewComposite(NewCompositeOperand::Flat {
kind: crate::value_layout::CompositeKind::Array,
count: 3,
byte_size: 24,
});
let cost = NOMINAL_COST_MODEL.heap_alloc_cost(&op, &chunk);
assert!(matches!(cost, OpCost::Fixed(_)));
assert_eq!(cost.evaluate(&OpCostContext::default()), 24);
}
#[test]
fn heap_alloc_bytes_text_add_reports_zero_in_fixed_view() {
let chunk = Chunk {
name: alloc::string::String::from("test"),
ops: alloc::vec::Vec::new(),
constants: alloc::vec::Vec::new(),
struct_templates: alloc::vec::Vec::new(),
local_count: 0,
param_count: 0,
block_type: BlockType::Func,
param_types: alloc::vec::Vec::new(),
debug_pool: None,
};
assert_eq!(NOMINAL_COST_MODEL.heap_alloc_bytes(&Op::Add, &chunk), 0);
}
}
#[cfg(test)]
mod flat_scalar_bridge_tests {
use super::*;
use crate::value_layout::ScalarKind;
type V = Value;
const W8: usize = 8;
const F8: usize = 8;
fn roundtrip(v: V, kind: ScalarKind, word_bytes: usize, float_bytes: usize, size: usize) -> V {
let mut buf = alloc::vec![0u8; size];
v.write_scalar_le(&mut buf, 0, word_bytes, float_bytes)
.expect("write_scalar_le in roundtrip");
V::read_scalar_le(&buf, 0, kind, word_bytes, float_bytes)
.expect("read_scalar_le in roundtrip")
}
#[test]
fn bool_byte_roundtrip() {
assert_eq!(
roundtrip(V::Bool(true), ScalarKind::Bool, W8, F8, 1),
V::Bool(true)
);
assert_eq!(
roundtrip(V::Bool(false), ScalarKind::Bool, W8, F8, 1),
V::Bool(false)
);
assert_eq!(
roundtrip(V::Byte(0xAB), ScalarKind::Byte, W8, F8, 1),
V::Byte(0xAB)
);
}
#[test]
fn int_roundtrip_full_word() {
for n in [0i64, 42, -5, i64::MAX, i64::MIN, -1] {
assert_eq!(roundtrip(V::Int(n), ScalarKind::Int, W8, F8, 8), V::Int(n));
}
}
#[test]
fn fixed_roundtrip_returns_fixed_kind() {
assert_eq!(
roundtrip(V::Fixed(-123), ScalarKind::Fixed, W8, F8, 8),
V::Fixed(-123)
);
}
#[test]
fn int_narrow_word_sign_extends() {
assert_eq!(roundtrip(V::Int(-5), ScalarKind::Int, 2, F8, 2), V::Int(-5));
assert_eq!(
roundtrip(V::Int(1234), ScalarKind::Int, 2, F8, 2),
V::Int(1234)
);
assert_eq!(
roundtrip(V::Int(-32768), ScalarKind::Int, 2, F8, 2),
V::Int(-32768)
);
}
#[test]
fn unit_writes_no_bytes() {
let mut buf = [0xFFu8; 0];
V::Unit
.write_scalar_le(&mut buf, 0, W8, F8)
.expect("write unit");
assert_eq!(
V::read_scalar_le(&buf, 0, ScalarKind::Unit, W8, F8).expect("read unit"),
V::Unit
);
}
#[test]
fn read_write_scalar_le_are_total() {
use crate::bytecode::ScalarError;
let buf = [0u8; 2];
assert_eq!(
V::read_scalar_le(&buf, 8, ScalarKind::Int, W8, F8),
Err(ScalarError::OutOfBounds),
"offset past the end"
);
assert_eq!(
V::read_scalar_le(&buf, 0, ScalarKind::Int, W8, F8),
Err(ScalarError::OutOfBounds),
"an 8-byte word read overruns a 2-byte buffer"
);
assert_eq!(
V::read_scalar_le(&buf, 0, ScalarKind::Text, W8, F8),
Err(ScalarError::ReferenceKind),
"a reference kind on the fixed-scalar path"
);
let mut dst = [0u8; 1];
assert_eq!(
V::Bool(true).write_scalar_le(&mut dst, 4, W8, F8),
Err(ScalarError::OutOfBounds),
"write past the end"
);
}
#[cfg(feature = "floats")]
#[test]
fn float_roundtrip_f64_and_f32_width() {
assert_eq!(
roundtrip(V::Float(0.1), ScalarKind::Float, W8, F8, 8),
V::Float(0.1)
);
assert_eq!(
roundtrip(V::Float(0.5), ScalarKind::Float, W8, 4, 4),
V::Float(0.5)
);
}
}
#[cfg(test)]
mod materialise_kstrings_tests {
use super::*;
use crate::kstring::KString;
type V = Value;
fn make_arena() -> keleusma_arena::Arena {
keleusma_arena::Arena::with_capacity(1024)
}
#[test]
fn scalar_values_are_cloned_unchanged() {
let arena = make_arena();
assert_eq!(V::Int(42).materialise_kstrings(&arena), V::Int(42));
assert_eq!(V::Bool(true).materialise_kstrings(&arena), V::Bool(true));
assert_eq!(V::Unit.materialise_kstrings(&arena), V::Unit);
assert_eq!(V::None.materialise_kstrings(&arena), V::None);
}
#[test]
fn staticstr_is_cloned_unchanged() {
let arena = make_arena();
let v = V::StaticStr(alloc::string::String::from("hello"));
assert_eq!(v.materialise_kstrings(&arena), v);
}
#[test]
fn kstr_becomes_staticstr_with_arena_contents() {
let arena = make_arena();
let handle = KString::alloc(&arena, "the original bytes").expect("alloc");
let v: V = V::KStr(handle);
let materialised = v.materialise_kstrings(&arena);
match materialised {
V::StaticStr(s) => assert_eq!(s, "the original bytes"),
other => panic!("expected StaticStr, got {:?}", other),
}
}
#[test]
fn tuple_walks_recursively() {
let arena = make_arena();
let handle = KString::alloc(&arena, "inner").expect("alloc");
let v = V::Tuple(TupleBody::boxed(alloc::vec![
V::Int(1),
V::KStr(handle),
V::Bool(false),
]));
let materialised = v.materialise_kstrings(&arena);
match materialised {
V::Tuple(items) => {
let items = items.elements();
assert_eq!(items.len(), 3);
assert_eq!(items[0], V::Int(1));
match &items[1] {
V::StaticStr(s) => assert_eq!(s, "inner"),
other => panic!("expected StaticStr inside tuple, got {:?}", other),
}
assert_eq!(items[2], V::Bool(false));
}
other => panic!("expected Tuple, got {:?}", other),
}
}
#[test]
fn enum_with_kstr_payload_walks_recursively() {
let arena = make_arena();
let handle = KString::alloc(&arena, "payload").expect("alloc");
let v = V::Enum(EnumBody::boxed(
alloc::string::String::from("Option"),
alloc::string::String::from("Some"),
alloc::vec![V::KStr(handle)],
));
let materialised = v.materialise_kstrings(&arena);
match materialised {
V::Enum(EnumBody::Boxed(b)) => {
assert_eq!(b.fields.len(), 1);
match &b.fields[0] {
V::StaticStr(s) => assert_eq!(s, "payload"),
other => panic!("expected StaticStr inside enum, got {:?}", other),
}
}
other => panic!("expected Enum, got {:?}", other),
}
}
#[test]
fn struct_walks_recursively() {
let arena = make_arena();
let handle = KString::alloc(&arena, "field-value").expect("alloc");
let v = V::Struct(StructBody::boxed(
alloc::string::String::from("Point"),
alloc::vec![
(alloc::string::String::from("x"), V::Int(7)),
(alloc::string::String::from("name"), V::KStr(handle)),
],
));
let materialised = v.materialise_kstrings(&arena);
match materialised {
V::Struct(StructBody::Boxed(b)) => {
assert_eq!(b.fields.len(), 2);
assert_eq!(b.fields[0].1, V::Int(7));
match &b.fields[1].1 {
V::StaticStr(s) => assert_eq!(s, "field-value"),
other => panic!("expected StaticStr inside struct, got {:?}", other),
}
}
other => panic!("expected Struct, got {:?}", other),
}
}
}