use crate::*;
use ::core::ops::AddAssign;
#[derive(Clone)]
pub struct TextPage(pub [u16; 256]);
impl Default for TextPage {
fn default() -> TextPage {
TextPage([0; 256])
}
}
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub struct JumpOffset(i32);
impl JumpOffset {
pub fn sentinel() -> JumpOffset {
JumpOffset(0)
}
pub fn offset(n: i32) -> JumpOffset {
JumpOffset(n)
}
pub fn new(current: JumpTarget, to: JumpTarget) -> Result<JumpOffset, ValidationError> {
if to == JumpTarget::SENTINEL {
Ok(JumpOffset::sentinel())
} else {
let offset = (to.0 as i32) - (current.0 as i32);
let fits = offset == ((offset << (32 - 22)) >> (32 - 22));
if !fits {
Err(ValidationError::LabelJumpTooLarge)
} else {
Ok(JumpOffset(offset))
}
}
}
}
#[derive(Clone, Copy)]
pub struct LabelTarget(u32);
impl ::core::fmt::Debug for LabelTarget {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("LabelTarget")
.field("arity", &self.arity())
.field("depth", &self.depth())
.field("jump", &self.jump())
.finish()
}
}
impl From<u32> for LabelTarget {
fn from(value: u32) -> Self {
LabelTarget(value)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[repr(u8)]
pub enum LabelArity {
None = 0,
I32 = 1,
I64 = 2,
}
impl LabelTarget {
fn new(result_type: ResultType, depth: u8, offset: JumpOffset) -> LabelTarget {
let rt = match result_type.0 {
None => 0,
Some(ValType::I32 | ValType::F32) => 1,
Some(ValType::I64 | ValType::F64) => 2,
};
LabelTarget((rt << 30) | ((depth as u32) << 22) | (offset.0 as u32) & 0x3FFFFF)
}
fn early_return(result_type: ResultType) -> LabelTarget {
Self::new(result_type, 0, JumpOffset::sentinel())
}
pub fn with_jump(self, offset: JumpOffset) -> LabelTarget {
LabelTarget(
self.0 & 0xFFC00000 | ((offset.0 as u32) & 0x3FFFFF), )
}
pub fn jump(&self) -> JumpOffset {
let u = self.0 & 0x3FFFFF;
JumpOffset(((u << 10) as i32) >> 10)
}
pub fn is_sentinel(&self) -> bool {
self.jump() == JumpOffset(0)
}
pub fn arity(&self) -> LabelArity {
match (self.0 >> 30) & 0b11 {
0 => LabelArity::None,
1 => LabelArity::I32,
2 => LabelArity::I64,
_ => unreachable!(),
}
}
pub fn depth(&self) -> u8 {
((self.0 >> 22) & 0xFF) as u8
}
}
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub struct JumpTarget(pub u32);
impl core::ops::Add<JumpOffset> for JumpTarget {
type Output = JumpTarget;
fn add(self, rhs: JumpOffset) -> JumpTarget {
JumpTarget(((self.0 as i32) + rhs.0) as u32)
}
}
impl core::ops::Add<u32> for JumpTarget {
type Output = JumpTarget;
fn add(self, rhs: u32) -> JumpTarget {
JumpTarget(self.0 + rhs)
}
}
impl AddAssign<JumpOffset> for JumpTarget {
fn add_assign(&mut self, rhs: JumpOffset) {
let a = (self.0 as i32) + rhs.0;
self.0 = a as u32;
}
}
impl JumpTarget {
pub const SENTINEL: JumpTarget = JumpTarget(0xFFFF_FFFFu32);
pub fn page(&self) -> usize {
(self.0 >> 8) as usize
}
pub fn offset(&self) -> usize {
(self.0 & 0xFF) as usize
}
}
#[derive(Clone, Copy)]
pub enum BlockKind {
Loop, Block, If,
}
#[derive(Clone)]
struct ControlFrame {
kind: BlockKind,
label: ResultType,
out: ResultType,
height: u16,
target: JumpTarget,
unreachable: bool,
}
#[derive(Debug)]
pub struct GlobalVariable {
pub reference: Ref,
pub ty: ValType,
pub mutable: bool,
}
#[derive(Debug, Copy, Clone)]
pub struct CompilerOptions {
pub allow_memory_grow: bool,
pub max_backpatch_iterations: u32,
pub max_code_pages: u32,
}
pub struct CodeBuilder {
pages: Vec<Box<TextPage>>,
offset: usize,
options: CompilerOptions,
}
impl CodeBuilder {
pub fn new(options: CompilerOptions) -> Result<CodeBuilder, AllocError> {
if options.max_code_pages >= (1 << 24) {
panic!("SpaceWasm supports up to 24-bit code pages");
}
Ok(CodeBuilder {
pages: Vec::new(options.max_code_pages)?,
offset: 0,
options,
})
}
pub(crate) fn options(&self) -> &CompilerOptions {
&self.options
}
fn backpatch(
&mut self,
start: JumpTarget,
mut f: impl FnMut(&mut Self, JumpTarget, LabelTarget) -> Result<(), ValidationError>,
) -> Result<(), ValidationError> {
if start == JumpTarget::SENTINEL {
return Ok(());
}
let mut next = start;
let mut n = 0;
loop {
let address = next;
let lt = LabelTarget(self.read_32(address)?);
f(self, address, lt)?;
if lt.is_sentinel() {
break;
}
next = address + lt.jump();
n += 1;
if self.options().max_backpatch_iterations != 0
&& n > self.options().max_backpatch_iterations
{
return Err(ValidationError::PossibleBackpatchCycle);
}
}
Ok(())
}
pub fn pages(&self) -> &[Box<TextPage>] {
&self.pages
}
pub fn offset(&self) -> usize {
self.offset
}
pub fn pc(&self) -> JumpTarget {
let (page_index, offset) = {
if self.offset == 256 {
(self.pages.len(), 0)
} else if self.pages.is_empty() {
(0, 0)
} else {
(self.pages.len() - 1, self.offset)
}
};
assert!(page_index < (1 << 24));
assert!(offset < 256);
JumpTarget(((page_index as u32) << 8) | (offset as u32))
}
fn add_page(&mut self) -> Result<(), AllocError> {
self.pages.try_push(Box::new(TextPage::default())?)?;
self.offset = 0;
Ok(())
}
fn push(&mut self, c: u16) -> Result<(), AllocError> {
if self.offset == 0 && self.pages.is_empty() {
self.add_page()?;
} else if self.offset >= 256 {
self.add_page()?;
}
let idx = self.pages.len() - 1;
self.pages[idx].0[self.offset] = c;
self.offset += 1;
Ok(())
}
fn write(&mut self, address: JumpTarget, value: u16) -> Result<(), ValidationError> {
let page_index = address.page();
let offset = address.offset();
if page_index >= self.pages.len() || offset >= 256 {
Err(ValidationError::PageFault)
} else {
self.pages[page_index].0[offset] = value;
Ok(())
}
}
fn write_32(&mut self, address: JumpTarget, value: u32) -> Result<(), ValidationError> {
self.write(address, value as u16)?;
self.write(address + 1, (value >> 16) as u16)?;
Ok(())
}
fn read(&self, address: JumpTarget) -> Result<u16, ValidationError> {
let page_index = address.page();
let offset = address.offset();
if page_index >= self.pages.len() || offset >= 256 {
Err(ValidationError::PageFault)
} else {
Ok(self.pages[page_index].0[offset])
}
}
fn read_32(&self, address: JumpTarget) -> Result<u32, ValidationError> {
let w1 = self.read(address)?;
let w2 = self.read(address + 1)?;
Ok((w1 as u32) | ((w2 as u32) << 16))
}
}
pub struct TextBuilder<'a, const MAX_CONTROL_FRAMES: usize, const MAX_STACK_DEPTH: usize> {
code: &'a mut CodeBuilder,
store: &'a Store,
module: &'a Module,
func: &'a Func,
control_frames: StaticVec<ControlFrame, MAX_CONTROL_FRAMES>,
value_stack: StaticVec<OperandType, MAX_STACK_DEPTH>,
stack_highwater: usize,
br_table_result: Option<ResultType>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub(crate) enum OperandType {
Unknown,
Known(ValType),
}
impl From<ValType> for OperandType {
fn from(value: ValType) -> Self {
OperandType::Known(value)
}
}
impl From<&ValType> for OperandType {
fn from(value: &ValType) -> Self {
OperandType::Known(*value)
}
}
impl From<OperandType> for ValType {
fn from(value: OperandType) -> Self {
match value {
OperandType::Unknown => ValType::I32,
OperandType::Known(t) => t,
}
}
}
impl From<OperandType> for u8 {
fn from(value: OperandType) -> Self {
let vty: ValType = value.into();
vty as u8
}
}
impl<'a, const MAX_CONTROL_FRAMES: usize, const MAX_STACK_DEPTH: usize>
TextBuilder<'a, MAX_CONTROL_FRAMES, MAX_STACK_DEPTH>
{
pub fn new(
code: &'a mut CodeBuilder,
store: &'a Store,
module: &'a Module,
func: &'a Func,
) -> Self {
let mut builder = TextBuilder {
code,
store,
module,
func,
control_frames: Default::default(),
value_stack: Default::default(),
stack_highwater: 0,
br_table_result: None,
};
let return_type = ResultType(func.return_ty);
let _ = builder.push_control(BlockKind::Block, return_type, return_type);
builder
}
pub fn options(&self) -> &CompilerOptions {
self.code.options()
}
pub fn store(&self) -> &'a Store {
self.store
}
pub fn func(&self) -> &'a Func {
self.func
}
pub fn module(&self) -> &'a Module {
self.module
}
pub fn stack_usage(&self) -> usize {
self.stack_highwater
}
pub fn check_br_table_result(&mut self, r: ResultType) -> Result<(), ValidationError> {
if let Some(other) = self.br_table_result {
if other != r {
Err(ValidationError::BlockResultTypeMismatch)
} else {
Ok(())
}
} else {
self.br_table_result = Some(r);
Ok(())
}
}
pub fn check_and_clear_br_table_result(
&mut self,
def_result: ResultType,
) -> Result<(), ValidationError> {
if let Some(other) = self.br_table_result {
if other != def_result {
Err(ValidationError::BlockResultTypeMismatch)
} else {
self.br_table_result = None;
Ok(())
}
} else {
Ok(())
}
}
pub fn get_local(&self, x: LocalIdx) -> Result<LocalVariable, ValidationError> {
if x.0 > 0xFFFF {
return Err(ValidationError::LocalIdxOutOfRange);
}
let x = x.0 as u16;
let signature = self
.module
.types
.get(self.func.ty.0 as usize)
.ok_or(ValidationError::TypeIdxOutOfRange)?;
let mut current_offset = 0usize;
let mut current_index = 0;
for (i, p_ty) in signature.params.iter().enumerate() {
if x == i as u16 {
let frame_offset =
(((current_offset / 4) as i32) - (self.func.parameter_size as i32)) as i16;
return Ok(LocalVariable {
frame_offset,
ty: *p_ty,
});
}
current_offset += p_ty.size();
current_index += 1;
}
current_offset = 0;
for (n, ty) in &self.func.locals {
if current_index + n > x {
let offset = current_offset + ty.size() * (x - current_index) as usize;
return Ok(LocalVariable {
frame_offset: ((offset / 4) as i16) + 2,
ty: *ty,
});
}
let section_size = *n as usize * ty.size();
current_offset += section_size;
current_index += n;
}
Err(ValidationError::LocalIdxOutOfRange)
}
pub fn get_global(&self, x: GlobalIdx) -> Result<GlobalVariable, ValidationError> {
let reference = self
.module
.get_global_ref(x)
.ok_or(ValidationError::GlobalIdxOutOfRange)?;
match reference {
Ref::Module(idx) => {
let g = self
.module
.globals
.get(idx as usize)
.ok_or(ValidationError::GlobalIdxOutOfRange)?;
Ok(GlobalVariable {
reference,
ty: g.type_.ty,
mutable: g.type_.mutable,
})
}
Ref::Host { module, index } => {
let module = self.store.host_modules().get(module.0 as usize).unwrap();
let global = module.globals.get(index as usize).unwrap();
Ok(GlobalVariable {
reference,
ty: global.value.ty(),
mutable: global.value.mutable(),
})
}
Ref::Extern { module, index } => {
let module = self.store.modules().get(module.0 as usize).unwrap();
let global = module.globals.get(index as usize).unwrap();
Ok(GlobalVariable {
reference,
ty: global.type_.ty,
mutable: global.type_.mutable,
})
}
}
}
pub fn pc(&self) -> JumpTarget {
self.code.pc()
}
pub(crate) fn mark_unreachable(&mut self) {
if let Some(frame) = self.control_frames.last_mut() {
self.value_stack.truncate(frame.height as usize);
frame.unreachable = true;
}
}
fn is_unreachable(&self) -> bool {
self.control_frames
.last()
.map(|c| c.unreachable)
.unwrap_or(false)
}
fn control_frame_stack_len(&self) -> usize {
let height = self
.control_frames
.last()
.map(|c| c.height as usize)
.unwrap_or(0);
self.value_stack.len() - height
}
pub(crate) fn write_label_target(
&mut self,
label: LabelIdx,
) -> Result<ResultType, ValidationError> {
if label.0 as usize >= self.control_frames.len() {
return Err(ValidationError::InvalidLabelIndex);
}
let idx = self.control_frames.len() - 1 - label.0 as usize;
let result = if idx == 0 {
let lt = LabelTarget::early_return(ResultType(self.func.return_ty));
self.code.push(lt.0 as u16)?;
self.code.push((lt.0 >> 16) as u16)?;
ResultType(self.func.return_ty)
} else {
let pc = self.pc();
let start_stack_height = self.stack_height();
let end_stack_height = self.stack_height_for(self.control_frames[idx].height as usize);
let stack_delta = start_stack_height - end_stack_height;
if stack_delta > u8::MAX as usize {
return Err(ValidationError::LabelStackJumpTooDeep);
}
let control_frame = &mut self.control_frames[idx];
match control_frame.kind {
BlockKind::Loop => {
let lt = LabelTarget::new(
ResultType(None),
stack_delta as u8,
JumpOffset::new(pc, control_frame.target)?,
);
self.code.push(lt.0 as u16)?;
self.code.push((lt.0 >> 16) as u16)?;
ResultType(None)
}
BlockKind::Block | BlockKind::If => {
let target = control_frame.target;
control_frame.target = self.code.pc();
let lt = LabelTarget::new(
control_frame.out,
(stack_delta & 0xFF) as u8,
JumpOffset::new(pc, target)?,
);
self.code.push(lt.0 as u16)?;
self.code.push((lt.0 >> 16) as u16)?;
control_frame.label
}
}
};
Ok(result)
}
pub(crate) fn stack_height(&self) -> usize {
let mut size: usize = 0;
for i in self.value_stack.iter() {
size += match i {
OperandType::Unknown => break,
OperandType::Known(ValType::I32) => 1,
OperandType::Known(ValType::I64) => 2,
OperandType::Known(ValType::F32) => 1,
OperandType::Known(ValType::F64) => 2,
}
}
size
}
pub(crate) fn stack_height_for(&self, truncate_len: usize) -> usize {
assert!(truncate_len <= self.value_stack.len());
let mut size: usize = 0;
for i in self.value_stack[0..truncate_len].iter() {
size += match i {
OperandType::Unknown => break,
OperandType::Known(ValType::I32) => 1,
OperandType::Known(ValType::I64) => 2,
OperandType::Known(ValType::F32) => 1,
OperandType::Known(ValType::F64) => 2,
}
}
size
}
pub(crate) fn push_stack(&mut self, ty: impl Into<OperandType>) -> Result<(), ValidationError> {
self.value_stack.push(ty.into())?;
let l = self.stack_height();
if l > self.stack_highwater {
self.stack_highwater = l;
}
Ok(())
}
pub(crate) fn pop_stack_t(&mut self) -> Result<OperandType, ValidationError> {
if self.control_frame_stack_len() == 0 && self.is_unreachable() {
Ok(OperandType::Unknown)
} else if self.control_frame_stack_len() == 0 {
Err(ValidationError::StackUnderflow)
} else {
Ok(self
.value_stack
.pop()
.ok_or(ValidationError::StackUnderflow)?)
}
}
pub(crate) fn pop_result_type(&mut self, result: ResultType) -> Result<(), ValidationError> {
if let Some(result) = result.0 {
self.pop_stack(result)?;
Ok(())
} else {
Ok(())
}
}
pub(crate) fn push_result_type(&mut self, result: ResultType) -> Result<(), ValidationError> {
if let Some(result) = result.0 {
self.push_stack(result)
} else {
Ok(())
}
}
pub(crate) fn pop_stack(
&mut self,
expect: impl Into<OperandType>,
) -> Result<OperandType, ValidationError> {
let expect = expect.into();
let actual = self.pop_stack_t()?;
if actual == OperandType::Unknown {
return Ok(expect);
}
if expect == OperandType::Unknown {
return Ok(actual);
}
if actual != expect {
Err(ValidationError::TypeMismatch)
} else {
Ok(actual)
}
}
pub(crate) fn push_control(
&mut self,
kind: BlockKind,
label: ResultType,
out: ResultType,
) -> Result<(), ValidationError> {
let frame = ControlFrame {
kind,
label,
out,
height: self.value_stack.len() as u16,
target: JumpTarget::SENTINEL,
unreachable: false,
};
self.control_frames.push(frame)?;
Ok(())
}
pub(crate) fn set_control_target(&mut self, target: JumpTarget) -> Result<(), ValidationError> {
let frame = self
.control_frames
.last_mut()
.ok_or(ValidationError::InvalidEndBlock)?;
frame.target = target;
Ok(())
}
pub(crate) fn write_if_else_target(&mut self) -> Result<(), ValidationError> {
let out = self
.control_frames
.last()
.ok_or(ValidationError::InvalidEndBlock)?
.out;
let patch_location = self.pc();
let placeholder = LabelTarget::new(out, 0, JumpOffset::sentinel());
self.code.push(placeholder.0 as u16)?;
self.code.push((placeholder.0 >> 16) as u16)?;
self.control_frames.last_mut().unwrap().target = patch_location;
Ok(())
}
pub(crate) fn pop_control_and_patch_if(
&mut self,
) -> Result<(ResultType, JumpTarget), ValidationError> {
let (out, height) = {
let Some(last) = self.control_frames.last() else {
return Err(ValidationError::InvalidEndBlock);
};
(last.out, last.height)
};
self.pop_result_type(out)?;
if self.value_stack.len() != height as usize {
return Err(ValidationError::BlockResultTypeMismatch);
}
let last = self.control_frames.pop().unwrap();
let pc = self.pc();
let mut prev = JumpTarget::SENTINEL;
let mut remaining_chain = JumpTarget::SENTINEL;
self.code.backpatch(last.target, |code, address, label| {
if label.is_sentinel() {
let patched = label.with_jump(JumpOffset::new(address, pc)?);
code.write_32(address, patched.0)?;
if prev != JumpTarget::SENTINEL {
let old_label = LabelTarget(code.read_32(prev)?);
let new_tail = old_label.with_jump(JumpOffset::sentinel());
code.write_32(prev, new_tail.0)?;
remaining_chain = last.target;
}
} else {
prev = address;
}
Ok(())
})?;
Ok((last.out, remaining_chain))
}
pub(crate) fn pop_control(&mut self) -> Result<ResultType, ValidationError> {
let (out, height) = {
let Some(last) = self.control_frames.last() else {
return Err(ValidationError::InvalidEndBlock);
};
(last.out, last.height)
};
self.pop_result_type(out)?;
if self.value_stack.len() != height as usize {
return Err(ValidationError::BlockResultTypeMismatch);
}
let last = self.control_frames.pop().unwrap();
match last.kind {
BlockKind::Loop => (),
BlockKind::Block => {
let pc = self.pc();
self.code.backpatch(last.target, |code, address, label| {
let patched = label.with_jump(JumpOffset::new(address, pc)?);
code.write_32(address, patched.0)?;
Ok(())
})?;
}
BlockKind::If => {
if last.out.0.is_none() || last.unreachable {
let pc = self.pc();
self.code.backpatch(last.target, |code, address, label| {
let patched = label.with_jump(JumpOffset::new(address, pc)?);
code.write_32(address, patched.0)?;
Ok(())
})?;
} else {
Err(ValidationError::BlockResultTypeMismatch)?;
}
}
}
Ok(last.out)
}
pub(crate) fn instr(&mut self, op: u8) -> Result<(), AllocError> {
self.code.push((op as u16) << 8)
}
pub(crate) fn instr_imm_8(&mut self, op: u8, imm: u8) -> Result<(), AllocError> {
self.code.push((op as u16) << 8 | (imm as u16))
}
pub(crate) fn instr_imm_8_or_16(&mut self, op: u8, idx: u32) -> Result<(), ValidationError> {
if idx > 0xFFFF {
Err(ValidationError::IdxTooLarge)
} else if idx < 0xFF {
self.instr_imm_8(op, idx as u8)?;
Ok(())
} else {
self.instr_imm_8(op, 0xFF)?;
self.code.push(idx as u16)?;
Ok(())
}
}
pub(crate) fn instr_mem(&mut self, op: u8, m: MemArg) -> Result<(), ValidationError> {
if m.align >= 0xFF {
Err(ValidationError::MemAlignTooLarge)
} else {
self.instr_imm_8(op, m.align as u8)?;
self.code.push((m.offset & 0xFFFF) as u16)?;
self.code.push(((m.offset >> 16) & 0xFFFF) as u16)?;
Ok(())
}
}
pub(crate) fn instr_imm_8_or_32(&mut self, op: u8, i: u32) -> Result<(), AllocError> {
if i < 0xFF {
self.instr_imm_8(op, i as u8)?;
} else {
self.instr_imm_8(op, 0xFF)?;
self.write_32(i)?;
}
Ok(())
}
pub(crate) fn instr_imm_8_or_64(&mut self, op: u8, i: u64) -> Result<(), AllocError> {
if i < 0xFF {
self.instr_imm_8(op, i as u8)?;
} else {
self.instr_imm_8(op, 0xFF)?;
self.write_64(i)?;
}
Ok(())
}
pub(crate) fn write_16(&mut self, word: u16) -> Result<(), AllocError> {
self.code.push(word)
}
pub(crate) fn write_32(&mut self, i: u32) -> Result<(), AllocError> {
self.code.push(i as u16)?;
self.code.push((i >> 16) as u16)?;
Ok(())
}
pub(crate) fn write_64(&mut self, i: u64) -> Result<(), AllocError> {
self.code.push(i as u16)?;
self.code.push((i >> 16) as u16)?;
self.code.push((i >> 32) as u16)?;
self.code.push((i >> 48) as u16)?;
Ok(())
}
}
#[cfg(kani)]
mod kani_proofs {
use super::*;
#[kani::proof]
fn proof_label_target_roundtrip() {
let type_selector: u8 = kani::any();
let depth: u8 = kani::any();
let offset_raw: i32 = kani::any();
kani::assume(offset_raw >= -(1 << 21) && offset_raw < (1 << 21));
let result_type = match type_selector % 5 {
0 => ResultType(None),
1 => ResultType(Some(ValType::I32)),
2 => ResultType(Some(ValType::I64)),
3 => ResultType(Some(ValType::F32)),
4 => ResultType(Some(ValType::F64)),
_ => unreachable!(),
};
let offset = JumpOffset(offset_raw);
let label = LabelTarget::new(result_type, depth, offset);
let decoded_arity = label.arity();
let decoded_depth = label.depth();
let decoded_offset = label.jump();
let expected_arity = match result_type.0 {
None => LabelArity::None,
Some(ValType::I32 | ValType::F32) => LabelArity::I32,
Some(ValType::I64 | ValType::F64) => LabelArity::I64,
};
assert_eq!(decoded_arity, expected_arity, "Arity must decode correctly");
assert_eq!(decoded_depth, depth, "Depth must decode correctly");
assert_eq!(
decoded_offset.0, offset.0,
"Offset must decode correctly with proper sign extension"
);
}
#[kani::proof]
fn proof_jump_offset_validation() {
let current: u32 = kani::any();
let to: u32 = kani::any();
kani::assume(current < (1 << 30));
kani::assume(to < (1 << 30));
let current_target = JumpTarget(current);
let to_target = JumpTarget(to);
let result = JumpOffset::new(current_target, to_target);
let offset = (to as i32).wrapping_sub(current as i32);
let fits_in_22_bits = offset == ((offset << 10) >> 10);
if fits_in_22_bits {
assert!(
result.is_ok(),
"Offset within 22-bit range should be accepted"
);
if let Ok(jump_offset) = result {
assert_eq!(
jump_offset.0, offset,
"Accepted offset should match computed value"
);
}
} else {
assert!(
result.is_err(),
"Offset outside 22-bit range should be rejected"
);
}
}
#[kani::proof]
fn proof_jump_target_addition() {
let pc: u32 = kani::any();
let offset_raw: i32 = kani::any();
kani::assume(pc < (1 << 30));
kani::assume(offset_raw >= -(1 << 21) && offset_raw < (1 << 21));
let target = JumpTarget(pc);
let offset = JumpOffset(offset_raw);
let result = target + offset;
let expected = ((pc as i32).wrapping_add(offset_raw)) as u32;
assert_eq!(
result.0, expected,
"JumpTarget + JumpOffset should compute correct address"
);
}
#[kani::proof]
fn proof_jump_offset_sentinel() {
let current: u32 = kani::any();
kani::assume(current < (1 << 30));
let current_target = JumpTarget(current);
let sentinel = JumpTarget::SENTINEL;
let result = JumpOffset::new(current_target, sentinel);
assert!(
result.is_ok(),
"Creating offset to SENTINEL should always succeed"
);
if let Ok(offset) = result {
assert_eq!(
offset,
JumpOffset::sentinel(),
"Offset to SENTINEL should be sentinel"
);
assert_eq!(offset.0, 0, "Sentinel offset should be 0");
}
}
}