#![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,
#[rkyv(omit_bounds)]
fields: Vec<ConstValue>,
},
None,
}
pub type Value = GenericValue<i64, f64>;
#[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(Vec<GenericValue<W, F>>),
Array(Vec<GenericValue<W, F>>),
Struct {
type_name: String,
fields: Vec<(String, GenericValue<W, F>)>,
},
Enum {
type_name: String,
variant: String,
fields: Vec<GenericValue<W, F>>,
},
None,
Opaque(alloc::sync::Arc<dyn crate::opaque::HostOpaque>),
#[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)) | (Self::Array(a), Self::Array(b)) => a == b,
(
Self::Struct {
type_name: na,
fields: fa,
},
Self::Struct {
type_name: nb,
fields: fb,
},
) => na == nb && fa == fb,
(
Self::Enum {
type_name: na,
variant: va,
fields: fa,
},
Self::Enum {
type_name: nb,
variant: vb,
fields: fb,
},
) => na == nb && va == vb && fa == fb,
(Self::Opaque(a), Self::Opaque(b)) => alloc::sync::Arc::ptr_eq(a, b),
_ => false,
}
}
}
impl<W: crate::word::Word, F: crate::float::Float> GenericValue<W, F> {
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",
#[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(items) | Self::Array(items) => items.iter().any(Self::contains_dynstr),
Self::Struct { fields, .. } => fields.iter().any(|(_, v)| v.contains_dynstr()),
Self::Enum { fields, .. } => fields.iter().any(Self::contains_dynstr),
_ => false,
}
}
pub fn from_const_archived(c: &ArchivedConstValue) -> 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(items.iter().map(Self::from_const_archived).collect())
}
ArchivedConstValue::Array(items) => {
Self::Array(items.iter().map(Self::from_const_archived).collect())
}
ArchivedConstValue::Struct { type_name, fields } => {
use alloc::string::ToString;
Self::Struct {
type_name: type_name.as_str().to_string(),
fields: fields
.iter()
.map(|kv| (kv.0.as_str().to_string(), Self::from_const_archived(&kv.1)))
.collect(),
}
}
ArchivedConstValue::Enum {
type_name,
variant,
fields,
} => {
use alloc::string::ToString;
Self::Enum {
type_name: type_name.as_str().to_string(),
variant: variant.as_str().to_string(),
fields: fields.iter().map(Self::from_const_archived).collect(),
}
}
ArchivedConstValue::None => Self::None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Archive, Serialize, Deserialize)]
pub enum BlockType {
Func,
Reentrant,
Stream,
}
#[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,
NewStruct(u16),
NewEnum(u16, u16, u8),
NewArray(u16),
NewTuple(u8),
GetField(u16),
GetIndex,
GetTupleField(u8),
GetEnumField(u8),
Len,
IsEnum(u16, u16),
IsStruct(u16),
IntToFloat,
FloatToInt,
WordToByte,
ByteToWord,
WordToFixed(u8),
FixedToWord(u8),
FixedMul(u8),
FixedDiv(u8),
Trap(u16),
CheckedAdd,
CheckedSub,
CheckedMul,
CheckedNeg,
CheckedDiv,
CheckedMod,
PushImmediate(u8),
PopN(u8),
BitAnd,
BitOr,
BitXor,
Shl,
Shr,
CallVerifiedNative(u16, u8),
CallExternalNative(u16, u8),
}
pub const VALUE_SLOT_SIZE_BYTES: u32 = 32;
#[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,
}
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::NewStruct(template_idx) => {
let idx = *template_idx as usize;
let field_count = chunk
.struct_templates
.get(idx)
.map_or(0, |t| t.field_names.len() as u32);
OpCost::Fixed(self.slots_to_bytes(field_count))
}
Op::NewEnum(_, _, n) => OpCost::Fixed(self.slots_to_bytes(*n as u32)),
Op::NewArray(n) => OpCost::Fixed(self.slots_to_bytes(*n as u32)),
Op::NewTuple(n) => OpCost::Fixed(self.slots_to_bytes(*n as u32)),
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,
};
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::NewStruct(_) | Op::NewEnum(_, _, _) | Op::NewArray(_) | Op::NewTuple(_) => 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::NewStruct(_) | Op::NewEnum(_, _, _) | Op::NewArray(_) | Op::NewTuple(_) => 1,
Op::GetField(_)
| Op::GetIndex
| Op::GetTupleField(_)
| Op::GetEnumField(_)
| 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(_) => 1,
Op::PopN(_) => 0,
Op::BitAnd | Op::BitOr | Op::BitXor | Op::Shl | Op::Shr => 0,
Op::CallVerifiedNative(_, _) | Op::CallExternalNative(_, _) => 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::NewStruct(_) => 0,
Op::NewEnum(_, _, n) => *n as u32,
Op::NewArray(n) => *n as u32,
Op::NewTuple(n) => *n 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 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 DataSlot {
pub name: String,
pub visibility: SlotVisibility,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Archive, Serialize, Deserialize)]
pub enum SlotVisibility {
Shared,
Private,
}
#[derive(Debug, Clone, Archive, Serialize, Deserialize)]
pub struct DataLayout {
pub slots: Vec<DataSlot>,
}
#[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>,
}
#[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 flags: u8,
pub shared_data_bytes: u32,
pub private_data_bytes: u32,
pub schema_hash: u32,
}
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::Tuple(items) => items
.into_iter()
.map(ConstValue::try_from_value)
.collect::<Result<Vec<_>, _>>()
.map(ConstValue::Tuple),
Value::Array(items) => items
.into_iter()
.map(ConstValue::try_from_value)
.collect::<Result<Vec<_>, _>>()
.map(ConstValue::Array),
Value::Struct { type_name, fields } => {
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::Enum {
type_name,
variant,
fields,
} => {
let cfields: Result<Vec<_>, _> =
fields.into_iter().map(ConstValue::try_from_value).collect();
Ok(ConstValue::Enum {
type_name,
variant,
fields: cfields?,
})
}
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(items.into_iter().map(ConstValue::into_value).collect())
}
ConstValue::Array(items) => {
Value::Array(items.into_iter().map(ConstValue::into_value).collect())
}
ConstValue::Struct { type_name, fields } => Value::Struct {
type_name,
fields: fields
.into_iter()
.map(|(n, v)| (n, v.into_value()))
.collect(),
},
ConstValue::Enum {
type_name,
variant,
fields,
} => Value::Enum {
type_name,
variant,
fields: fields.into_iter().map(ConstValue::into_value).collect(),
},
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 fn value_from_archived<W: crate::word::Word, F: crate::float::Float>(
archived: &ArchivedConstValue,
) -> GenericValue<W, F> {
GenericValue::<W, F>::from_const_archived(archived)
}
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::NewArray(2),
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,
};
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_scales_with_slot_size() {
let nominal = NOMINAL_COST_MODEL;
let custom = CostModel {
value_slot_bytes: VALUE_SLOT_SIZE_BYTES / 2,
op_cycles: nominal_op_cycles,
};
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(),
};
let op = Op::NewArray(4);
let nominal_bytes = nominal.heap_alloc_bytes(&op, &chunk);
let custom_bytes = custom.heap_alloc_bytes(&op, &chunk);
assert_eq!(nominal_bytes, 4 * VALUE_SLOT_SIZE_BYTES);
assert_eq!(custom_bytes, 4 * (VALUE_SLOT_SIZE_BYTES / 2));
assert_eq!(custom_bytes * 2, 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,
};
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(),
};
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(),
};
let cost = NOMINAL_COST_MODEL.heap_alloc_cost(&Op::NewArray(3), &chunk);
assert!(matches!(cost, OpCost::Fixed(_)));
assert_eq!(
cost.evaluate(&OpCostContext::default()),
3 * VALUE_SLOT_SIZE_BYTES
);
}
#[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(),
};
assert_eq!(NOMINAL_COST_MODEL.heap_alloc_bytes(&Op::Add, &chunk), 0);
}
}