pub mod runtime;
use std::collections::BTreeMap;
use crate::ir::{
ControlOp, EffectContract, EffectResource, IntrinsicDescriptor, IntrinsicOp, MemoryOp,
MemoryRegion, TypedMemoryAccess, WeavyLowered, WeavyOp, WeavyProgram,
};
pub type DefaultThunk = unsafe extern "C" fn(ctx: *const (), slot: *mut u8);
#[derive(Clone, Debug)]
pub struct DefaultOp {
pub offset: usize,
pub ctx: *const (),
pub default: DefaultThunk,
}
#[derive(Clone, Copy, Debug)]
pub struct SeqThunks {
pub ctx: *const (),
pub from_raw_parts:
unsafe extern "C" fn(ctx: *const (), list: *mut u8, ptr: *mut u8, len: usize, cap: usize),
pub len: unsafe extern "C" fn(ctx: *const (), list: *const u8) -> usize,
pub data: unsafe extern "C" fn(ctx: *const (), list: *const u8) -> *const u8,
}
#[derive(Clone, Copy, Debug)]
pub struct SetThunks {
pub ctx: *const (),
pub len: unsafe extern "C" fn(ctx: *const (), set: *const u8) -> usize,
pub init_with_capacity: unsafe extern "C" fn(ctx: *const (), set: *mut u8, cap: usize),
pub insert: unsafe extern "C" fn(ctx: *const (), set: *mut u8, value: *mut u8) -> bool,
pub iter_init: unsafe extern "C" fn(ctx: *const (), set: *const u8) -> *mut (),
pub iter_next:
unsafe extern "C" fn(ctx: *const (), iter: *mut (), value_out: *mut *const u8) -> bool,
pub iter_dealloc: unsafe extern "C" fn(ctx: *const (), iter: *mut ()),
}
pub type ByteValidator = unsafe extern "C" fn(ptr: *const u8, len: usize) -> bool;
#[derive(Clone, Copy, Debug)]
pub struct BorrowThunks {
pub ctx: *const (),
pub set_borrowed:
unsafe extern "C" fn(ctx: *const (), field: *mut u8, ptr: *const u8, len: usize) -> bool,
pub len: unsafe extern "C" fn(ctx: *const (), field: *const u8) -> usize,
pub data: unsafe extern "C" fn(ctx: *const (), field: *const u8) -> *const u8,
}
#[derive(Clone, Copy, Debug)]
pub struct OptionThunks {
pub ctx: *const (),
pub is_some: unsafe extern "C" fn(ctx: *const (), option: *const u8) -> bool,
pub get_value: unsafe extern "C" fn(ctx: *const (), option: *const u8) -> *const u8,
pub init_some: unsafe extern "C" fn(ctx: *const (), option: *mut u8, value: *mut u8),
pub init_none: unsafe extern "C" fn(ctx: *const (), option: *mut u8),
}
#[derive(Clone, Copy, Debug)]
pub struct MapThunks {
pub ctx: *const (),
pub len: unsafe extern "C" fn(ctx: *const (), map: *const u8) -> usize,
pub init_with_capacity: unsafe extern "C" fn(ctx: *const (), map: *mut u8, cap: usize),
pub insert: unsafe extern "C" fn(ctx: *const (), map: *mut u8, key: *mut u8, value: *mut u8),
pub iter_init: unsafe extern "C" fn(ctx: *const (), map: *const u8) -> *mut (),
pub iter_next: unsafe extern "C" fn(
ctx: *const (),
iter: *mut (),
key_out: *mut *const u8,
value_out: *mut *const u8,
) -> bool,
pub iter_dealloc: unsafe extern "C" fn(ctx: *const (), iter: *mut ()),
}
#[derive(Clone, Copy, Debug)]
pub struct ResultThunks {
pub ctx: *const (),
pub is_ok: unsafe extern "C" fn(ctx: *const (), result: *const u8) -> bool,
pub get_ok: unsafe extern "C" fn(ctx: *const (), result: *const u8) -> *const u8,
pub get_err: unsafe extern "C" fn(ctx: *const (), result: *const u8) -> *const u8,
pub init_ok: unsafe extern "C" fn(ctx: *const (), result: *mut u8, value: *mut u8),
pub init_err: unsafe extern "C" fn(ctx: *const (), result: *mut u8, value: *mut u8),
}
#[derive(Clone, Copy, Debug)]
pub struct PointerThunks {
pub ctx: *const (),
pub borrow: unsafe extern "C" fn(ctx: *const (), pointer: *const u8) -> *const u8,
pub init: unsafe extern "C" fn(ctx: *const (), pointer: *mut u8, value: *mut u8),
pub retain_decode_pointee: bool,
pub drop_pointee: Option<unsafe extern "C" fn(ctx: *const (), value: *mut u8)>,
}
#[derive(Clone, Copy, Debug)]
pub struct OpaqueThunks {
pub ctx: *const (),
pub encode: unsafe extern "C" fn(ctx: *const (), field: *const u8, out: *mut Vec<u8>),
pub decode:
unsafe extern "C" fn(ctx: *const (), bytes: *const u8, len: usize, slot: *mut u8) -> bool,
}
#[derive(Clone, Debug)]
pub struct Descriptor<SchemaRef> {
pub schema: SchemaRef,
pub layout: Layout,
pub access: Access<SchemaRef>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Layout {
pub size: usize,
pub align: usize,
}
#[derive(Clone, Debug)]
pub enum Access<SchemaRef> {
Scalar,
Record(RecordAccess<SchemaRef>),
Enum(EnumAccess<SchemaRef>),
Option(OptionAccess<SchemaRef>),
Array {
element: Box<Descriptor<SchemaRef>>,
count: usize,
stride: usize,
},
Tensor(TensorAccess<SchemaRef>),
Sequence(SequenceAccess<SchemaRef>),
Set(SetAccess<SchemaRef>),
Map(MapAccess<SchemaRef>),
Result(ResultAccess<SchemaRef>),
Pointer(PointerAccess<SchemaRef>),
Dynamic,
Opaque(OpaqueThunks),
Recurse,
}
#[derive(Clone, Debug)]
pub struct RecordAccess<SchemaRef> {
pub fields: Vec<FieldAccess<SchemaRef>>,
pub byte_ownership: RecordByteOwnership,
pub construct: Construct,
}
#[derive(Clone, Debug)]
pub struct FieldAccess<SchemaRef> {
pub offset: usize,
pub descriptor: Descriptor<SchemaRef>,
pub default: Option<FieldDefault>,
}
#[derive(Clone, Copy, Debug)]
pub struct FieldDefault {
pub ctx: *const (),
pub thunk: DefaultThunk,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct RecordByteOwnership {
pub ranges: Vec<ByteRange>,
}
impl RecordByteOwnership {
#[must_use]
pub fn unknown(layout_size: usize) -> Self {
if layout_size == 0 {
Self::default()
} else {
Self {
ranges: vec![ByteRange {
offset: 0,
len: layout_size,
owner: ByteOwner::Unknown,
}],
}
}
}
#[must_use]
pub fn fields_only<SchemaRef>(fields: &[FieldAccess<SchemaRef>]) -> Self {
let Some(fields) = sorted_field_ranges(fields) else {
return Self::default();
};
Self {
ranges: fields
.into_iter()
.map(|field| ByteRange {
offset: field.offset,
len: field.len,
owner: ByteOwner::Field(field.index),
})
.collect(),
}
}
#[must_use]
pub fn from_record_layout<SchemaRef>(
layout: Layout,
fields: &[FieldAccess<SchemaRef>],
) -> Self {
let Some(fields) = sorted_field_ranges(fields) else {
return Self::unknown(layout.size);
};
let Some(last_end) = fields
.last()
.map_or(Some(0), |field| field.offset.checked_add(field.len))
else {
return Self::unknown(layout.size);
};
if last_end > layout.size {
return Self::unknown(layout.size);
}
let mut ranges = Vec::with_capacity(fields.len().saturating_mul(2).saturating_add(1));
let mut cursor = 0usize;
for field in fields {
if cursor < field.offset {
ranges.push(ByteRange {
offset: cursor,
len: field.offset - cursor,
owner: ByteOwner::Padding,
});
}
if field.len != 0 {
ranges.push(ByteRange {
offset: field.offset,
len: field.len,
owner: ByteOwner::Field(field.index),
});
}
cursor = field.offset + field.len;
}
if cursor < layout.size {
ranges.push(ByteRange {
offset: cursor,
len: layout.size - cursor,
owner: ByteOwner::Padding,
});
}
Self { ranges }
}
#[must_use]
pub fn is_padding_range(&self, offset: usize, len: usize) -> bool {
if len == 0 {
return true;
}
let Some(end) = offset.checked_add(len) else {
return false;
};
let mut cursor = offset;
for range in &self.ranges {
let Some(range_end) = range.offset.checked_add(range.len) else {
return false;
};
if range_end <= cursor {
continue;
}
if range.offset > cursor {
return false;
}
if range.owner != ByteOwner::Padding {
return false;
}
cursor = range_end.min(end);
if cursor == end {
return true;
}
}
false
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ByteRange {
pub offset: usize,
pub len: usize,
pub owner: ByteOwner,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ByteOwner {
Field(usize),
Padding,
Unknown,
}
#[derive(Clone, Copy)]
struct FieldByteRange {
index: usize,
offset: usize,
len: usize,
}
fn sorted_field_ranges<SchemaRef>(
fields: &[FieldAccess<SchemaRef>],
) -> Option<Vec<FieldByteRange>> {
let mut ranges = Vec::with_capacity(fields.len());
for (index, field) in fields.iter().enumerate() {
let len = field.descriptor.layout.size;
let end = field.offset.checked_add(len)?;
if len != 0 {
ranges.push(FieldByteRange {
index,
offset: field.offset,
len,
});
} else if end < field.offset {
return None;
}
}
ranges.sort_by_key(|range| (range.offset, range.index));
let mut prev_end = 0usize;
for range in &ranges {
if range.offset < prev_end {
return None;
}
prev_end = range.offset.checked_add(range.len)?;
}
Some(ranges)
}
#[derive(Clone, Debug)]
pub enum Construct {
InPlace,
Thunk(Thunk),
}
#[derive(Clone, Debug)]
pub struct EnumAccess<SchemaRef> {
pub tag: Tag,
pub variants: Vec<VariantAccess<SchemaRef>>,
}
#[derive(Clone, Debug)]
pub enum Tag {
Direct { offset: usize, width: usize },
Niche { offset: usize, width: usize },
Thunk { read: Thunk, write: Thunk },
}
#[derive(Clone, Debug)]
pub struct VariantAccess<SchemaRef> {
pub index: u32,
pub selector: u64,
pub payload: RecordAccess<SchemaRef>,
}
#[derive(Clone, Debug)]
pub struct OptionAccess<SchemaRef> {
pub presence: Presence,
pub some: Box<Descriptor<SchemaRef>>,
}
#[derive(Clone, Debug)]
pub enum Presence {
Tag {
offset: usize,
width: usize,
none_value: u64,
},
Niche {
offset: usize,
width: usize,
none_pattern: Vec<u8>,
},
Thunk {
is_some: Thunk,
set_none: Thunk,
set_some: Thunk,
},
Vtable(OptionThunks),
}
#[derive(Clone, Debug)]
pub struct SequenceAccess<SchemaRef> {
pub element: Box<Descriptor<SchemaRef>>,
pub storage: SequenceStorage,
}
#[derive(Clone, Debug)]
pub struct SetAccess<SchemaRef> {
pub element: Box<Descriptor<SchemaRef>>,
pub storage: SetStorage,
}
#[derive(Clone, Debug)]
pub enum SetStorage {
Vtable(SetThunks),
}
#[derive(Clone, Debug)]
pub enum SequenceStorage {
Owned {
ptr_offset: usize,
len_offset: usize,
cap_offset: Option<usize>,
allocate: Thunk,
},
Borrowed {
ptr_offset: usize,
len_offset: usize,
},
Thunk { len: Thunk, get: Thunk, push: Thunk },
Vtable(SeqThunks),
BorrowedVtable(BorrowThunks),
}
#[derive(Clone, Debug)]
pub struct ResultAccess<SchemaRef> {
pub ok: Box<Descriptor<SchemaRef>>,
pub err: Box<Descriptor<SchemaRef>>,
pub thunks: ResultThunks,
}
#[derive(Clone, Debug)]
pub struct PointerAccess<SchemaRef> {
pub pointee: Box<Descriptor<SchemaRef>>,
pub thunks: PointerThunks,
}
#[derive(Clone, Debug)]
pub struct MapAccess<SchemaRef> {
pub key: Box<Descriptor<SchemaRef>>,
pub value: Box<Descriptor<SchemaRef>>,
pub storage: MapStorage,
}
#[derive(Clone, Debug)]
pub enum MapStorage {
Thunk {
len: Thunk,
iterate: Thunk,
insert: Thunk,
},
Vtable(MapThunks),
}
#[derive(Clone, Debug)]
pub struct TensorAccess<SchemaRef> {
pub element: Box<Descriptor<SchemaRef>>,
pub shape: Thunk,
pub data: SequenceStorage,
pub reshape: Thunk,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Thunk {
pub name: String,
}
pub type MemProgram<BlockId> = crate::Program<MemOp<BlockId>>;
#[derive(Clone, Debug)]
pub enum MemOp<BlockId> {
Scalar {
offset: usize,
size: usize,
align: usize,
},
ScalarRun(Box<ScalarRunOp>),
NativeInt {
offset: usize,
mem_size: usize,
signed: bool,
},
Sequence(Box<SeqOp<BlockId>>),
Set(Box<SetOp<BlockId>>),
Bytes(Box<BytesOp>),
Borrow(Box<BorrowOp>),
Option(Box<OptionOp<BlockId>>),
Enum(Box<EnumOp<BlockId>>),
Map(Box<MapOp<BlockId>>),
Dynamic { field_offset: usize },
Result(Box<ResultOp<BlockId>>),
Pointer(Box<PointerOp<BlockId>>),
SkipWire(Box<SkipOp>),
Default(Box<DefaultOp>),
Opaque(Box<OpaqueOp>),
CallBlock { schema: BlockId, offset: usize },
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ScalarSegment {
pub offset: usize,
pub size: usize,
pub align: usize,
}
impl ScalarSegment {
fn end(self) -> Option<usize> {
self.offset.checked_add(self.size)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ScalarRunOp {
pub segments: Vec<ScalarSegment>,
}
#[derive(Clone, Debug)]
pub enum SkipOp {
Scalar { size: usize, align: usize },
Bytes { stride: usize, elem_align: usize },
Seq(Box<SkipOp>),
Option(Box<SkipOp>),
Enum(Vec<(u32, Vec<SkipOp>)>),
Map(Box<SkipOp>, Box<SkipOp>),
Struct(Vec<SkipOp>),
Dynamic,
}
#[derive(Clone, Debug)]
pub struct SeqOp<BlockId> {
pub field_offset: usize,
pub element: MemProgram<BlockId>,
pub stride: usize,
pub elem_align: usize,
pub min_wire: usize,
pub thunks: SeqThunks,
}
#[derive(Clone, Debug)]
pub struct SetOp<BlockId> {
pub field_offset: usize,
pub element: MemProgram<BlockId>,
pub elem_size: usize,
pub elem_align: usize,
pub min_wire: usize,
pub thunks: SetThunks,
}
#[derive(Clone, Debug)]
pub struct BytesOp {
pub field_offset: usize,
pub stride: usize,
pub elem_align: usize,
pub validate: ByteValidator,
pub thunks: SeqThunks,
}
#[derive(Clone, Debug)]
pub struct BorrowOp {
pub field_offset: usize,
pub stride: usize,
pub elem_align: usize,
pub thunks: BorrowThunks,
}
#[derive(Clone, Debug)]
pub struct OptionOp<BlockId> {
pub field_offset: usize,
pub some: MemProgram<BlockId>,
pub inner_size: usize,
pub inner_align: usize,
pub thunks: OptionThunks,
}
#[derive(Clone, Debug)]
pub struct EnumOp<BlockId> {
pub tag_offset: usize,
pub tag_width: usize,
pub variants: Vec<EnumVariantOp<BlockId>>,
pub writer_only: Vec<u32>,
}
#[derive(Clone, Debug)]
pub struct EnumVariantOp<BlockId> {
pub wire_index: u32,
pub selector: u64,
pub payload: MemProgram<BlockId>,
}
#[derive(Clone, Debug)]
pub struct MapOp<BlockId> {
pub field_offset: usize,
pub key: MemProgram<BlockId>,
pub value: MemProgram<BlockId>,
pub key_size: usize,
pub key_align: usize,
pub value_size: usize,
pub value_align: usize,
pub thunks: MapThunks,
}
#[derive(Clone, Debug)]
pub struct ResultOp<BlockId> {
pub field_offset: usize,
pub ok: MemProgram<BlockId>,
pub ok_size: usize,
pub ok_align: usize,
pub ok_wire_index: u32,
pub err: MemProgram<BlockId>,
pub err_size: usize,
pub err_align: usize,
pub err_wire_index: u32,
pub thunks: ResultThunks,
}
#[derive(Clone, Debug)]
pub struct PointerOp<BlockId> {
pub field_offset: usize,
pub pointee: MemProgram<BlockId>,
pub pointee_size: usize,
pub pointee_align: usize,
pub thunks: PointerThunks,
}
#[derive(Clone, Debug)]
pub struct OpaqueOp {
pub field_offset: usize,
pub thunks: OpaqueThunks,
}
pub type CanonicalMemProgram<BlockId> = WeavyProgram<BlockId, MemIntrinsic<BlockId>>;
pub type CanonicalMemLowered<BlockId> = WeavyLowered<BlockId, MemIntrinsic<BlockId>>;
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct CanonicalSeqOp<BlockId> {
pub field_offset: usize,
pub element: CanonicalMemProgram<BlockId>,
pub stride: usize,
pub elem_align: usize,
pub min_wire: usize,
pub thunks: SeqThunks,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct CanonicalSetOp<BlockId> {
pub field_offset: usize,
pub element: CanonicalMemProgram<BlockId>,
pub elem_size: usize,
pub elem_align: usize,
pub min_wire: usize,
pub thunks: SetThunks,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct CanonicalOptionOp<BlockId> {
pub field_offset: usize,
pub some: CanonicalMemProgram<BlockId>,
pub inner_size: usize,
pub inner_align: usize,
pub thunks: OptionThunks,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct CanonicalEnumOp<BlockId> {
pub tag_offset: usize,
pub tag_width: usize,
pub variants: Vec<CanonicalEnumVariantOp<BlockId>>,
pub writer_only: Vec<u32>,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct CanonicalEnumVariantOp<BlockId> {
pub wire_index: u32,
pub selector: u64,
pub payload: CanonicalMemProgram<BlockId>,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct CanonicalMapOp<BlockId> {
pub field_offset: usize,
pub key: CanonicalMemProgram<BlockId>,
pub value: CanonicalMemProgram<BlockId>,
pub key_size: usize,
pub key_align: usize,
pub value_size: usize,
pub value_align: usize,
pub thunks: MapThunks,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct CanonicalResultOp<BlockId> {
pub field_offset: usize,
pub ok: CanonicalMemProgram<BlockId>,
pub ok_size: usize,
pub ok_align: usize,
pub ok_wire_index: u32,
pub err: CanonicalMemProgram<BlockId>,
pub err_size: usize,
pub err_align: usize,
pub err_wire_index: u32,
pub thunks: ResultThunks,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct CanonicalPointerOp<BlockId> {
pub field_offset: usize,
pub pointee: CanonicalMemProgram<BlockId>,
pub pointee_size: usize,
pub pointee_align: usize,
pub thunks: PointerThunks,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum MemIntrinsic<BlockId> {
NativeInt {
offset: usize,
mem_size: usize,
signed: bool,
},
Sequence(Box<CanonicalSeqOp<BlockId>>),
Set(Box<CanonicalSetOp<BlockId>>),
Bytes(Box<BytesOp>),
Borrow(Box<BorrowOp>),
Option(Box<CanonicalOptionOp<BlockId>>),
Enum(Box<CanonicalEnumOp<BlockId>>),
Map(Box<CanonicalMapOp<BlockId>>),
Dynamic {
field_offset: usize,
},
Result(Box<CanonicalResultOp<BlockId>>),
Pointer(Box<CanonicalPointerOp<BlockId>>),
SkipWire(Box<SkipOp>),
Default(Box<DefaultOp>),
Opaque(Box<OpaqueOp>),
}
impl<BlockId> IntrinsicOp for MemIntrinsic<BlockId> {
fn descriptor(&self) -> IntrinsicDescriptor {
let name = match self {
MemIntrinsic::NativeInt { .. } => "native_int",
MemIntrinsic::Sequence(_) => "sequence",
MemIntrinsic::Set(_) => "set",
MemIntrinsic::Bytes(_) => "bytes",
MemIntrinsic::Borrow(_) => "borrow",
MemIntrinsic::Option(_) => "option",
MemIntrinsic::Enum(_) => "enum",
MemIntrinsic::Map(_) => "map",
MemIntrinsic::Dynamic { .. } => "dynamic",
MemIntrinsic::Result(_) => "result",
MemIntrinsic::Pointer(_) => "pointer",
MemIntrinsic::SkipWire(_) => "skip_wire",
MemIntrinsic::Default(_) => "default",
MemIntrinsic::Opaque(_) => "opaque",
};
IntrinsicDescriptor {
dialect: "weavy.mem",
name,
}
}
fn effect(&self) -> EffectContract {
match self {
MemIntrinsic::NativeInt {
offset, mem_size, ..
} => stream_memory_effect(*offset, *mem_size),
MemIntrinsic::Sequence(op) => owned_container_effect(op.field_offset),
MemIntrinsic::Set(op) => owned_container_effect(op.field_offset),
MemIntrinsic::Bytes(op) => owned_container_effect(op.field_offset),
MemIntrinsic::Borrow(op) => borrowed_run_effect(op.field_offset),
MemIntrinsic::Option(op) => thunked_handle_effect(op.field_offset)
.typed_memory(
MemoryRegion::unknown_offset(op.inner_size),
TypedMemoryAccess::MoveFrom,
)
.may_fail(),
MemIntrinsic::Enum(op) => stream_memory_effect(op.tag_offset, op.tag_width).barrier(),
MemIntrinsic::Map(op) => owned_container_effect(op.field_offset)
.typed_memory(
MemoryRegion::unknown_offset(op.key_size),
TypedMemoryAccess::MoveFrom,
)
.typed_memory(
MemoryRegion::unknown_offset(op.value_size),
TypedMemoryAccess::MoveFrom,
),
MemIntrinsic::Dynamic { field_offset } => EffectContract::opaque()
.read_resource(EffectResource::Input("wire"))
.advance_resource(EffectResource::Input("wire"))
.write_resource(EffectResource::Sink("wire"))
.typed_memory(
MemoryRegion::base_relative_unknown_size(*field_offset),
TypedMemoryAccess::Read,
)
.typed_memory(
MemoryRegion::base_relative_unknown_size(*field_offset),
TypedMemoryAccess::Initialize,
)
.may_fail()
.may_allocate()
.calls_user_code(),
MemIntrinsic::Result(op) => thunked_handle_effect(op.field_offset)
.typed_memory(
MemoryRegion::unknown_offset(op.ok_size),
TypedMemoryAccess::MoveFrom,
)
.typed_memory(
MemoryRegion::unknown_offset(op.err_size),
TypedMemoryAccess::MoveFrom,
)
.may_fail(),
MemIntrinsic::Pointer(op) => thunked_handle_effect(op.field_offset)
.typed_memory(
MemoryRegion::unknown_offset(op.pointee_size),
TypedMemoryAccess::MoveFrom,
)
.may_allocate()
.may_fail(),
MemIntrinsic::SkipWire(_) => EffectContract::new()
.read_resource(EffectResource::Input("wire"))
.advance_resource(EffectResource::Input("wire"))
.may_fail()
.ordered(),
MemIntrinsic::Default(op) => EffectContract::new()
.typed_memory(
MemoryRegion::base_relative_unknown_size(op.offset),
TypedMemoryAccess::Initialize,
)
.calls_user_code(),
MemIntrinsic::Opaque(op) => EffectContract::opaque()
.read_resource(EffectResource::Input("wire"))
.advance_resource(EffectResource::Input("wire"))
.write_resource(EffectResource::Sink("wire"))
.typed_memory(
MemoryRegion::base_relative_unknown_size(op.field_offset),
TypedMemoryAccess::Read,
)
.typed_memory(
MemoryRegion::base_relative_unknown_size(op.field_offset),
TypedMemoryAccess::Initialize,
)
.may_fail()
.may_allocate()
.calls_user_code(),
}
}
}
fn stream_memory_effect(offset: usize, size: usize) -> EffectContract {
EffectContract::new()
.read_resource(EffectResource::Input("wire"))
.advance_resource(EffectResource::Input("wire"))
.write_resource(EffectResource::Sink("wire"))
.typed_memory(
MemoryRegion::base_relative(offset, size),
TypedMemoryAccess::Read,
)
.typed_memory(
MemoryRegion::base_relative(offset, size),
TypedMemoryAccess::Initialize,
)
.may_fail()
.ordered()
}
fn thunked_handle_effect(field_offset: usize) -> EffectContract {
EffectContract::new()
.read_resource(EffectResource::Input("wire"))
.advance_resource(EffectResource::Input("wire"))
.write_resource(EffectResource::Sink("wire"))
.typed_memory(
MemoryRegion::base_relative_unknown_size(field_offset),
TypedMemoryAccess::Read,
)
.typed_memory(
MemoryRegion::base_relative_unknown_size(field_offset),
TypedMemoryAccess::Initialize,
)
.may_fail()
.calls_user_code()
}
fn owned_container_effect(field_offset: usize) -> EffectContract {
thunked_handle_effect(field_offset).may_allocate()
}
fn borrowed_run_effect(field_offset: usize) -> EffectContract {
EffectContract::new()
.read_resource(EffectResource::Input("wire"))
.advance_resource(EffectResource::Input("wire"))
.write_resource(EffectResource::Sink("wire"))
.typed_memory(
MemoryRegion::base_relative_unknown_size(field_offset),
TypedMemoryAccess::Read,
)
.typed_memory(
MemoryRegion::base_relative_unknown_size(field_offset),
TypedMemoryAccess::Initialize,
)
.may_fail()
.calls_user_code()
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum CanonicalMemError {
Return,
Zero,
Move,
Drop,
Init,
Aggregate,
}
impl core::fmt::Display for CanonicalMemError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
CanonicalMemError::Return => write!(f, "canonical return has no MemOp equivalent"),
CanonicalMemError::Zero => write!(f, "canonical zero has no MemOp equivalent"),
CanonicalMemError::Move => write!(f, "canonical move has no MemOp equivalent"),
CanonicalMemError::Drop => write!(f, "canonical drop has no MemOp equivalent"),
CanonicalMemError::Init => write!(f, "canonical init has no MemOp equivalent"),
CanonicalMemError::Aggregate => {
write!(f, "canonical aggregate has no MemOp equivalent")
}
}
}
}
impl std::error::Error for CanonicalMemError {}
#[must_use]
pub fn canonical_mem_program<BlockId>(
program: MemProgram<BlockId>,
) -> CanonicalMemProgram<BlockId> {
program.into_iter().map(canonical_mem_op).collect()
}
#[must_use]
pub fn canonical_mem_lowered<BlockId>(
lowered: crate::Lowered<BlockId, MemOp<BlockId>>,
) -> CanonicalMemLowered<BlockId>
where
BlockId: Ord,
{
let program = canonical_mem_program(lowered.program);
let blocks = lowered
.blocks
.into_iter()
.map(|(id, block)| (id, canonical_mem_program(block)))
.collect();
crate::Lowered { program, blocks }
}
pub fn mem_program_from_canonical<BlockId>(
program: CanonicalMemProgram<BlockId>,
) -> Result<MemProgram<BlockId>, CanonicalMemError> {
program.into_iter().map(mem_op_from_canonical).collect()
}
pub fn mem_lowered_from_canonical<BlockId>(
lowered: CanonicalMemLowered<BlockId>,
) -> Result<crate::Lowered<BlockId, MemOp<BlockId>>, CanonicalMemError>
where
BlockId: Ord,
{
let program = mem_program_from_canonical(lowered.program)?;
let blocks = lowered
.blocks
.into_iter()
.map(|(id, block)| Ok((id, mem_program_from_canonical(block)?)))
.collect::<Result<_, CanonicalMemError>>()?;
Ok(crate::Lowered { program, blocks })
}
#[must_use]
pub fn canonical_mem_program_stats<BlockId>(
program: &[WeavyOp<BlockId, MemIntrinsic<BlockId>>],
) -> crate::ir::ProgramStats {
let mut stats = crate::ir::program_stats(program);
for op in program {
if let WeavyOp::Intrinsic(intrinsic) = op {
add_canonical_mem_intrinsic_stats(intrinsic, &mut stats);
}
}
stats
}
#[must_use]
pub fn canonical_mem_lowered_stats<BlockId>(
lowered: &CanonicalMemLowered<BlockId>,
) -> crate::ir::LoweredProgramStats
where
BlockId: Ord,
{
let root = canonical_mem_program_stats(&lowered.program);
let mut blocks = crate::ir::ProgramStats::default();
for block in lowered.blocks.values() {
blocks.accumulate(canonical_mem_program_stats(block));
}
let mut total = root;
total.accumulate(blocks);
crate::ir::LoweredProgramStats {
root,
blocks,
total,
block_count: lowered.blocks.len(),
}
}
#[must_use]
pub fn canonical_mem_intrinsic_counts<BlockId>(
program: &[WeavyOp<BlockId, MemIntrinsic<BlockId>>],
) -> BTreeMap<IntrinsicDescriptor, usize> {
let mut counts = crate::ir::intrinsic_counts(program);
for op in program {
if let WeavyOp::Intrinsic(intrinsic) = op {
add_canonical_mem_intrinsic_counts(intrinsic, &mut counts);
}
}
counts
}
#[must_use]
pub fn canonical_mem_lowered_intrinsic_counts<BlockId>(
lowered: &CanonicalMemLowered<BlockId>,
) -> BTreeMap<IntrinsicDescriptor, usize>
where
BlockId: Ord,
{
let mut counts = canonical_mem_intrinsic_counts(&lowered.program);
for block in lowered.blocks.values() {
for (descriptor, count) in canonical_mem_intrinsic_counts(block) {
*counts.entry(descriptor).or_default() += count;
}
}
counts
}
#[must_use]
pub fn canonical_mem_program_effect_stats<BlockId>(
program: &[WeavyOp<BlockId, MemIntrinsic<BlockId>>],
) -> crate::ir::EffectStats {
let mut stats = crate::ir::effect_stats(program);
for op in program {
if let WeavyOp::Intrinsic(intrinsic) = op {
add_canonical_mem_intrinsic_effect_stats(intrinsic, &mut stats);
}
}
stats
}
#[must_use]
pub fn canonical_mem_lowered_effect_stats<BlockId>(
lowered: &CanonicalMemLowered<BlockId>,
) -> crate::ir::LoweredEffectStats
where
BlockId: Ord,
{
let root = canonical_mem_program_effect_stats(&lowered.program);
let mut blocks = crate::ir::EffectStats::default();
for block in lowered.blocks.values() {
blocks.accumulate(canonical_mem_program_effect_stats(block));
}
let mut total = root;
total.accumulate(blocks);
crate::ir::LoweredEffectStats {
root,
blocks,
total,
block_count: lowered.blocks.len(),
}
}
fn canonical_mem_op<BlockId>(op: MemOp<BlockId>) -> WeavyOp<BlockId, MemIntrinsic<BlockId>> {
match op {
MemOp::Scalar {
offset,
size,
align,
} => WeavyOp::Memory(MemoryOp::ScalarCopy {
offset,
size,
align,
}),
MemOp::ScalarRun(run) => WeavyOp::Memory(MemoryOp::ScalarRun {
segments: run.segments,
}),
MemOp::CallBlock { schema, offset } => WeavyOp::Control(ControlOp::CallBlock {
block: schema,
base_offset: offset,
}),
MemOp::NativeInt {
offset,
mem_size,
signed,
} => WeavyOp::Intrinsic(MemIntrinsic::NativeInt {
offset,
mem_size,
signed,
}),
MemOp::Sequence(op) => {
WeavyOp::Intrinsic(MemIntrinsic::Sequence(Box::new(CanonicalSeqOp {
field_offset: op.field_offset,
element: canonical_mem_program(op.element),
stride: op.stride,
elem_align: op.elem_align,
min_wire: op.min_wire,
thunks: op.thunks,
})))
}
MemOp::Set(op) => WeavyOp::Intrinsic(MemIntrinsic::Set(Box::new(CanonicalSetOp {
field_offset: op.field_offset,
element: canonical_mem_program(op.element),
elem_size: op.elem_size,
elem_align: op.elem_align,
min_wire: op.min_wire,
thunks: op.thunks,
}))),
MemOp::Bytes(op) => WeavyOp::Intrinsic(MemIntrinsic::Bytes(op)),
MemOp::Borrow(op) => WeavyOp::Intrinsic(MemIntrinsic::Borrow(op)),
MemOp::Option(op) => {
WeavyOp::Intrinsic(MemIntrinsic::Option(Box::new(CanonicalOptionOp {
field_offset: op.field_offset,
some: canonical_mem_program(op.some),
inner_size: op.inner_size,
inner_align: op.inner_align,
thunks: op.thunks,
})))
}
MemOp::Enum(op) => WeavyOp::Intrinsic(MemIntrinsic::Enum(Box::new(CanonicalEnumOp {
tag_offset: op.tag_offset,
tag_width: op.tag_width,
variants: op
.variants
.into_iter()
.map(|variant| CanonicalEnumVariantOp {
wire_index: variant.wire_index,
selector: variant.selector,
payload: canonical_mem_program(variant.payload),
})
.collect(),
writer_only: op.writer_only,
}))),
MemOp::Map(op) => WeavyOp::Intrinsic(MemIntrinsic::Map(Box::new(CanonicalMapOp {
field_offset: op.field_offset,
key: canonical_mem_program(op.key),
value: canonical_mem_program(op.value),
key_size: op.key_size,
key_align: op.key_align,
value_size: op.value_size,
value_align: op.value_align,
thunks: op.thunks,
}))),
MemOp::Dynamic { field_offset } => {
WeavyOp::Intrinsic(MemIntrinsic::Dynamic { field_offset })
}
MemOp::Result(op) => {
WeavyOp::Intrinsic(MemIntrinsic::Result(Box::new(CanonicalResultOp {
field_offset: op.field_offset,
ok: canonical_mem_program(op.ok),
ok_size: op.ok_size,
ok_align: op.ok_align,
ok_wire_index: op.ok_wire_index,
err: canonical_mem_program(op.err),
err_size: op.err_size,
err_align: op.err_align,
err_wire_index: op.err_wire_index,
thunks: op.thunks,
})))
}
MemOp::Pointer(op) => {
WeavyOp::Intrinsic(MemIntrinsic::Pointer(Box::new(CanonicalPointerOp {
field_offset: op.field_offset,
pointee: canonical_mem_program(op.pointee),
pointee_size: op.pointee_size,
pointee_align: op.pointee_align,
thunks: op.thunks,
})))
}
MemOp::SkipWire(op) => WeavyOp::Intrinsic(MemIntrinsic::SkipWire(op)),
MemOp::Default(op) => WeavyOp::Intrinsic(MemIntrinsic::Default(op)),
MemOp::Opaque(op) => WeavyOp::Intrinsic(MemIntrinsic::Opaque(op)),
}
}
fn mem_op_from_canonical<BlockId>(
op: WeavyOp<BlockId, MemIntrinsic<BlockId>>,
) -> Result<MemOp<BlockId>, CanonicalMemError> {
Ok(match op {
WeavyOp::Control(ControlOp::CallBlock { block, base_offset }) => MemOp::CallBlock {
schema: block,
offset: base_offset,
},
WeavyOp::Control(ControlOp::Return) => return Err(CanonicalMemError::Return),
WeavyOp::Memory(MemoryOp::ScalarCopy {
offset,
size,
align,
}) => MemOp::Scalar {
offset,
size,
align,
},
WeavyOp::Memory(MemoryOp::ScalarRun { segments }) => {
MemOp::ScalarRun(Box::new(ScalarRunOp { segments }))
}
WeavyOp::Memory(MemoryOp::Zero { .. }) => return Err(CanonicalMemError::Zero),
WeavyOp::Memory(MemoryOp::Move { .. }) => return Err(CanonicalMemError::Move),
WeavyOp::Memory(MemoryOp::Drop { .. }) => return Err(CanonicalMemError::Drop),
WeavyOp::Init(_) => return Err(CanonicalMemError::Init),
WeavyOp::Aggregate(_) => return Err(CanonicalMemError::Aggregate),
WeavyOp::Intrinsic(intrinsic) => return mem_op_from_intrinsic(intrinsic),
})
}
fn mem_op_from_intrinsic<BlockId>(
intrinsic: MemIntrinsic<BlockId>,
) -> Result<MemOp<BlockId>, CanonicalMemError> {
Ok(match intrinsic {
MemIntrinsic::NativeInt {
offset,
mem_size,
signed,
} => MemOp::NativeInt {
offset,
mem_size,
signed,
},
MemIntrinsic::Sequence(op) => MemOp::Sequence(Box::new(SeqOp {
field_offset: op.field_offset,
element: mem_program_from_canonical(op.element)?,
stride: op.stride,
elem_align: op.elem_align,
min_wire: op.min_wire,
thunks: op.thunks,
})),
MemIntrinsic::Set(op) => MemOp::Set(Box::new(SetOp {
field_offset: op.field_offset,
element: mem_program_from_canonical(op.element)?,
elem_size: op.elem_size,
elem_align: op.elem_align,
min_wire: op.min_wire,
thunks: op.thunks,
})),
MemIntrinsic::Bytes(op) => MemOp::Bytes(op),
MemIntrinsic::Borrow(op) => MemOp::Borrow(op),
MemIntrinsic::Option(op) => MemOp::Option(Box::new(OptionOp {
field_offset: op.field_offset,
some: mem_program_from_canonical(op.some)?,
inner_size: op.inner_size,
inner_align: op.inner_align,
thunks: op.thunks,
})),
MemIntrinsic::Enum(op) => MemOp::Enum(Box::new(EnumOp {
tag_offset: op.tag_offset,
tag_width: op.tag_width,
variants: op
.variants
.into_iter()
.map(|variant| {
Ok(EnumVariantOp {
wire_index: variant.wire_index,
selector: variant.selector,
payload: mem_program_from_canonical(variant.payload)?,
})
})
.collect::<Result<_, CanonicalMemError>>()?,
writer_only: op.writer_only,
})),
MemIntrinsic::Map(op) => MemOp::Map(Box::new(MapOp {
field_offset: op.field_offset,
key: mem_program_from_canonical(op.key)?,
value: mem_program_from_canonical(op.value)?,
key_size: op.key_size,
key_align: op.key_align,
value_size: op.value_size,
value_align: op.value_align,
thunks: op.thunks,
})),
MemIntrinsic::Dynamic { field_offset } => MemOp::Dynamic { field_offset },
MemIntrinsic::Result(op) => MemOp::Result(Box::new(ResultOp {
field_offset: op.field_offset,
ok: mem_program_from_canonical(op.ok)?,
ok_size: op.ok_size,
ok_align: op.ok_align,
ok_wire_index: op.ok_wire_index,
err: mem_program_from_canonical(op.err)?,
err_size: op.err_size,
err_align: op.err_align,
err_wire_index: op.err_wire_index,
thunks: op.thunks,
})),
MemIntrinsic::Pointer(op) => MemOp::Pointer(Box::new(PointerOp {
field_offset: op.field_offset,
pointee: mem_program_from_canonical(op.pointee)?,
pointee_size: op.pointee_size,
pointee_align: op.pointee_align,
thunks: op.thunks,
})),
MemIntrinsic::SkipWire(op) => MemOp::SkipWire(op),
MemIntrinsic::Default(op) => MemOp::Default(op),
MemIntrinsic::Opaque(op) => MemOp::Opaque(op),
})
}
fn add_canonical_mem_intrinsic_stats<BlockId>(
intrinsic: &MemIntrinsic<BlockId>,
stats: &mut crate::ir::ProgramStats,
) {
match intrinsic {
MemIntrinsic::Sequence(op) => stats.accumulate(canonical_mem_program_stats(&op.element)),
MemIntrinsic::Set(op) => stats.accumulate(canonical_mem_program_stats(&op.element)),
MemIntrinsic::Option(op) => stats.accumulate(canonical_mem_program_stats(&op.some)),
MemIntrinsic::Enum(op) => {
for variant in &op.variants {
stats.accumulate(canonical_mem_program_stats(&variant.payload));
}
}
MemIntrinsic::Map(op) => {
stats.accumulate(canonical_mem_program_stats(&op.key));
stats.accumulate(canonical_mem_program_stats(&op.value));
}
MemIntrinsic::Result(op) => {
stats.accumulate(canonical_mem_program_stats(&op.ok));
stats.accumulate(canonical_mem_program_stats(&op.err));
}
MemIntrinsic::Pointer(op) => stats.accumulate(canonical_mem_program_stats(&op.pointee)),
MemIntrinsic::NativeInt { .. }
| MemIntrinsic::Bytes(_)
| MemIntrinsic::Borrow(_)
| MemIntrinsic::Dynamic { .. }
| MemIntrinsic::SkipWire(_)
| MemIntrinsic::Default(_)
| MemIntrinsic::Opaque(_) => {}
}
}
fn add_canonical_mem_intrinsic_counts<BlockId>(
intrinsic: &MemIntrinsic<BlockId>,
counts: &mut BTreeMap<IntrinsicDescriptor, usize>,
) {
match intrinsic {
MemIntrinsic::Sequence(op) => {
add_canonical_mem_program_intrinsic_counts(&op.element, counts)
}
MemIntrinsic::Set(op) => add_canonical_mem_program_intrinsic_counts(&op.element, counts),
MemIntrinsic::Option(op) => add_canonical_mem_program_intrinsic_counts(&op.some, counts),
MemIntrinsic::Enum(op) => {
for variant in &op.variants {
add_canonical_mem_program_intrinsic_counts(&variant.payload, counts);
}
}
MemIntrinsic::Map(op) => {
add_canonical_mem_program_intrinsic_counts(&op.key, counts);
add_canonical_mem_program_intrinsic_counts(&op.value, counts);
}
MemIntrinsic::Result(op) => {
add_canonical_mem_program_intrinsic_counts(&op.ok, counts);
add_canonical_mem_program_intrinsic_counts(&op.err, counts);
}
MemIntrinsic::Pointer(op) => {
add_canonical_mem_program_intrinsic_counts(&op.pointee, counts)
}
MemIntrinsic::NativeInt { .. }
| MemIntrinsic::Bytes(_)
| MemIntrinsic::Borrow(_)
| MemIntrinsic::Dynamic { .. }
| MemIntrinsic::SkipWire(_)
| MemIntrinsic::Default(_)
| MemIntrinsic::Opaque(_) => {}
}
}
fn add_canonical_mem_intrinsic_effect_stats<BlockId>(
intrinsic: &MemIntrinsic<BlockId>,
stats: &mut crate::ir::EffectStats,
) {
match intrinsic {
MemIntrinsic::Sequence(op) => {
stats.accumulate(canonical_mem_program_effect_stats(&op.element))
}
MemIntrinsic::Set(op) => stats.accumulate(canonical_mem_program_effect_stats(&op.element)),
MemIntrinsic::Option(op) => stats.accumulate(canonical_mem_program_effect_stats(&op.some)),
MemIntrinsic::Enum(op) => {
for variant in &op.variants {
stats.accumulate(canonical_mem_program_effect_stats(&variant.payload));
}
}
MemIntrinsic::Map(op) => {
stats.accumulate(canonical_mem_program_effect_stats(&op.key));
stats.accumulate(canonical_mem_program_effect_stats(&op.value));
}
MemIntrinsic::Result(op) => {
stats.accumulate(canonical_mem_program_effect_stats(&op.ok));
stats.accumulate(canonical_mem_program_effect_stats(&op.err));
}
MemIntrinsic::Pointer(op) => {
stats.accumulate(canonical_mem_program_effect_stats(&op.pointee))
}
MemIntrinsic::NativeInt { .. }
| MemIntrinsic::Bytes(_)
| MemIntrinsic::Borrow(_)
| MemIntrinsic::Dynamic { .. }
| MemIntrinsic::SkipWire(_)
| MemIntrinsic::Default(_)
| MemIntrinsic::Opaque(_) => {}
}
}
fn add_canonical_mem_program_intrinsic_counts<BlockId>(
program: &[WeavyOp<BlockId, MemIntrinsic<BlockId>>],
counts: &mut BTreeMap<IntrinsicDescriptor, usize>,
) {
for (descriptor, count) in canonical_mem_intrinsic_counts(program) {
*counts.entry(descriptor).or_default() += count;
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LoweringError {
ArrayBulkCopySizeOverflow,
ArrayElementOffsetOverflow,
}
#[must_use]
pub fn element_min_wire<BlockId>(element: &[MemOp<BlockId>]) -> usize {
let zero_sized = element.iter().all(|op| match op {
MemOp::Scalar { size: 0, .. } => true,
MemOp::ScalarRun(run) => run.segments.iter().all(|segment| segment.size == 0),
_ => false,
});
usize::from(!zero_sized)
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct MemProgramStats {
pub op_count: usize,
pub scalar_op_count: usize,
pub scalar_run_count: usize,
pub scalar_run_segment_count: usize,
pub native_int_count: usize,
pub sequence_count: usize,
pub set_count: usize,
pub bytes_count: usize,
pub borrow_count: usize,
pub option_count: usize,
pub enum_count: usize,
pub enum_variant_count: usize,
pub map_count: usize,
pub dynamic_count: usize,
pub result_count: usize,
pub pointer_count: usize,
pub skip_wire_count: usize,
pub default_count: usize,
pub opaque_count: usize,
pub call_block_count: usize,
}
impl MemProgramStats {
pub fn accumulate(&mut self, other: Self) {
self.op_count += other.op_count;
self.scalar_op_count += other.scalar_op_count;
self.scalar_run_count += other.scalar_run_count;
self.scalar_run_segment_count += other.scalar_run_segment_count;
self.native_int_count += other.native_int_count;
self.sequence_count += other.sequence_count;
self.set_count += other.set_count;
self.bytes_count += other.bytes_count;
self.borrow_count += other.borrow_count;
self.option_count += other.option_count;
self.enum_count += other.enum_count;
self.enum_variant_count += other.enum_variant_count;
self.map_count += other.map_count;
self.dynamic_count += other.dynamic_count;
self.result_count += other.result_count;
self.pointer_count += other.pointer_count;
self.skip_wire_count += other.skip_wire_count;
self.default_count += other.default_count;
self.opaque_count += other.opaque_count;
self.call_block_count += other.call_block_count;
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct LoweredMemProgramStats {
pub root: MemProgramStats,
pub blocks: MemProgramStats,
pub total: MemProgramStats,
pub block_count: usize,
}
impl LoweredMemProgramStats {
pub fn accumulate(&mut self, other: Self) {
self.root.accumulate(other.root);
self.blocks.accumulate(other.blocks);
self.total.accumulate(other.total);
self.block_count += other.block_count;
}
}
#[must_use]
pub fn mem_program_stats<BlockId>(program: &[MemOp<BlockId>]) -> MemProgramStats {
let mut stats = MemProgramStats::default();
add_mem_program_stats(program, &mut stats);
stats
}
#[must_use]
pub fn lowered_mem_program_stats<BlockId>(
lowered: &crate::Lowered<BlockId, MemOp<BlockId>>,
) -> LoweredMemProgramStats {
let root = mem_program_stats(&lowered.program);
let mut blocks = MemProgramStats::default();
for block in lowered.blocks.values() {
blocks.accumulate(mem_program_stats(block));
}
let mut total = root;
total.accumulate(blocks);
LoweredMemProgramStats {
root,
blocks,
total,
block_count: lowered.blocks.len(),
}
}
fn add_mem_program_stats<BlockId>(program: &[MemOp<BlockId>], stats: &mut MemProgramStats) {
for op in program {
stats.op_count += 1;
match op {
MemOp::Scalar { .. } => stats.scalar_op_count += 1,
MemOp::ScalarRun(run) => {
stats.scalar_run_count += 1;
stats.scalar_run_segment_count += run.segments.len();
}
MemOp::NativeInt { .. } => stats.native_int_count += 1,
MemOp::Sequence(seq) => {
stats.sequence_count += 1;
add_mem_program_stats(&seq.element, stats);
}
MemOp::Set(set) => {
stats.set_count += 1;
add_mem_program_stats(&set.element, stats);
}
MemOp::Bytes(_) => stats.bytes_count += 1,
MemOp::Borrow(_) => stats.borrow_count += 1,
MemOp::Option(option) => {
stats.option_count += 1;
add_mem_program_stats(&option.some, stats);
}
MemOp::Enum(en) => {
stats.enum_count += 1;
stats.enum_variant_count += en.variants.len();
for variant in &en.variants {
add_mem_program_stats(&variant.payload, stats);
}
}
MemOp::Map(map) => {
stats.map_count += 1;
add_mem_program_stats(&map.key, stats);
add_mem_program_stats(&map.value, stats);
}
MemOp::Dynamic { .. } => stats.dynamic_count += 1,
MemOp::Result(result) => {
stats.result_count += 1;
add_mem_program_stats(&result.ok, stats);
add_mem_program_stats(&result.err, stats);
}
MemOp::Pointer(pointer) => {
stats.pointer_count += 1;
add_mem_program_stats(&pointer.pointee, stats);
}
MemOp::SkipWire(_) => stats.skip_wire_count += 1,
MemOp::Default(_) => stats.default_count += 1,
MemOp::Opaque(_) => stats.opaque_count += 1,
MemOp::CallBlock { .. } => stats.call_block_count += 1,
}
}
}
#[must_use]
pub fn bulk_scalar_align<BlockId>(element: &[MemOp<BlockId>], stride: usize) -> Option<usize> {
match element {
[
MemOp::Scalar {
offset: 0,
size,
align,
},
] if *size == stride && *align != 0 && stride.is_multiple_of(*align) => Some(*align),
_ => None,
}
}
#[must_use]
pub fn group_record_scalars<BlockId>(
program: MemProgram<BlockId>,
ownership: &RecordByteOwnership,
record_base: usize,
) -> MemProgram<BlockId> {
let mut out = Vec::with_capacity(program.len());
let mut run = Vec::new();
for op in program {
if let Some(segments) = scalar_segments(&op) {
if run_can_append(&run, &segments, ownership, record_base) {
run.extend(segments);
} else {
flush_scalar_run(&mut out, &mut run);
run.extend(segments);
}
} else {
flush_scalar_run(&mut out, &mut run);
out.push(op);
}
}
flush_scalar_run(&mut out, &mut run);
out
}
#[must_use]
pub fn owned_sequence_op<BlockId>(
field_offset: usize,
element: MemProgram<BlockId>,
stride: usize,
elem_align: usize,
validate: ByteValidator,
thunks: SeqThunks,
) -> MemOp<BlockId> {
let element = fuse(element);
if bulk_scalar_align(&element, stride).is_some() {
MemOp::Bytes(Box::new(BytesOp {
field_offset,
stride,
elem_align,
validate,
thunks,
}))
} else {
let min_wire = element_min_wire(&element);
MemOp::Sequence(Box::new(SeqOp {
field_offset,
element,
stride,
elem_align,
min_wire,
thunks,
}))
}
}
#[must_use]
pub fn set_op<BlockId>(
field_offset: usize,
element: MemProgram<BlockId>,
elem_size: usize,
elem_align: usize,
thunks: SetThunks,
) -> MemOp<BlockId> {
let element = fuse(element);
let min_wire = element_min_wire(&element);
MemOp::Set(Box::new(SetOp {
field_offset,
element,
elem_size,
elem_align,
min_wire,
thunks,
}))
}
fn scalar_segments<BlockId>(op: &MemOp<BlockId>) -> Option<Vec<ScalarSegment>> {
match op {
MemOp::Scalar {
offset,
size,
align,
} => Some(vec![ScalarSegment {
offset: *offset,
size: *size,
align: *align,
}]),
MemOp::ScalarRun(run) => Some(run.segments.clone()),
_ => None,
}
}
fn run_can_append(
run: &[ScalarSegment],
next: &[ScalarSegment],
ownership: &RecordByteOwnership,
record_base: usize,
) -> bool {
let (Some(last), Some(first)) = (run.last(), next.first()) else {
return true;
};
let Some(last_end) = last.end() else {
return false;
};
if first.offset < last_end {
return false;
}
if first.offset == last_end {
return true;
}
absolute_gap_is_padding(ownership, record_base, last_end, first.offset)
}
fn absolute_gap_is_padding(
ownership: &RecordByteOwnership,
record_base: usize,
start: usize,
end: usize,
) -> bool {
let Some(rel_start) = start.checked_sub(record_base) else {
return false;
};
let Some(rel_end) = end.checked_sub(record_base) else {
return false;
};
if rel_end < rel_start {
return false;
}
ownership.is_padding_range(rel_start, rel_end - rel_start)
}
fn flush_scalar_run<BlockId>(out: &mut MemProgram<BlockId>, run: &mut Vec<ScalarSegment>) {
match run.len() {
0 => {}
1 => {
let segment = run[0];
out.push(MemOp::Scalar {
offset: segment.offset,
size: segment.size,
align: segment.align,
});
}
_ => {
out.push(MemOp::ScalarRun(Box::new(ScalarRunOp {
segments: core::mem::take(run),
})));
return;
}
}
run.clear();
}
pub fn lower_record_fields<SchemaRef, BlockId, Error>(
fields: &[FieldAccess<SchemaRef>],
base: usize,
out: &mut MemProgram<BlockId>,
mut lower_field: impl FnMut(
&Descriptor<SchemaRef>,
usize,
&mut MemProgram<BlockId>,
) -> Result<(), Error>,
) -> Result<(), Error> {
for field in fields {
lower_field(&field.descriptor, base + field.offset, out)?;
}
Ok(())
}
pub fn lower_fixed_array<BlockId, Error>(
count: usize,
stride: usize,
base: usize,
out: &mut MemProgram<BlockId>,
mut lower_element: impl FnMut(usize, &mut MemProgram<BlockId>) -> Result<(), Error>,
) -> Result<(), Error>
where
Error: From<LoweringError>,
{
let mut element_ops = Vec::new();
lower_element(0, &mut element_ops)?;
let element_ops = fuse(element_ops);
if let Some(align) = bulk_scalar_align(&element_ops, stride) {
out.push(MemOp::Scalar {
offset: base,
size: fixed_array_copy_size(count, stride).map_err(Error::from)?,
align,
});
return Ok(());
}
for index in 0..count {
let offset = array_element_offset(base, index, stride).map_err(Error::from)?;
lower_element(offset, out)?;
}
Ok(())
}
pub fn fixed_array_copy_size(count: usize, stride: usize) -> Result<usize, LoweringError> {
count
.checked_mul(stride)
.ok_or(LoweringError::ArrayBulkCopySizeOverflow)
}
pub fn array_element_offset(
base: usize,
index: usize,
stride: usize,
) -> Result<usize, LoweringError> {
let rel = index
.checked_mul(stride)
.ok_or(LoweringError::ArrayElementOffsetOverflow)?;
base.checked_add(rel)
.ok_or(LoweringError::ArrayElementOffsetOverflow)
}
#[must_use]
pub fn fuse<BlockId>(program: MemProgram<BlockId>) -> MemProgram<BlockId> {
let mut out: MemProgram<BlockId> = Vec::with_capacity(program.len());
let mut wire_pos: Option<usize> = Some(0);
for op in program {
match op {
MemOp::Scalar {
offset,
size,
align,
} => {
let pad = wire_pos.map(|p| align.wrapping_sub(p & (align - 1)) & (align - 1));
let fuses = pad == Some(0)
&& matches!(
out.last(),
Some(MemOp::Scalar { offset: po, size: ps, .. }) if po + ps == offset
);
if fuses {
if let Some(MemOp::Scalar { size: ps, .. }) = out.last_mut() {
*ps += size;
}
} else {
out.push(MemOp::Scalar {
offset,
size,
align,
});
}
wire_pos = wire_pos.map(|p| p + pad.unwrap_or(0) + size);
}
run @ MemOp::ScalarRun(_) => {
out.push(run);
wire_pos = None;
}
MemOp::NativeInt {
offset,
mem_size,
signed,
} => {
let align = 8usize;
let size = 8usize;
let pad = wire_pos.map(|p| align.wrapping_sub(p & (align - 1)) & (align - 1));
out.push(MemOp::NativeInt {
offset,
mem_size,
signed,
});
wire_pos = wire_pos.map(|p| p + pad.unwrap_or(0) + size);
}
seq @ (MemOp::Sequence(_)
| MemOp::Set(_)
| MemOp::Bytes(_)
| MemOp::Borrow(_)
| MemOp::Option(_)
| MemOp::Enum(_)
| MemOp::Map(_)
| MemOp::Result(_)
| MemOp::Pointer(_)
| MemOp::Dynamic { .. }
| MemOp::Opaque(_)
| MemOp::CallBlock { .. }
| MemOp::SkipWire(_)) => {
out.push(seq);
wire_pos = None;
}
def @ MemOp::Default(_) => out.push(def),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
unsafe extern "C" fn option_is_some(_ctx: *const (), _option: *const u8) -> bool {
false
}
unsafe extern "C" fn option_get_value(_ctx: *const (), _option: *const u8) -> *const u8 {
core::ptr::null()
}
unsafe extern "C" fn option_init_some(_ctx: *const (), _option: *mut u8, _value: *mut u8) {}
unsafe extern "C" fn option_init_none(_ctx: *const (), _option: *mut u8) {}
fn option_thunks() -> OptionThunks {
OptionThunks {
ctx: core::ptr::null(),
is_some: option_is_some,
get_value: option_get_value,
init_some: option_init_some,
init_none: option_init_none,
}
}
fn field(offset: usize, size: usize) -> FieldAccess<()> {
FieldAccess {
offset,
descriptor: Descriptor {
schema: (),
layout: Layout { size, align: 1 },
access: Access::Scalar,
},
default: None,
}
}
#[test]
fn element_min_wire_distinguishes_zero_sized_programs() {
assert_eq!(element_min_wire::<()>(&[]), 0);
assert_eq!(
element_min_wire(&[MemOp::<()>::Scalar {
offset: 0,
size: 0,
align: 1,
}]),
0
);
assert_eq!(
element_min_wire(&[MemOp::<()>::Scalar {
offset: 0,
size: 4,
align: 4,
}]),
1
);
}
#[test]
fn mem_program_stats_count_inline_shapes_and_lowered_blocks() {
let program = vec![
MemOp::<u8>::Scalar {
offset: 0,
size: 4,
align: 4,
},
MemOp::ScalarRun(Box::new(ScalarRunOp {
segments: vec![
ScalarSegment {
offset: 8,
size: 2,
align: 2,
},
ScalarSegment {
offset: 12,
size: 4,
align: 4,
},
],
})),
MemOp::Enum(Box::new(EnumOp {
tag_offset: 16,
tag_width: 4,
variants: vec![
EnumVariantOp {
wire_index: 0,
selector: 0,
payload: vec![MemOp::Dynamic { field_offset: 24 }],
},
EnumVariantOp {
wire_index: 1,
selector: 1,
payload: vec![MemOp::NativeInt {
offset: 32,
mem_size: 8,
signed: false,
}],
},
],
writer_only: vec![9],
})),
MemOp::CallBlock {
schema: 7,
offset: 40,
},
];
let stats = mem_program_stats(&program);
assert_eq!(stats.op_count, 6);
assert_eq!(stats.scalar_op_count, 1);
assert_eq!(stats.scalar_run_count, 1);
assert_eq!(stats.scalar_run_segment_count, 2);
assert_eq!(stats.enum_count, 1);
assert_eq!(stats.enum_variant_count, 2);
assert_eq!(stats.dynamic_count, 1);
assert_eq!(stats.native_int_count, 1);
assert_eq!(stats.call_block_count, 1);
let mut lowered = crate::Lowered::new(program);
lowered.blocks.insert(
7,
vec![
MemOp::Scalar {
offset: 0,
size: 1,
align: 1,
},
MemOp::SkipWire(Box::new(SkipOp::Scalar { size: 4, align: 4 })),
],
);
let lowered_stats = lowered_mem_program_stats(&lowered);
assert_eq!(lowered_stats.block_count, 1);
assert_eq!(lowered_stats.root.op_count, 6);
assert_eq!(lowered_stats.blocks.op_count, 2);
assert_eq!(lowered_stats.blocks.skip_wire_count, 1);
assert_eq!(lowered_stats.total.op_count, 8);
assert_eq!(lowered_stats.total.scalar_op_count, 2);
}
#[test]
fn canonical_mem_program_promotes_scalars_runs_and_block_calls() {
let program = vec![
MemOp::<u8>::Scalar {
offset: 0,
size: 4,
align: 4,
},
MemOp::ScalarRun(Box::new(ScalarRunOp {
segments: vec![
ScalarSegment {
offset: 8,
size: 2,
align: 2,
},
ScalarSegment {
offset: 12,
size: 4,
align: 4,
},
],
})),
MemOp::NativeInt {
offset: 16,
mem_size: 8,
signed: true,
},
MemOp::CallBlock {
schema: 9,
offset: 24,
},
];
let canonical = canonical_mem_program(program);
let stats = crate::ir::program_stats(&canonical);
assert_eq!(stats.op_count, 4);
assert_eq!(stats.memory_op_count, 2);
assert_eq!(stats.scalar_copy_count, 1);
assert_eq!(stats.scalar_run_count, 1);
assert_eq!(stats.scalar_run_segment_count, 2);
assert_eq!(stats.control_op_count, 1);
assert_eq!(stats.block_call_count, 1);
assert_eq!(stats.intrinsic_op_count, 1);
let counts = crate::ir::intrinsic_counts(&canonical);
assert_eq!(
counts.get(&crate::ir::IntrinsicDescriptor {
dialect: "weavy.mem",
name: "native_int",
}),
Some(&1)
);
let roundtripped = mem_program_from_canonical(canonical).unwrap();
match roundtripped.as_slice() {
[
MemOp::Scalar {
offset,
size,
align,
},
MemOp::ScalarRun(run),
MemOp::NativeInt {
offset: native_offset,
mem_size,
signed,
},
MemOp::CallBlock {
schema,
offset: block_offset,
},
] => {
assert_eq!((*offset, *size, *align), (0, 4, 4));
assert_eq!(run.segments.len(), 2);
assert_eq!((*native_offset, *mem_size, *signed), (16, 8, true));
assert_eq!((*schema, *block_offset), (9, 24));
}
other => panic!("unexpected roundtrip shape: {other:?}"),
}
}
#[test]
fn canonical_mem_program_rejects_non_legacy_ops() {
let program: CanonicalMemProgram<()> =
vec![crate::ir::WeavyOp::Memory(crate::ir::MemoryOp::Zero {
offset: 0,
size: 8,
})];
let err = mem_program_from_canonical(program).unwrap_err();
assert_eq!(err, CanonicalMemError::Zero);
}
#[test]
fn canonical_mem_stats_enter_nested_intrinsic_programs() {
let program = vec![MemOp::<u8>::Option(Box::new(OptionOp {
field_offset: 0,
some: vec![
MemOp::Scalar {
offset: 4,
size: 4,
align: 4,
},
MemOp::NativeInt {
offset: 8,
mem_size: 8,
signed: false,
},
],
inner_size: 16,
inner_align: 8,
thunks: option_thunks(),
}))];
let canonical = canonical_mem_program(program);
let shallow = crate::ir::program_stats(&canonical);
assert_eq!(shallow.op_count, 1);
assert_eq!(shallow.intrinsic_op_count, 1);
let recursive = canonical_mem_program_stats(&canonical);
assert_eq!(recursive.op_count, 3);
assert_eq!(recursive.memory_op_count, 1);
assert_eq!(recursive.scalar_copy_count, 1);
assert_eq!(recursive.intrinsic_op_count, 2);
let counts = canonical_mem_intrinsic_counts(&canonical);
assert_eq!(
counts.get(&crate::ir::IntrinsicDescriptor {
dialect: "weavy.mem",
name: "option",
}),
Some(&1)
);
assert_eq!(
counts.get(&crate::ir::IntrinsicDescriptor {
dialect: "weavy.mem",
name: "native_int",
}),
Some(&1)
);
let roundtripped = mem_program_from_canonical(canonical).unwrap();
let stats = mem_program_stats(&roundtripped);
assert_eq!(stats.option_count, 1);
assert_eq!(stats.scalar_op_count, 1);
assert_eq!(stats.native_int_count, 1);
}
#[test]
fn canonical_mem_effect_stats_enter_nested_intrinsic_programs() {
let program = vec![MemOp::<u8>::Option(Box::new(OptionOp {
field_offset: 4,
some: vec![MemOp::NativeInt {
offset: 8,
mem_size: 8,
signed: true,
}],
inner_size: 8,
inner_align: 8,
thunks: option_thunks(),
}))];
let canonical = canonical_mem_program(program);
let stats = canonical_mem_program_effect_stats(&canonical);
assert_eq!(stats.op_count, 2);
assert_eq!(stats.intrinsic_op_count, 2);
assert_eq!(stats.input_read_count, 2);
assert_eq!(stats.input_advance_count, 2);
assert_eq!(stats.sink_write_count, 2);
assert_eq!(stats.may_fail_count, 2);
assert_eq!(stats.calls_user_code_count, 1);
assert_eq!(stats.typed_memory_read_count, 2);
assert_eq!(stats.typed_memory_initialize_count, 2);
assert_eq!(stats.typed_memory_move_count, 1);
assert_eq!(stats.ordered_count, 1);
assert_eq!(stats.barrier_count, 1);
assert_eq!(stats.opaque_count, 0);
}
#[test]
fn canonical_mem_effect_stats_count_explicit_opaque_barriers() {
let program = vec![WeavyOp::<(), _>::Intrinsic(MemIntrinsic::Dynamic {
field_offset: 24,
})];
let stats = canonical_mem_program_effect_stats(&program);
assert_eq!(stats.op_count, 1);
assert_eq!(stats.intrinsic_op_count, 1);
assert_eq!(stats.opaque_count, 1);
assert_eq!(stats.barrier_count, 1);
assert_eq!(stats.may_allocate_count, 1);
assert_eq!(stats.calls_user_code_count, 1);
}
#[test]
fn bulk_scalar_align_requires_one_full_stride_scalar() {
let scalar = [MemOp::<()>::Scalar {
offset: 0,
size: 8,
align: 4,
}];
assert_eq!(bulk_scalar_align(&scalar, 8), Some(4));
let partial = [MemOp::<()>::Scalar {
offset: 0,
size: 4,
align: 4,
}];
assert_eq!(bulk_scalar_align(&partial, 8), None);
let shifted = [MemOp::<()>::Scalar {
offset: 4,
size: 4,
align: 4,
}];
assert_eq!(bulk_scalar_align(&shifted, 4), None);
}
#[test]
fn fixed_array_lowering_collapses_full_stride_scalar_elements() {
let mut out = Vec::new();
lower_fixed_array::<(), LoweringError>(3, 4, 16, &mut out, |base, out| {
out.push(MemOp::Scalar {
offset: base,
size: 4,
align: 4,
});
Ok(())
})
.unwrap();
match out.as_slice() {
[
MemOp::Scalar {
offset,
size,
align,
},
] => {
assert_eq!((*offset, *size, *align), (16, 12, 4));
}
other => panic!("expected one collapsed scalar op, got {other:?}"),
}
}
#[test]
fn fixed_array_lowering_replays_structured_elements_at_checked_offsets() {
let mut out = Vec::new();
lower_fixed_array::<(), LoweringError>(2, 8, 16, &mut out, |base, out| {
out.push(MemOp::Scalar {
offset: base + 4,
size: 4,
align: 4,
});
Ok(())
})
.unwrap();
let offsets: Vec<_> = out
.iter()
.map(|op| match op {
MemOp::Scalar { offset, .. } => *offset,
other => panic!("unexpected op {other:?}"),
})
.collect();
assert_eq!(offsets, [20, 28]);
}
#[test]
fn fixed_array_offset_helpers_report_overflow() {
assert_eq!(
fixed_array_copy_size(usize::MAX, 2),
Err(LoweringError::ArrayBulkCopySizeOverflow)
);
assert_eq!(
array_element_offset(usize::MAX - 1, 1, 2),
Err(LoweringError::ArrayElementOffsetOverflow)
);
}
#[test]
fn record_byte_ownership_marks_internal_and_tail_padding() {
let fields = [field(0, 4), field(8, 2)];
let ownership =
RecordByteOwnership::from_record_layout(Layout { size: 12, align: 4 }, &fields);
assert_eq!(
ownership.ranges,
[
ByteRange {
offset: 0,
len: 4,
owner: ByteOwner::Field(0),
},
ByteRange {
offset: 4,
len: 4,
owner: ByteOwner::Padding,
},
ByteRange {
offset: 8,
len: 2,
owner: ByteOwner::Field(1),
},
ByteRange {
offset: 10,
len: 2,
owner: ByteOwner::Padding,
},
]
);
}
#[test]
fn record_field_ranges_do_not_turn_gaps_into_padding() {
let fields = [field(0, 4), field(8, 2)];
let ownership = RecordByteOwnership::fields_only(&fields);
assert_eq!(
ownership.ranges,
[
ByteRange {
offset: 0,
len: 4,
owner: ByteOwner::Field(0),
},
ByteRange {
offset: 8,
len: 2,
owner: ByteOwner::Field(1),
},
]
);
}
#[test]
fn record_byte_ownership_falls_back_to_unknown_for_bad_ranges() {
let overlapping = [field(0, 8), field(4, 4)];
let out_of_bounds = [field(8, 8)];
assert_eq!(
RecordByteOwnership::from_record_layout(Layout { size: 12, align: 4 }, &overlapping),
RecordByteOwnership::unknown(12)
);
assert_eq!(
RecordByteOwnership::from_record_layout(Layout { size: 12, align: 4 }, &out_of_bounds),
RecordByteOwnership::unknown(12)
);
}
#[test]
fn record_byte_ownership_answers_padding_ranges() {
let fields = [field(0, 4), field(8, 2)];
let ownership =
RecordByteOwnership::from_record_layout(Layout { size: 12, align: 4 }, &fields);
assert!(ownership.is_padding_range(4, 4));
assert!(ownership.is_padding_range(10, 2));
assert!(!ownership.is_padding_range(2, 4));
assert!(!ownership.is_padding_range(12, 1));
}
#[test]
fn record_scalar_grouping_crosses_explicit_padding() {
let fields = [field(0, 4), field(8, 2)];
let ownership =
RecordByteOwnership::from_record_layout(Layout { size: 12, align: 4 }, &fields);
let program = vec![
MemOp::<()>::Scalar {
offset: 16,
size: 4,
align: 4,
},
MemOp::Scalar {
offset: 24,
size: 2,
align: 2,
},
];
let grouped = group_record_scalars(program, &ownership, 16);
match grouped.as_slice() {
[MemOp::ScalarRun(run)] => assert_eq!(
run.segments,
[
ScalarSegment {
offset: 16,
size: 4,
align: 4,
},
ScalarSegment {
offset: 24,
size: 2,
align: 2,
},
]
),
other => panic!("expected one scalar run, got {other:?}"),
}
}
#[test]
fn record_scalar_grouping_crosses_contiguous_wire_padding() {
let fields = [field(0, 4), field(4, 8)];
let ownership =
RecordByteOwnership::from_record_layout(Layout { size: 12, align: 8 }, &fields);
let program = vec![
MemOp::<()>::Scalar {
offset: 0,
size: 4,
align: 4,
},
MemOp::Scalar {
offset: 4,
size: 8,
align: 8,
},
];
let grouped = group_record_scalars(program, &ownership, 0);
match grouped.as_slice() {
[MemOp::ScalarRun(run)] => assert_eq!(run.segments.len(), 2),
other => panic!("expected one scalar run, got {other:?}"),
}
}
#[test]
fn record_scalar_grouping_does_not_cross_unknown_gap() {
let fields = [field(0, 4), field(8, 2)];
let ownership = RecordByteOwnership::fields_only(&fields);
let program = vec![
MemOp::<()>::Scalar {
offset: 0,
size: 4,
align: 4,
},
MemOp::Scalar {
offset: 8,
size: 2,
align: 2,
},
];
let grouped = group_record_scalars(program, &ownership, 0);
assert!(matches!(
grouped.as_slice(),
[
MemOp::Scalar { offset: 0, .. },
MemOp::Scalar { offset: 8, .. }
]
));
}
}