extern crate alloc;
use alloc::string::String;
use alloc::vec::Vec;
use crate::bytecode::{BlockType, Chunk, ConstValue, Module, Op};
#[derive(Debug, Clone)]
pub struct VerifyError {
pub chunk_name: String,
pub message: String,
}
#[derive(Debug, Clone, Copy)]
enum BlockKind {
If,
Loop,
}
fn analyze_yield_coverage(
ops: &[Op],
start: usize,
end: usize,
initial: bool,
break_states: &mut Vec<bool>,
) -> Option<bool> {
let mut has_yielded = initial;
let mut ip = start;
let end = end.min(ops.len());
while ip < end {
match &ops[ip] {
Op::Yield => {
has_yielded = true;
ip += 1;
}
Op::Break(_) => {
break_states.push(has_yielded);
return None;
}
Op::BreakIf(_) => {
break_states.push(has_yielded);
ip += 1;
}
Op::If(target) => {
let target = *target as usize;
if target > 0 && target <= ops.len() && matches!(&ops[target - 1], Op::Else(_)) {
let endif_pos = if let Op::Else(e) = &ops[target - 1] {
*e as usize
} else {
unreachable!()
};
let then_result =
analyze_yield_coverage(ops, ip + 1, target - 1, has_yielded, break_states);
let else_result =
analyze_yield_coverage(ops, target, endif_pos, has_yielded, break_states);
match (then_result, else_result) {
(Some(a), Some(b)) => has_yielded = a && b,
(Some(a), None) => has_yielded = a,
(None, Some(b)) => has_yielded = b,
(None, None) => return None,
}
ip = endif_pos + 1;
} else {
let then_result =
analyze_yield_coverage(ops, ip + 1, target, has_yielded, break_states);
match then_result {
Some(a) => has_yielded = a && has_yielded,
None => {
}
}
ip = target + 1;
}
}
Op::Loop(target) => {
let loop_exit_target = *target as usize;
let endloop_ip = loop_exit_target.saturating_sub(1);
let mut loop_breaks: Vec<bool> = Vec::new();
let _body_result =
analyze_yield_coverage(ops, ip + 1, endloop_ip, has_yielded, &mut loop_breaks);
if loop_breaks.is_empty() {
return None;
}
has_yielded = loop_breaks.iter().all(|&b| b);
ip = loop_exit_target;
}
Op::Else(_) | Op::EndIf | Op::EndLoop(_) => {
ip += 1;
}
_ => {
ip += 1;
}
}
}
Some(has_yielded)
}
fn loop_body_all_paths_yield_no_inner_loop(ops: &[Op], start: usize, end: usize) -> bool {
let start = start.min(ops.len());
let end = end.clamp(start, ops.len());
if ops[start..end].iter().any(|op| matches!(op, Op::Loop(_))) {
return false;
}
let mut break_states: Vec<bool> = Vec::new();
matches!(
analyze_yield_coverage(ops, start, end, false, &mut break_states),
Some(true)
)
}
fn op_wcet_cycles(
chunk: &Chunk,
ip: usize,
cost_model: &crate::bytecode::CostModel,
wcet_extra: &[u32],
) -> Result<u32, VerifyError> {
let extra = wcet_extra.get(ip).copied().unwrap_or(0);
if extra == u32::MAX {
return Err(VerifyError {
chunk_name: chunk.name.clone(),
message: alloc::format!(
"text operation at instruction {} runs in time proportional to an \
unbounded-length string; WCET cannot be statically bounded",
ip
),
});
}
Ok(cost_model.cycles(&chunk.ops[ip]).saturating_add(extra))
}
fn wcet_region(
chunk: &Chunk,
start: usize,
end: usize,
break_costs: &mut Vec<u32>,
cost_model: &crate::bytecode::CostModel,
clamp_productive_yield_loops: bool,
wcet_extra: &[u32],
) -> Result<Option<u32>, VerifyError> {
let ops = &chunk.ops;
let mut cost: u32 = 0;
let mut ip = start;
let end = end.min(ops.len());
while ip < end {
match &ops[ip] {
Op::Break(_) => {
cost = cost.saturating_add(op_wcet_cycles(chunk, ip, cost_model, wcet_extra)?);
break_costs.push(cost);
return Ok(None);
}
Op::Trap(_) => {
cost = cost.saturating_add(op_wcet_cycles(chunk, ip, cost_model, wcet_extra)?);
let _ = cost;
return Ok(None);
}
Op::BreakIf(_) => {
cost = cost.saturating_add(op_wcet_cycles(chunk, ip, cost_model, wcet_extra)?);
break_costs.push(cost);
ip += 1;
}
Op::If(target) => {
cost = cost.saturating_add(op_wcet_cycles(chunk, ip, cost_model, wcet_extra)?);
let target = *target as usize;
if target > 0 && target <= ops.len() && matches!(&ops[target - 1], Op::Else(_)) {
let endif_pos = if let Op::Else(e) = &ops[target - 1] {
*e as usize
} else {
unreachable!()
};
let then_cost = wcet_region(
chunk,
ip + 1,
target - 1,
break_costs,
cost_model,
clamp_productive_yield_loops,
wcet_extra,
)?;
let else_cost = wcet_region(
chunk,
target,
endif_pos,
break_costs,
cost_model,
clamp_productive_yield_loops,
wcet_extra,
)?;
let branch_cost = match (then_cost, else_cost) {
(Some(a), Some(b)) => Some(if a > b { a } else { b }),
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(None, None) => return Ok(None),
};
cost += branch_cost.unwrap_or(0);
ip = endif_pos + 1;
} else {
let then_cost = wcet_region(
chunk,
ip + 1,
target,
break_costs,
cost_model,
clamp_productive_yield_loops,
wcet_extra,
)?;
match then_cost {
Some(c) => cost += c,
None => {
}
}
ip = target + 1;
}
}
Op::Loop(target) => {
cost = cost.saturating_add(op_wcet_cycles(chunk, ip, cost_model, wcet_extra)?);
let loop_exit_target = *target as usize;
let endloop_ip = loop_exit_target.saturating_sub(1);
let mut loop_break_costs: Vec<u32> = Vec::new();
let body_cost = wcet_region(
chunk,
ip + 1,
endloop_ip,
&mut loop_break_costs,
cost_model,
clamp_productive_yield_loops,
wcet_extra,
)?;
if loop_break_costs.is_empty() && body_cost.is_none() {
return Ok(None);
}
let iter_count = if body_cost.is_none()
|| (clamp_productive_yield_loops
&& loop_body_all_paths_yield_no_inner_loop(ops, ip + 1, endloop_ip))
{
1
} else {
match extract_loop_iteration_bound(chunk, ip) {
Some(n) => n,
Option::None => {
return Err(VerifyError {
chunk_name: chunk.name.clone(),
message: alloc::format!(
"loop at instruction {} has no statically extractable \
iteration bound; strict mode requires loops with \
fall-through bodies to match the canonical for-range \
pattern",
ip
),
});
}
}
};
let body_cost_total = body_cost.unwrap_or(0).saturating_mul(iter_count);
let max_break = loop_break_costs.iter().copied().max().unwrap_or(0);
cost += if max_break > body_cost_total {
max_break
} else {
body_cost_total
};
ip = loop_exit_target;
}
Op::Else(_) | Op::EndIf | Op::EndLoop(_) => {
ip += 1;
}
_ => {
cost = cost.saturating_add(op_wcet_cycles(chunk, ip, cost_model, wcet_extra)?);
ip += 1;
}
}
}
Ok(Some(cost))
}
fn extract_loop_iteration_bound(chunk: &Chunk, loop_ip: usize) -> Option<u32> {
let ops = &chunk.ops;
if loop_ip + 4 >= ops.len() {
return None;
}
let var_slot = match &ops[loop_ip + 1] {
Op::GetLocal(s) => *s,
_ => return None,
};
let end_slot = match &ops[loop_ip + 2] {
Op::GetLocal(s) => *s,
_ => return None,
};
if !matches!(&ops[loop_ip + 3], Op::CmpGe) {
return None;
}
if !matches!(&ops[loop_ip + 4], Op::BreakIf(_)) {
return None;
}
let end_val = trace_const_set_local(chunk, loop_ip, end_slot)?;
let start_val = trace_const_set_local(chunk, loop_ip, var_slot)?;
if !loop_body_advances_induction(chunk, loop_ip, var_slot, end_slot) {
return None;
}
if end_val >= start_val {
let count = (end_val - start_val) as u64;
if count > u32::MAX as u64 {
None
} else {
Some(count as u32)
}
} else {
Some(0)
}
}
fn loop_body_advances_induction(
chunk: &Chunk,
loop_ip: usize,
var_slot: u16,
end_slot: u16,
) -> bool {
let ops = &chunk.ops;
let Some(Op::Loop(exit)) = ops.get(loop_ip) else {
return false;
};
let exit = *exit as usize;
if exit == 0 || exit > ops.len() {
return false;
}
let endloop = exit - 1;
if !matches!(ops.get(endloop), Some(Op::EndLoop(_))) {
return false;
}
let body = loop_ip + 5;
if endloop < 5 || endloop - 5 < body {
return false;
}
let mut var_sets = 0u32;
for op in &ops[body..endloop] {
if let Op::SetLocal(s) = op {
if *s == end_slot {
return false;
}
if *s == var_slot {
var_sets += 1;
}
}
}
if var_sets != 1 {
return false;
}
match &ops[endloop - 5..endloop] {
[
Op::GetLocal(g),
Op::Const(c),
Op::CheckedAdd,
Op::PopN(2),
Op::SetLocal(s),
] if *g == var_slot && *s == var_slot => {
matches!(chunk.constants.get(*c as usize), Some(ConstValue::Int(n)) if *n > 0)
}
_ => false,
}
}
fn trace_const_set_local(chunk: &Chunk, before_ip: usize, slot: u16) -> Option<i64> {
let ops = &chunk.ops;
let mut ip = before_ip;
while ip > 0 {
ip -= 1;
if let Op::SetLocal(s) = &ops[ip]
&& *s == slot
{
if ip == 0 {
return None;
}
if let Op::Const(idx) = &ops[ip - 1]
&& let Some(ConstValue::Int(n)) = chunk.constants.get(*idx as usize)
{
return Some(*n);
}
if ip >= 2
&& matches!(&ops[ip - 1], Op::Len)
&& let Op::GetLocal(arr_slot) = &ops[ip - 2]
{
return trace_literal_array_length(chunk, ip - 2, *arr_slot);
}
return None;
}
}
None
}
fn trace_literal_array_length(chunk: &Chunk, before_ip: usize, arr_slot: u16) -> Option<i64> {
let ops = &chunk.ops;
let mut ip = before_ip;
while ip > 0 {
ip -= 1;
if let Op::SetLocal(s) = &ops[ip]
&& *s == arr_slot
{
if ip == 0 {
return None;
}
if let Op::NewComposite(o) = &ops[ip - 1]
&& o.kind() == crate::value_layout::CompositeKind::Array
{
return Some(o.count() as i64);
}
if let Op::GetLocal(other_slot) = &ops[ip - 1] {
return trace_literal_array_length(chunk, ip - 1, *other_slot);
}
return None;
}
}
None
}
#[derive(Debug, Clone, Copy)]
struct McuResult {
peak_above_initial: u32,
delta: i32,
heap_total: u32,
}
impl McuResult {
fn empty() -> Self {
Self {
peak_above_initial: 0,
delta: 0,
heap_total: 0,
}
}
}
struct CallResolver<'a> {
chunk_wcmu: &'a [Option<(u32, u32)>],
native_wcmu: &'a [u32],
shared_layout: &'a [crate::bytecode::SharedSlotLayout],
}
impl<'a> CallResolver<'a> {
fn empty() -> Self {
Self {
chunk_wcmu: &[],
native_wcmu: &[],
shared_layout: &[],
}
}
fn resolve_chunk(&self, idx: u16) -> (u32, u32) {
self.chunk_wcmu
.get(idx as usize)
.and_then(|o| *o)
.unwrap_or((0, 0))
}
fn resolve_native(&self, idx: u16) -> u32 {
self.native_wcmu.get(idx as usize).copied().unwrap_or(0)
}
}
fn op_iteration_heap(op: &Op, chunk: &Chunk, resolver: &CallResolver) -> u32 {
op.heap_alloc(chunk)
.saturating_add(shared_composite_copyout_bytes(op, resolver.shared_layout))
}
fn shared_composite_copyout_bytes(
op: &Op,
shared_layout: &[crate::bytecode::SharedSlotLayout],
) -> u32 {
let slot_copyout = |slot: usize| -> u32 {
shared_layout
.get(slot)
.filter(|e| e.kind & crate::bytecode::SHARED_SLOT_COMPOSITE_FLAG != 0)
.map_or(0, |e| e.len as u32)
};
match op {
Op::GetData(slot) => slot_copyout(*slot as usize),
Op::GetDataIndexed(base, len) => (0..*len as usize)
.map(|i| slot_copyout(*base as usize + i))
.max()
.unwrap_or(0),
_ => 0,
}
}
fn wcmu_region(
chunk: &Chunk,
start: usize,
end: usize,
break_results: &mut Vec<McuResult>,
resolver: &CallResolver,
value_slot_bytes: u32,
) -> Result<Option<McuResult>, VerifyError> {
let ops = &chunk.ops;
let mut current_offset: i32 = 0;
let mut peak: u32 = 0;
let mut heap: u32 = 0;
let mut ip = start;
let end = end.min(ops.len());
while ip < end {
let op = &ops[ip];
match op {
Op::Break(_) => {
let shrink = op.stack_shrink() as i32;
let growth = op.stack_growth() as i32;
let during_peak = (current_offset + growth).max(0) as u32;
peak = peak.max(during_peak);
heap = heap.saturating_add(op_iteration_heap(op, chunk, resolver));
current_offset += growth - shrink;
break_results.push(McuResult {
peak_above_initial: peak,
delta: current_offset,
heap_total: heap,
});
return Ok(None);
}
Op::Trap(_) => {
let shrink = op.stack_shrink() as i32;
let growth = op.stack_growth() as i32;
let during_peak = (current_offset + growth).max(0) as u32;
peak = peak.max(during_peak);
heap = heap.saturating_add(op_iteration_heap(op, chunk, resolver));
current_offset += growth - shrink;
let _ = current_offset;
let _ = peak;
let _ = heap;
return Ok(None);
}
Op::BreakIf(_) => {
let shrink = op.stack_shrink() as i32;
let growth = op.stack_growth() as i32;
let during_peak = (current_offset + growth).max(0) as u32;
peak = peak.max(during_peak);
heap = heap.saturating_add(op_iteration_heap(op, chunk, resolver));
current_offset += growth - shrink;
break_results.push(McuResult {
peak_above_initial: peak,
delta: current_offset,
heap_total: heap,
});
ip += 1;
}
Op::If(target) => {
let shrink = op.stack_shrink() as i32;
let growth = op.stack_growth() as i32;
let during_peak = (current_offset + growth).max(0) as u32;
peak = peak.max(during_peak);
heap = heap.saturating_add(op_iteration_heap(op, chunk, resolver));
current_offset += growth - shrink;
let target = *target as usize;
if target > 0 && target <= ops.len() && matches!(&ops[target - 1], Op::Else(_)) {
let endif_pos = if let Op::Else(e) = &ops[target - 1] {
*e as usize
} else {
unreachable!()
};
let then_branch = wcmu_subregion(
chunk,
ip + 1,
target - 1,
current_offset,
break_results,
resolver,
value_slot_bytes,
)?;
let else_branch = wcmu_subregion(
chunk,
target,
endif_pos,
current_offset,
break_results,
resolver,
value_slot_bytes,
)?;
match (then_branch, else_branch) {
(Some(a), Some(b)) => {
peak = peak.max(a.peak_above_initial).max(b.peak_above_initial);
heap = heap.saturating_add(a.heap_total.max(b.heap_total));
current_offset = a.delta.max(b.delta);
}
(Some(a), None) => {
peak = peak.max(a.peak_above_initial);
heap = heap.saturating_add(a.heap_total);
current_offset = a.delta;
}
(None, Some(b)) => {
peak = peak.max(b.peak_above_initial);
heap = heap.saturating_add(b.heap_total);
current_offset = b.delta;
}
(None, None) => {
return Ok(None);
}
}
ip = endif_pos + 1;
} else {
let then_branch = wcmu_subregion(
chunk,
ip + 1,
target,
current_offset,
break_results,
resolver,
value_slot_bytes,
)?;
if let Some(a) = then_branch {
peak = peak.max(a.peak_above_initial);
heap = heap.saturating_add(a.heap_total);
current_offset = current_offset.max(a.delta);
}
ip = target + 1;
}
}
Op::Loop(target) => {
let shrink = op.stack_shrink() as i32;
let growth = op.stack_growth() as i32;
let during_peak = (current_offset + growth).max(0) as u32;
peak = peak.max(during_peak);
heap = heap.saturating_add(op_iteration_heap(op, chunk, resolver));
current_offset += growth - shrink;
let loop_exit_target = *target as usize;
let endloop_ip = loop_exit_target.saturating_sub(1);
let mut loop_breaks: Vec<McuResult> = Vec::new();
let body = wcmu_subregion(
chunk,
ip + 1,
endloop_ip,
current_offset,
&mut loop_breaks,
resolver,
value_slot_bytes,
)?;
let body_peak = body.as_ref().map_or(0, |r| r.peak_above_initial);
let body_heap_one = body.as_ref().map_or(0, |r| r.heap_total);
let iter_count = if body.is_none() {
1
} else {
match extract_loop_iteration_bound(chunk, ip) {
Some(n) => n,
Option::None => {
return Err(VerifyError {
chunk_name: chunk.name.clone(),
message: alloc::format!(
"loop at instruction {} has no statically extractable \
iteration bound; strict mode requires loops with \
fall-through bodies to match the canonical for-range \
pattern",
ip
),
});
}
}
};
let body_heap = body_heap_one.saturating_mul(iter_count);
let break_peak = loop_breaks
.iter()
.map(|r| r.peak_above_initial)
.max()
.unwrap_or(0);
let break_heap = loop_breaks.iter().map(|r| r.heap_total).max().unwrap_or(0);
peak = peak.max(body_peak).max(break_peak);
heap = heap.saturating_add(body_heap.max(break_heap));
if loop_breaks.is_empty() && body.is_none() {
return Ok(None);
}
ip = loop_exit_target;
}
Op::Call(callee_idx, n_args) => {
let (callee_stack_bytes, callee_heap_bytes) = resolver.resolve_chunk(*callee_idx);
let callee_stack_slots = (callee_stack_bytes / value_slot_bytes) as i32;
let n = *n_args as i32;
let during_peak = (current_offset + callee_stack_slots - n)
.max(current_offset + 1)
.max(0) as u32;
peak = peak.max(during_peak);
heap = heap.saturating_add(callee_heap_bytes);
current_offset += 1 - n;
ip += 1;
}
Op::CallVerifiedNative(native_idx, n_args)
| Op::CallExternalNative(native_idx, n_args) => {
let native_heap = resolver.resolve_native(*native_idx);
let n = *n_args as i32;
let during_peak = (current_offset + 1).max(0) as u32;
peak = peak.max(during_peak);
heap = heap.saturating_add(native_heap);
current_offset += 1 - n;
ip += 1;
}
Op::Else(_) | Op::EndIf | Op::EndLoop(_) => {
ip += 1;
}
_ => {
let shrink = op.stack_shrink() as i32;
let growth = op.stack_growth() as i32;
let during_peak = (current_offset + growth).max(0) as u32;
peak = peak.max(during_peak);
heap = heap.saturating_add(op_iteration_heap(op, chunk, resolver));
current_offset += growth - shrink;
ip += 1;
}
}
}
Ok(Some(McuResult {
peak_above_initial: peak,
delta: current_offset,
heap_total: heap,
}))
}
fn wcmu_subregion(
chunk: &Chunk,
start: usize,
end: usize,
offset_at_start: i32,
break_results: &mut Vec<McuResult>,
resolver: &CallResolver,
value_slot_bytes: u32,
) -> Result<Option<McuResult>, VerifyError> {
let mut sub_breaks: Vec<McuResult> = Vec::new();
let result = wcmu_region(
chunk,
start,
end,
&mut sub_breaks,
resolver,
value_slot_bytes,
)?;
for b in sub_breaks {
break_results.push(McuResult {
peak_above_initial: (offset_at_start.max(0) as u32) + b.peak_above_initial,
delta: offset_at_start + b.delta,
heap_total: b.heap_total,
});
}
Ok(result.map(|r| McuResult {
peak_above_initial: (offset_at_start.max(0) as u32) + r.peak_above_initial,
delta: offset_at_start + r.delta,
heap_total: r.heap_total,
}))
}
pub fn wcmu_stream_iteration(chunk: &Chunk) -> Result<(u32, u32), VerifyError> {
wcmu_stream_iteration_with_value_slot_bytes(chunk, crate::bytecode::VALUE_SLOT_SIZE_BYTES)
}
pub fn wcmu_stream_iteration_with_value_slot_bytes(
chunk: &Chunk,
value_slot_bytes: u32,
) -> Result<(u32, u32), VerifyError> {
if chunk.block_type != BlockType::Stream {
return Err(VerifyError {
chunk_name: chunk.name.clone(),
message: String::from("wcmu_stream_iteration requires a Stream block"),
});
}
let ops = &chunk.ops;
let stream_pos = ops
.iter()
.position(|op| matches!(op, Op::Stream))
.ok_or_else(|| VerifyError {
chunk_name: chunk.name.clone(),
message: String::from("Stream block missing Stream instruction"),
})?;
let reset_pos = ops
.iter()
.position(|op| matches!(op, Op::Reset))
.ok_or_else(|| VerifyError {
chunk_name: chunk.name.clone(),
message: String::from("Stream block missing Reset instruction"),
})?;
let mut breaks: Vec<McuResult> = Vec::new();
let resolver = CallResolver::empty();
let body = wcmu_region(
chunk,
stream_pos + 1,
reset_pos,
&mut breaks,
&resolver,
value_slot_bytes,
)?
.unwrap_or(McuResult::empty());
let body_peak = body.peak_above_initial;
let body_heap = body.heap_total;
let stack_slots = chunk.local_count as u32 + body_peak;
let stack_bytes = stack_slots * value_slot_bytes;
Ok((stack_bytes, body_heap))
}
fn chunk_wcet_extra(
chunk: &Chunk,
cost_model: &crate::bytecode::CostModel,
native_bounds: &[NativeIterationBound],
) -> Vec<u32> {
let mut extra = crate::text_size::chunk_text_wcet_cycles(chunk, cost_model.text_byte_cycles);
if !native_bounds.is_empty() {
for (ip, op) in chunk.ops.iter().enumerate() {
if let Op::CallVerifiedNative(idx, _) = op
&& let Some(b) = native_bounds.get(*idx as usize)
{
extra[ip] = extra[ip].saturating_add(b.per_call_wcet_cycles);
}
}
}
extra
}
fn external_native_wcet(chunk: &Chunk, native_bounds: &[NativeIterationBound]) -> u32 {
let mut seen: alloc::collections::BTreeSet<u16> = alloc::collections::BTreeSet::new();
let mut total: u32 = 0;
for op in &chunk.ops {
if let Op::CallExternalNative(idx, _) = op
&& seen.insert(*idx)
&& let Some(b) = native_bounds.get(*idx as usize)
&& let Some(max_inv) = b.max_invocations
{
total = total.saturating_add(b.per_call_wcet_cycles.saturating_mul(max_inv));
}
}
total
}
pub fn wcet_stream_iteration(chunk: &Chunk) -> Result<u32, VerifyError> {
wcet_stream_iteration_with_cost_model(chunk, &crate::bytecode::NOMINAL_COST_MODEL, &[])
}
pub fn wcet_stream_iteration_with_cost_model(
chunk: &Chunk,
cost_model: &crate::bytecode::CostModel,
native_bounds: &[NativeIterationBound],
) -> Result<u32, VerifyError> {
if chunk.block_type != BlockType::Stream {
return Err(VerifyError {
chunk_name: chunk.name.clone(),
message: String::from("wcet_stream_iteration requires a Stream block"),
});
}
let ops = &chunk.ops;
let stream_pos = ops
.iter()
.position(|op| matches!(op, Op::Stream))
.ok_or_else(|| VerifyError {
chunk_name: chunk.name.clone(),
message: String::from("Stream block missing Stream instruction"),
})?;
let reset_pos = ops
.iter()
.position(|op| matches!(op, Op::Reset))
.ok_or_else(|| VerifyError {
chunk_name: chunk.name.clone(),
message: String::from("Stream block missing Reset instruction"),
})?;
let mut break_costs: Vec<u32> = Vec::new();
let wcet_extra = chunk_wcet_extra(chunk, cost_model, native_bounds);
let body_cost = wcet_region(
chunk,
stream_pos + 1,
reset_pos,
&mut break_costs,
cost_model,
false,
&wcet_extra,
)?;
let overhead = cost_model.cycles(&ops[stream_pos]) + cost_model.cycles(&ops[reset_pos]);
let region_cost = body_cost.unwrap_or(0);
Ok(overhead
.saturating_add(region_cost)
.saturating_add(external_native_wcet(chunk, native_bounds)))
}
pub fn wcet_whole_chunk(chunk: &Chunk) -> Result<u32, VerifyError> {
wcet_whole_chunk_with_cost_model(chunk, &crate::bytecode::NOMINAL_COST_MODEL, &[])
}
pub fn wcet_whole_chunk_with_cost_model(
chunk: &Chunk,
cost_model: &crate::bytecode::CostModel,
native_bounds: &[NativeIterationBound],
) -> Result<u32, VerifyError> {
if chunk.block_type == BlockType::Stream {
return Err(VerifyError {
chunk_name: chunk.name.clone(),
message: String::from("wcet_whole_chunk requires a non-Stream block"),
});
}
let external = external_native_wcet(chunk, native_bounds);
if chunk.block_type == BlockType::Reentrant
&& let Some(segmented) = reentrant_segmented_wcet(chunk, cost_model, native_bounds)?
{
return Ok(segmented.saturating_add(external));
}
let clamp = chunk.block_type == BlockType::Reentrant;
let mut break_costs: Vec<u32> = Vec::new();
let wcet_extra = chunk_wcet_extra(chunk, cost_model, native_bounds);
let body_cost = wcet_region(
chunk,
0,
chunk.ops.len(),
&mut break_costs,
cost_model,
clamp,
&wcet_extra,
)?;
Ok(body_cost.unwrap_or(0).saturating_add(external))
}
pub fn module_wcet_with_bounds(
module: &Module,
bounds: &[NativeIterationBound],
cost_model: &crate::bytecode::CostModel,
) -> Result<Vec<u32>, VerifyError> {
module
.chunks
.iter()
.map(|chunk| match chunk.block_type {
BlockType::Stream => wcet_stream_iteration_with_cost_model(chunk, cost_model, bounds),
_ => wcet_whole_chunk_with_cost_model(chunk, cost_model, bounds),
})
.collect()
}
fn reentrant_segmented_wcet(
chunk: &Chunk,
cost_model: &crate::bytecode::CostModel,
native_bounds: &[NativeIterationBound],
) -> Result<Option<u32>, VerifyError> {
let mut depth: i32 = 0;
let mut yields: Vec<usize> = Vec::new();
for (ip, op) in chunk.ops.iter().enumerate() {
match op {
Op::If(_) | Op::Loop(_) => depth += 1,
Op::EndIf | Op::EndLoop(_) => depth -= 1,
Op::Yield => {
if depth != 0 {
return Ok(None);
}
yields.push(ip);
}
_ => {}
}
}
if yields.is_empty() {
return Ok(None);
}
let len = chunk.ops.len();
let wcet_extra = chunk_wcet_extra(chunk, cost_model, native_bounds);
let mut max_cost: u32 = 0;
let mut seg_start: usize = 0;
for &y in &yields {
let mut break_costs: Vec<u32> = Vec::new();
let c = wcet_region(
chunk,
seg_start,
y + 1,
&mut break_costs,
cost_model,
false,
&wcet_extra,
)?
.unwrap_or(0);
max_cost = max_cost.max(c);
seg_start = y + 1;
}
let mut break_costs: Vec<u32> = Vec::new();
let c = wcet_region(
chunk,
seg_start,
len,
&mut break_costs,
cost_model,
false,
&wcet_extra,
)?
.unwrap_or(0);
max_cost = max_cost.max(c);
Ok(Some(max_cost))
}
pub fn wcmu_whole_chunk(chunk: &Chunk) -> Result<(u32, u32), VerifyError> {
wcmu_whole_chunk_with_value_slot_bytes(chunk, crate::bytecode::VALUE_SLOT_SIZE_BYTES)
}
pub fn wcmu_whole_chunk_with_value_slot_bytes(
chunk: &Chunk,
value_slot_bytes: u32,
) -> Result<(u32, u32), VerifyError> {
if chunk.block_type == BlockType::Stream {
return Err(VerifyError {
chunk_name: chunk.name.clone(),
message: String::from("wcmu_whole_chunk requires a non-Stream block"),
});
}
let mut breaks: Vec<McuResult> = Vec::new();
let resolver = CallResolver::empty();
let body = wcmu_region(
chunk,
0,
chunk.ops.len(),
&mut breaks,
&resolver,
value_slot_bytes,
)?
.unwrap_or(McuResult::empty());
let stack_slots = chunk.local_count as u32 + body.peak_above_initial;
let stack_bytes = stack_slots * value_slot_bytes;
Ok((stack_bytes, body.heap_total))
}
pub fn module_wcmu(module: &Module, native_wcmu: &[u32]) -> Result<Vec<(u32, u32)>, VerifyError> {
module_wcmu_with_value_slot_bytes(module, native_wcmu, crate::bytecode::VALUE_SLOT_SIZE_BYTES)
}
pub fn module_wcmu_with_value_slot_bytes(
module: &Module,
native_wcmu: &[u32],
value_slot_bytes: u32,
) -> Result<Vec<(u32, u32)>, VerifyError> {
let n = module.chunks.len();
let mut chunk_wcmu: Vec<Option<(u32, u32)>> = alloc::vec![None; n];
let mut chunk_returns_text: Vec<bool> = alloc::vec![false; n];
let order = topological_call_order(module)?;
for chunk_idx in order {
let chunk = &module.chunks[chunk_idx];
let resolver = CallResolver {
chunk_wcmu: &chunk_wcmu,
native_wcmu,
shared_layout: module
.data_layout
.as_ref()
.map_or(&[], |dl| &dl.shared_layout),
};
let (wcmu_result, returns_text) =
compute_chunk_wcmu(chunk, &resolver, &chunk_returns_text, value_slot_bytes)?;
chunk_wcmu[chunk_idx] = Some(wcmu_result);
chunk_returns_text[chunk_idx] = returns_text;
}
Ok(chunk_wcmu
.into_iter()
.map(|o| o.unwrap_or((0, 0)))
.collect())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct RuntimeFootprint {
pub max_operand_slots: u32,
pub max_frame_depth: u32,
pub max_heap_bytes: u32,
}
pub fn module_call_depth(module: &Module) -> Result<u32, VerifyError> {
let n = module.chunks.len();
let order = topological_call_order(module)?;
let mut depth = alloc::vec![1u32; n];
for idx in order {
let mut d = 1u32;
for op in &module.chunks[idx].ops {
if let Op::Call(callee, _) = op {
let c = *callee as usize;
if c < n {
d = d.max(depth[c].saturating_add(1));
}
}
}
depth[idx] = d;
}
Ok(depth.into_iter().max().unwrap_or(0))
}
pub fn module_runtime_footprint(
module: &Module,
native_wcmu: &[u32],
) -> Result<RuntimeFootprint, VerifyError> {
let per_chunk = module_wcmu_with_value_slot_bytes(module, native_wcmu, 1)?;
let mut max_operand_slots = 0u32;
let mut max_heap_bytes = 0u32;
for (stack_slots, heap_bytes) in per_chunk {
max_operand_slots = max_operand_slots.max(stack_slots);
max_heap_bytes = max_heap_bytes.max(heap_bytes);
}
let max_frame_depth = module_call_depth(module)?;
Ok(RuntimeFootprint {
max_operand_slots,
max_frame_depth,
max_heap_bytes,
})
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct NativeIterationBound {
pub per_call_wcmu_bytes: u32,
pub per_call_wcet_cycles: u32,
pub max_invocations: Option<u32>,
}
pub fn module_wcmu_with_bounds(
module: &Module,
bounds: &[NativeIterationBound],
value_slot_bytes: u32,
) -> Result<Vec<(u32, u32)>, VerifyError> {
let n = module.chunks.len();
let mut chunk_wcmu: Vec<Option<(u32, u32)>> = alloc::vec![None; n];
let mut chunk_returns_text: Vec<bool> = alloc::vec![false; n];
let per_site_native_wcmu: Vec<u32> = bounds
.iter()
.map(|b| match b.max_invocations {
Some(_) => 0,
None => b.per_call_wcmu_bytes,
})
.collect();
let order = topological_call_order(module)?;
for chunk_idx in order {
let chunk = &module.chunks[chunk_idx];
let resolver = CallResolver {
chunk_wcmu: &chunk_wcmu,
native_wcmu: &per_site_native_wcmu,
shared_layout: module
.data_layout
.as_ref()
.map_or(&[], |dl| &dl.shared_layout),
};
let (mut wcmu_result, returns_text) =
compute_chunk_wcmu(chunk, &resolver, &chunk_returns_text, value_slot_bytes)?;
let mut seen_external: alloc::collections::BTreeSet<u16> =
alloc::collections::BTreeSet::new();
for op in &chunk.ops {
if let Op::CallExternalNative(idx, _) = op
&& seen_external.insert(*idx)
&& let Some(bound) = bounds.get(*idx as usize)
&& let Some(max_inv) = bound.max_invocations
{
let contribution = bound.per_call_wcmu_bytes.saturating_mul(max_inv);
wcmu_result.1 = wcmu_result.1.saturating_add(contribution);
}
}
chunk_wcmu[chunk_idx] = Some(wcmu_result);
chunk_returns_text[chunk_idx] = returns_text;
}
Ok(chunk_wcmu
.into_iter()
.map(|o| o.unwrap_or((0, 0)))
.collect())
}
pub fn module_chunk_text_analyses(
module: &Module,
) -> Result<Vec<crate::text_size::ChunkTextAnalysis>, VerifyError> {
let n = module.chunks.len();
let mut chunk_returns_text: Vec<bool> = alloc::vec![false; n];
let mut analyses: Vec<crate::text_size::ChunkTextAnalysis> = alloc::vec![
crate::text_size::ChunkTextAnalysis {
heap_alloc: 0,
returns_text: false,
yields_text: false,
};
n
];
let order = topological_call_order(module)?;
for chunk_idx in order {
let chunk = &module.chunks[chunk_idx];
let analysis = crate::text_size::analyze_chunk_text(chunk, &chunk_returns_text);
chunk_returns_text[chunk_idx] = analysis.returns_text;
analyses[chunk_idx] = analysis;
}
Ok(analyses)
}
fn topological_call_order(module: &Module) -> Result<Vec<usize>, VerifyError> {
let n = module.chunks.len();
let mut visited = alloc::vec![false; n];
let mut on_stack = alloc::vec![false; n];
let mut order = Vec::new();
for i in 0..n {
if !visited[i] {
topo_visit(module, i, &mut visited, &mut on_stack, &mut order)?;
}
}
Ok(order)
}
fn topo_visit(
module: &Module,
idx: usize,
visited: &mut [bool],
on_stack: &mut [bool],
order: &mut Vec<usize>,
) -> Result<(), VerifyError> {
if on_stack[idx] {
return Err(VerifyError {
chunk_name: module.chunks[idx].name.clone(),
message: String::from("recursive call detected during WCMU topological sort"),
});
}
if visited[idx] {
return Ok(());
}
on_stack[idx] = true;
for op in &module.chunks[idx].ops {
if let Op::Call(callee, _) = op {
let callee_idx = *callee as usize;
if callee_idx < module.chunks.len() {
topo_visit(module, callee_idx, visited, on_stack, order)?;
}
}
}
on_stack[idx] = false;
visited[idx] = true;
order.push(idx);
Ok(())
}
fn compute_chunk_wcmu(
chunk: &Chunk,
resolver: &CallResolver,
chunk_returns_text: &[bool],
value_slot_bytes: u32,
) -> Result<((u32, u32), bool), VerifyError> {
let (start, end) = match chunk.block_type {
BlockType::Stream => {
let stream_pos = chunk
.ops
.iter()
.position(|op| matches!(op, Op::Stream))
.ok_or_else(|| VerifyError {
chunk_name: chunk.name.clone(),
message: String::from("Stream block missing Stream instruction"),
})?;
let reset_pos = chunk
.ops
.iter()
.position(|op| matches!(op, Op::Reset))
.ok_or_else(|| VerifyError {
chunk_name: chunk.name.clone(),
message: String::from("Stream block missing Reset instruction"),
})?;
(stream_pos + 1, reset_pos)
}
BlockType::Func | BlockType::Reentrant => (0, chunk.ops.len()),
};
let mut breaks: Vec<McuResult> = Vec::new();
let body = wcmu_region(chunk, start, end, &mut breaks, resolver, value_slot_bytes)?
.unwrap_or(McuResult::empty());
let stack_slots = chunk.local_count as u32 + body.peak_above_initial;
let stack_bytes = stack_slots * value_slot_bytes;
let text_analysis = crate::text_size::analyze_chunk_text(chunk, chunk_returns_text);
let heap_total = body.heap_total.saturating_add(text_analysis.heap_alloc);
Ok(((stack_bytes, heap_total), text_analysis.returns_text))
}
pub fn budget_for_stream(chunk: &Chunk) -> Result<keleusma_arena::Budget, VerifyError> {
let (stack_bytes, heap_bytes) = wcmu_stream_iteration(chunk)?;
Ok(keleusma_arena::Budget::new(
stack_bytes as usize,
heap_bytes as usize,
))
}
pub fn verify_resource_bounds(module: &Module, arena_capacity: usize) -> Result<(), VerifyError> {
verify_resource_bounds_with_natives(module, arena_capacity, &[])
}
pub fn verify_resource_bounds_with_cost_model(
module: &Module,
arena_capacity: usize,
cost_model: &crate::bytecode::CostModel,
native_wcmu: &[u32],
) -> Result<(), VerifyError> {
verify_resource_bounds_with_natives_and_value_slot_bytes(
module,
arena_capacity,
native_wcmu,
cost_model.value_slot_bytes,
)
}
pub fn verify_resource_bounds_with_natives(
module: &Module,
arena_capacity: usize,
native_wcmu: &[u32],
) -> Result<(), VerifyError> {
verify_resource_bounds_with_natives_and_value_slot_bytes(
module,
arena_capacity,
native_wcmu,
crate::bytecode::VALUE_SLOT_SIZE_BYTES,
)
}
pub fn verify_resource_bounds_with_natives_and_value_slot_bytes(
module: &Module,
arena_capacity: usize,
native_wcmu: &[u32],
value_slot_bytes: u32,
) -> Result<(), VerifyError> {
let chunk_wcmu = module_wcmu_with_value_slot_bytes(module, native_wcmu, value_slot_bytes)?;
enforce_arena_capacity(module, arena_capacity, &chunk_wcmu)
}
pub fn verify_resource_bounds_with_bounds(
module: &Module,
arena_capacity: usize,
bounds: &[NativeIterationBound],
value_slot_bytes: u32,
) -> Result<(), VerifyError> {
let chunk_wcmu = module_wcmu_with_bounds(module, bounds, value_slot_bytes)?;
enforce_arena_capacity(module, arena_capacity, &chunk_wcmu)
}
fn enforce_arena_capacity(
module: &Module,
arena_capacity: usize,
chunk_wcmu: &[(u32, u32)],
) -> Result<(), VerifyError> {
for (chunk_idx, chunk) in module.chunks.iter().enumerate() {
if chunk.block_type != BlockType::Stream {
continue;
}
let (stack_bytes, heap_bytes) = chunk_wcmu[chunk_idx];
let budget = keleusma_arena::Budget::new(stack_bytes as usize, heap_bytes as usize);
if budget.total() > arena_capacity {
return Err(VerifyError {
chunk_name: chunk.name.clone(),
message: alloc::format!(
"WCMU budget {} bytes (bottom {} + top {}) exceeds arena capacity {} bytes",
budget.total(),
budget.bottom_bytes,
budget.top_bytes,
arena_capacity
),
});
}
}
Ok(())
}
pub fn chunk_verification_witness(chunk: &Chunk) -> alloc::vec::Vec<&'static str> {
let mut checks = alloc::vec!["block-nesting-and-offsets", "block-type-constraints"];
if chunk.block_type == BlockType::Stream {
checks.push("productive-divergence");
}
checks
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VerificationObligation {
pub op_index: u32,
pub pass: &'static str,
pub property: &'static str,
}
pub fn chunk_verification_obligations(
chunk: &Chunk,
module: &Module,
) -> alloc::vec::Vec<VerificationObligation> {
let mut obligations: alloc::vec::Vec<VerificationObligation> = alloc::vec::Vec::new();
let _ = verify_chunk(chunk, module, Some(&mut obligations));
obligations
}
pub fn verify(module: &Module) -> Result<(), VerifyError> {
for chunk in &module.chunks {
verify_chunk(chunk, module, None)?;
verify_stack_depth(chunk)?;
}
let wb = (1usize << module.word_bits_log2) / 8;
let fb = (1usize << module.float_bits_log2) / 8;
crate::verify_typed::typed_check_module(module, wb, fb).map_err(|e| VerifyError {
chunk_name: alloc::string::String::from("<typed operand-stack pass>"),
message: alloc::format!("typed operand-stack verification failed: {e:?}"),
})?;
Ok(())
}
pub(crate) fn op_depth_effect(op: &Op, _chunk: &Chunk) -> (i32, i32) {
match op {
Op::Const(_) | Op::GetLocal(_) | Op::GetData(_) | Op::PushImmediate(_) => (0, 1),
Op::Dup => (1, 1),
Op::SetLocal(_) | Op::SetData(_) => (1, -1),
Op::GetDataIndexed(_, _) => (1, 0),
Op::SetDataIndexed(_, _) => (2, -2),
Op::BoundsCheck(_) => (1, 0),
Op::Add
| Op::Sub
| Op::Mul
| Op::Div
| Op::Mod
| Op::CmpEq
| Op::CmpNe
| Op::CmpLt
| Op::CmpGt
| Op::CmpLe
| Op::CmpGe
| Op::BitAnd
| Op::BitOr
| Op::BitXor
| Op::Shl
| Op::Shr
| Op::FixedMul(_)
| Op::FixedDiv(_)
| Op::GetIndex(_) => (2, -1),
Op::Neg
| Op::Not
| Op::IntToFloat
| Op::FloatToInt
| Op::WordToByte
| Op::ByteToWord
| Op::WordToFixed(_)
| Op::FixedToWord(_)
| Op::GetField(_)
| Op::GetTupleField(_)
| Op::GetEnumField(_)
| Op::Len => (1, 0),
Op::Yield => (1, 0),
Op::IsEnum(_, _, _) | Op::IsStruct(_) => (1, 1),
Op::Call(_, n) => (*n as i32, 1 - *n as i32),
Op::CallVerifiedNative(_, n) | Op::CallExternalNative(_, n) => {
let m = (*n & 0x7F) as i32;
let produced = if *n & 0x80 != 0 { 2 } else { 1 };
(m, produced - m)
}
Op::NewComposite(op) => {
let c = op.count() as i32;
(c, 1 - c)
}
Op::CheckedAdd
| Op::CheckedSub
| Op::CheckedMod
| Op::CheckedMul(_)
| Op::CheckedDiv(_) => (2, 1),
Op::CheckedNeg => (1, 2),
Op::PopN(n) => (*n as i32, -(*n as i32)),
Op::If(_) | Op::BreakIf(_) => (1, -1),
Op::Else(_)
| Op::EndIf
| Op::Loop(_)
| Op::EndLoop(_)
| Op::Break(_)
| Op::Stream
| Op::Reset
| Op::Trap(_)
| Op::Return => (0, 0),
}
}
fn depth_underflow(chunk: &Chunk, ip: usize, op: &Op, need: i32, have: i32) -> VerifyError {
VerifyError {
chunk_name: chunk.name.clone(),
message: alloc::format!(
"{:?} at {} requires {} operand(s) but only {} are on the stack; the operand stack would underflow",
op,
ip,
need,
have
),
}
}
fn verify_stack_depth(chunk: &Chunk) -> Result<(), VerifyError> {
let mut breaks: Vec<i32> = Vec::new();
verify_depth_region(chunk, 0, chunk.ops.len(), 0, &mut breaks).map(|_| ())
}
fn verify_depth_region(
chunk: &Chunk,
start: usize,
end: usize,
entry: i32,
breaks: &mut Vec<i32>,
) -> Result<Option<i32>, VerifyError> {
let ops = &chunk.ops;
let mut depth = entry;
let mut ip = start;
let end = end.min(ops.len());
while ip < end {
let op = &ops[ip];
match op {
Op::Trap(_) | Op::Return => return Ok(None),
Op::Break(_) => {
breaks.push(depth);
return Ok(None);
}
Op::BreakIf(_) => {
let (req, net) = op_depth_effect(op, chunk);
if depth < req {
return Err(depth_underflow(chunk, ip, op, req, depth));
}
depth += net;
breaks.push(depth);
ip += 1;
}
Op::If(target) => {
let (req, net) = op_depth_effect(op, chunk);
if depth < req {
return Err(depth_underflow(chunk, ip, op, req, depth));
}
depth += net;
let target = *target as usize;
if target > 0 && target <= ops.len() && matches!(&ops[target - 1], Op::Else(_)) {
let endif = match &ops[target - 1] {
Op::Else(e) => *e as usize,
_ => unreachable!(),
};
let then_end = verify_depth_region(chunk, ip + 1, target - 1, depth, breaks)?;
let else_end = verify_depth_region(chunk, target, endif, depth, breaks)?;
depth = match (then_end, else_end) {
(Some(a), Some(b)) => a.max(b),
(Some(a), None) => a,
(None, Some(b)) => b,
(None, None) => return Ok(None),
};
ip = endif + 1;
} else {
let then_end = verify_depth_region(chunk, ip + 1, target, depth, breaks)?;
if let Some(a) = then_end {
depth = depth.max(a);
}
ip = target + 1;
}
}
Op::Loop(target) => {
let exit = *target as usize;
let mut loop_breaks: Vec<i32> = Vec::new();
let body_end =
verify_depth_region(chunk, ip + 1, exit.saturating_sub(1), depth, &mut loop_breaks)?;
depth = loop_breaks
.iter()
.copied()
.max()
.or(body_end)
.unwrap_or(depth);
ip = exit;
}
_ => {
let (req, net) = op_depth_effect(op, chunk);
if depth < req {
return Err(depth_underflow(chunk, ip, op, req, depth));
}
depth += net;
ip += 1;
}
}
}
Ok(Some(depth))
}
fn verify_chunk(
chunk: &Chunk,
module: &Module,
mut sink: Option<&mut alloc::vec::Vec<VerificationObligation>>,
) -> Result<(), VerifyError> {
const P1: &str = "block-nesting-and-offsets";
const P2: &str = "block-type-constraints";
const P3: &str = "productive-divergence";
let mut record = |op_index: usize, pass: &'static str, property: &'static str| {
if let Some(s) = sink.as_mut() {
s.push(VerificationObligation {
op_index: op_index as u32,
pass,
property,
});
}
};
{
let name = &chunk.name;
let ops = &chunk.ops;
let mut block_stack: Vec<(BlockKind, usize)> = Vec::new();
let mut loop_depth: usize = 0;
let mut if_stack: Vec<(usize, usize, bool)> = Vec::new();
for (ip, op) in ops.iter().enumerate() {
match op {
Op::If(target) => {
let t = *target as usize;
if t > ops.len() {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"If at {} targets {} which is out of bounds (len={})",
ip,
t,
ops.len()
),
});
}
block_stack.push((BlockKind::If, ip));
if_stack.push((ip, t, false));
record(ip, P1, "if-branch-target-in-bounds");
}
Op::Else(target) => {
let t = *target as usize;
match block_stack.last() {
Some((BlockKind::If, _)) => {}
_ => {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"Else at {} without matching If on block stack",
ip
),
});
}
}
record(ip, P1, "else-preceded-by-matching-if");
match if_stack.last_mut() {
Some(frame) => {
let (if_ip, if_target, _) = *frame;
if if_target != ip + 1 {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"If at {} targets {} but its Else at {} requires the false branch to enter the else body at {}",
if_ip,
if_target,
ip,
ip + 1
),
});
}
frame.2 = true;
}
None => {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"Else at {} has no matching If on the control-flow stack",
ip
),
});
}
}
record(ip, P1, "if-target-is-else-body-start");
if t >= ops.len() {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"Else at {} targets {} which is out of bounds (len={})",
ip,
t,
ops.len()
),
});
}
record(ip, P1, "else-target-in-bounds");
if !matches!(&ops[t], Op::EndIf) {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"Else at {} targets {} which is {:?}, expected EndIf",
ip,
t,
&ops[t]
),
});
}
record(ip, P1, "else-target-is-endif");
}
Op::EndIf => {
match if_stack.pop() {
Some((if_ip, if_target, else_seen)) => {
if !else_seen && if_target != ip {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"If at {} has no Else so its false branch must target its EndIf at {}, but targets {}",
if_ip,
ip,
if_target
),
});
}
}
None => {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"EndIf at {} with no matching If on the control-flow stack",
ip
),
});
}
}
match block_stack.pop() {
Some((BlockKind::If, _)) => {}
Some((BlockKind::Loop, _)) => {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!("EndIf at {} but expected EndLoop", ip),
});
}
None => {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!("EndIf at {} with no matching If", ip),
});
}
}
record(ip, P1, "endif-closes-open-if");
}
Op::Loop(target) => {
let t = *target as usize;
if t > ops.len() {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"Loop at {} targets {} which is out of bounds (len={})",
ip,
t,
ops.len()
),
});
}
block_stack.push((BlockKind::Loop, ip));
loop_depth += 1;
record(ip, P1, "loop-exit-target-in-bounds");
}
Op::EndLoop(target) => {
let t = *target as usize;
match block_stack.pop() {
Some((BlockKind::Loop, loop_ip)) => {
record(ip, P1, "endloop-closes-open-loop");
if t != loop_ip + 1 {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"EndLoop at {} back-edge targets {} but Loop is at {} (expected {})",
ip,
t,
loop_ip,
loop_ip + 1
),
});
}
record(ip, P1, "endloop-back-edge-targets-loop-entry");
if let Op::Loop(exit) = &ops[loop_ip]
&& *exit as usize != ip + 1
{
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"Loop at {} exits to {} but its EndLoop is at {} (expected exit {})",
loop_ip,
exit,
ip,
ip + 1
),
});
}
record(ip, P1, "loop-exit-targets-after-endloop");
}
Some((BlockKind::If, _)) => {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!("EndLoop at {} but expected EndIf", ip),
});
}
None => {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!("EndLoop at {} with no matching Loop", ip),
});
}
}
loop_depth -= 1;
}
Op::Break(target) => {
if loop_depth == 0 {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!("Break at {} outside any Loop block", ip),
});
}
record(ip, P1, "break-within-loop");
let t = *target as usize;
if t > ops.len() {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"Break at {} targets {} which is out of bounds (len={})",
ip,
t,
ops.len()
),
});
}
record(ip, P1, "break-target-in-bounds");
let enclosing_loop_exit =
block_stack
.iter()
.rev()
.find_map(|(k, lip)| match (k, &ops[*lip]) {
(BlockKind::Loop, Op::Loop(exit)) => Some(*exit as usize),
_ => None,
});
if let Some(exit) = enclosing_loop_exit
&& t != exit
{
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"Break at {} targets {} but its enclosing loop exits to {}",
ip,
t,
exit
),
});
}
record(ip, P1, "break-targets-loop-exit");
}
Op::BreakIf(target) => {
if loop_depth == 0 {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!("BreakIf at {} outside any Loop block", ip),
});
}
record(ip, P1, "break-if-within-loop");
let t = *target as usize;
if t > ops.len() {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"BreakIf at {} targets {} which is out of bounds (len={})",
ip,
t,
ops.len()
),
});
}
record(ip, P1, "break-if-target-in-bounds");
let enclosing_loop_exit =
block_stack
.iter()
.rev()
.find_map(|(k, lip)| match (k, &ops[*lip]) {
(BlockKind::Loop, Op::Loop(exit)) => Some(*exit as usize),
_ => None,
});
if let Some(exit) = enclosing_loop_exit
&& t != exit
{
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"BreakIf at {} targets {} but its enclosing loop exits to {}",
ip,
t,
exit
),
});
}
record(ip, P1, "break-if-targets-loop-exit");
}
Op::GetData(slot) | Op::SetData(slot) => {
let idx = *slot as usize;
let data_len = module.data_layout.as_ref().map_or(0, |dl| dl.slots.len());
if data_len == 0 {
let op_name = if matches!(op, Op::GetData(_)) {
"GetData"
} else {
"SetData"
};
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"{} at {} but module has no data layout declared",
op_name,
ip
),
});
}
record(ip, P1, "data-slot-layout-declared");
if idx >= data_len {
let op_name = if matches!(op, Op::GetData(_)) {
"GetData"
} else {
"SetData"
};
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"{} at {} references slot {} but data layout has {} slot(s)",
op_name,
ip,
idx,
data_len
),
});
}
record(ip, P1, "data-slot-index-in-range");
}
Op::GetDataIndexed(base, len) | Op::SetDataIndexed(base, len) => {
let data_len = module.data_layout.as_ref().map_or(0, |dl| dl.slots.len());
let op_name = if matches!(op, Op::GetDataIndexed(_, _)) {
"GetDataIndexed"
} else {
"SetDataIndexed"
};
if data_len == 0 {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"{} at {} but module has no data layout declared",
op_name,
ip
),
});
}
record(ip, P1, "data-range-layout-declared");
let base_usize = *base as usize;
let len_usize = *len as usize;
let end = base_usize.saturating_add(len_usize);
if end > data_len {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"{} at {} references slot range [{}, {}) but data layout has {} slot(s)",
op_name,
ip,
base_usize,
end,
data_len
),
});
}
record(ip, P1, "data-range-in-bounds");
}
Op::Const(idx)
| Op::IsStruct(idx)
| Op::GetField(crate::bytecode::StructField::Boxed { name_const: idx }) => {
let len = chunk.constants.len();
if *idx as usize >= len {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"{:?} at {} references constant {} but the pool has {} entr(ies)",
op,
ip,
idx,
len
),
});
}
record(ip, P1, "constant-index-in-range");
}
Op::IsEnum(e, v, d) => {
let len = chunk.constants.len();
if *e as usize >= len || *v as usize >= len || *d as usize >= len {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"{:?} at {} references constants ({}, {}, {}) but the pool has {} entr(ies)",
op,
ip,
e,
v,
d,
len
),
});
}
record(ip, P1, "constant-index-in-range");
}
Op::Call(callee, arg_count) => {
let nchunks = module.chunks.len();
if *callee as usize >= nchunks {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"Call at {} targets chunk {} but the module has {} chunk(s)",
ip,
callee,
nchunks
),
});
}
let callee_locals = module.chunks[*callee as usize].local_count;
if *arg_count as u16 > callee_locals {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"Call at {} passes {} arguments but callee chunk {} declares only {} local slot(s)",
ip,
arg_count,
callee,
callee_locals
),
});
}
record(ip, P1, "call-target-and-arity-in-range");
}
Op::WordToFixed(fb)
| Op::FixedToWord(fb)
| Op::FixedMul(fb)
| Op::FixedDiv(fb)
| Op::CheckedMul(fb)
| Op::CheckedDiv(fb) => {
let word_bits = 1usize << module.word_bits_log2;
if (*fb as usize) >= word_bits {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"{:?} at {} declares {} fraction bits but the word is {} bits; a Q-format fraction count must be less than the word width",
op,
ip,
fb,
word_bits
),
});
}
record(ip, P1, "fixed-frac-bits-in-range");
}
Op::GetLocal(slot) | Op::SetLocal(slot) => {
let nlocals = chunk.local_count as usize;
if *slot as usize >= nlocals {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"{:?} at {} references local slot {} but the chunk declares {} local(s)",
op,
ip,
slot,
nlocals
),
});
}
record(ip, P1, "local-slot-in-range");
}
Op::NewComposite(crate::bytecode::NewCompositeOperand::Boxed {
kind:
crate::value_layout::CompositeKind::Struct
| crate::value_layout::CompositeKind::Enum,
meta,
..
}) => {
let len = chunk.struct_templates.len();
if *meta as usize >= len {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"NewComposite at {} references struct/enum template {} but the chunk has {} template(s)",
ip,
meta,
len
),
});
}
record(ip, P1, "struct-template-index-in-range");
}
_ => {}
}
}
if !block_stack.is_empty() {
let (kind, ip) = block_stack.last().unwrap();
let kind_str = match kind {
BlockKind::If => "If",
BlockKind::Loop => "Loop",
};
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!("unclosed {} block opened at {}", kind_str, ip),
});
}
record(0, P1, "all-blocks-closed");
let mut yield_count = 0usize;
let mut stream_count = 0usize;
let mut reset_count = 0usize;
for op in ops {
match op {
Op::Yield => yield_count += 1,
Op::Stream => stream_count += 1,
Op::Reset => reset_count += 1,
_ => {}
}
}
let first_yield = ops.iter().position(|op| matches!(op, Op::Yield));
let first_stream = ops.iter().position(|op| matches!(op, Op::Stream));
let first_reset = ops.iter().position(|op| matches!(op, Op::Reset));
match chunk.block_type {
BlockType::Func => {
if yield_count > 0 {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"Func block contains {} Yield instruction(s)",
yield_count
),
});
}
record(0, P2, "func-has-no-yield");
if stream_count > 0 {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"Func block contains {} Stream instruction(s)",
stream_count
),
});
}
record(0, P2, "func-has-no-stream");
if reset_count > 0 {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"Func block contains {} Reset instruction(s)",
reset_count
),
});
}
record(0, P2, "func-has-no-reset");
}
BlockType::Reentrant => {
if yield_count == 0 {
return Err(VerifyError {
chunk_name: name.clone(),
message: String::from("Reentrant block must contain at least one Yield"),
});
}
record(first_yield.unwrap_or(0), P2, "reentrant-has-yield");
if stream_count > 0 {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"Reentrant block contains {} Stream instruction(s)",
stream_count
),
});
}
record(0, P2, "reentrant-has-no-stream");
if reset_count > 0 {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"Reentrant block contains {} Reset instruction(s)",
reset_count
),
});
}
record(0, P2, "reentrant-has-no-reset");
}
BlockType::Stream => {
if stream_count != 1 {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"Stream block must contain exactly one Stream, found {}",
stream_count
),
});
}
record(
first_stream.unwrap_or(0),
P2,
"stream-has-exactly-one-stream",
);
if reset_count != 1 {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"Stream block must contain exactly one Reset, found {}",
reset_count
),
});
}
record(first_reset.unwrap_or(0), P2, "stream-has-exactly-one-reset");
if yield_count == 0 {
return Err(VerifyError {
chunk_name: name.clone(),
message: String::from("Stream block must contain at least one Yield"),
});
}
record(first_yield.unwrap_or(0), P2, "stream-has-yield");
}
}
if chunk.block_type == BlockType::Stream {
let stream_pos = ops.iter().position(|op| matches!(op, Op::Stream));
let reset_pos = ops.iter().position(|op| matches!(op, Op::Reset));
if let (Some(s), Some(r)) = (stream_pos, reset_pos) {
let mut break_states: Vec<bool> = Vec::new();
let result = analyze_yield_coverage(ops, s + 1, r, false, &mut break_states);
if let Some(false) = result {
return Err(VerifyError {
chunk_name: name.clone(),
message: String::from(
"productivity violation: some path from Stream to Reset \
does not pass through any Yield",
),
});
}
record(s, P3, "every-stream-to-reset-path-yields");
}
}
}
Ok(())
}
#[cfg(all(test, feature = "compile"))]
mod tests {
use super::*;
use crate::bytecode::{BlockType, Chunk, ConstValue, Module, Op};
use alloc::vec;
fn make_module(chunks: Vec<Chunk>) -> Module {
Module {
schema_hash: 0,
enum_layouts: alloc::vec::Vec::new(),
signatures: alloc::vec::Vec::new(),
native_return_shapes: alloc::vec::Vec::new(),
chunks,
native_names: Vec::new(),
entry_point: Some(0),
data_layout: None,
word_bits_log2: crate::bytecode::RUNTIME_WORD_BITS_LOG2,
addr_bits_log2: crate::bytecode::RUNTIME_ADDRESS_BITS_LOG2,
float_bits_log2: crate::bytecode::RUNTIME_FLOAT_BITS_LOG2,
wcet_cycles: 0,
wcmu_bytes: 0,
aux_arena_bytes: 0,
persistent_composite_bytes: 0,
flags: 0,
shared_data_bytes: 0,
private_data_bytes: 0,
}
}
fn make_chunk(name: &str, ops: Vec<Op>, block_type: BlockType) -> Chunk {
let local_count = ops
.iter()
.filter_map(|op| match op {
Op::GetLocal(s) | Op::SetLocal(s) => Some(*s + 1),
_ => None,
})
.max()
.unwrap_or(0);
Chunk {
name: String::from(name),
ops,
constants: alloc::vec![crate::bytecode::ConstValue::Int(0)],
struct_templates: Vec::new(),
local_count,
param_count: 0,
block_type,
param_types: Vec::new(),
debug_pool: None,
}
}
#[test]
fn is_enum_is_struct_operand_models_agree() {
let dummy = make_chunk("d", vec![Op::Return], BlockType::Func);
for op in [Op::IsEnum(0, 0, 0), Op::IsStruct(0)] {
let reporting_net = op.stack_growth() as i32 - op.stack_shrink() as i32;
let (_, depth_net) = op_depth_effect(&op, &dummy);
assert_eq!(reporting_net, 1, "{op:?}: reporting model net must be +1");
assert_eq!(
reporting_net, depth_net,
"{op:?}: WCMU reporting and operand-depth models must agree"
);
}
}
#[test]
fn is_enum_accumulation_counted_in_wcmu() {
let one = make_chunk(
"one",
vec![Op::Const(0), Op::IsEnum(0, 0, 0), Op::Return],
BlockType::Func,
);
let four = make_chunk(
"four",
vec![
Op::Const(0),
Op::IsEnum(0, 0, 0),
Op::IsEnum(0, 0, 0),
Op::IsEnum(0, 0, 0),
Op::IsEnum(0, 0, 0),
Op::Return,
],
BlockType::Func,
);
let (stack_one, _) = wcmu_whole_chunk(&one).expect("wcmu one");
let (stack_four, _) = wcmu_whole_chunk(&four).expect("wcmu four");
assert_eq!(
stack_four - stack_one,
3 * crate::bytecode::VALUE_SLOT_SIZE_BYTES,
"each accumulated IsEnum result must add one operand slot to the WCMU"
);
}
#[test]
fn valid_func_chunk() {
let chunk = make_chunk("main", vec![Op::Const(0), Op::Return], BlockType::Func);
let module = make_module(vec![chunk]);
assert!(verify(&module).is_ok());
}
#[test]
fn const_oob_index_rejected_by_verifier() {
let mut chunk = make_chunk("main", vec![Op::Const(5), Op::Return], BlockType::Func);
chunk.constants = vec![ConstValue::Int(0)]; let module = make_module(vec![chunk]);
assert!(
verify(&module).is_err(),
"expected the verifier to reject the out-of-range Const index"
);
}
#[test]
fn local_slot_oob_index_rejected_by_verifier() {
let mut chunk = make_chunk("main", vec![Op::GetLocal(5), Op::Return], BlockType::Func);
chunk.local_count = 1; let module = make_module(vec![chunk]);
assert!(
verify(&module).is_err(),
"expected the verifier to reject the out-of-range local slot"
);
}
#[test]
fn struct_template_oob_index_rejected_by_verifier() {
use crate::bytecode::NewCompositeOperand;
use crate::value_layout::CompositeKind;
let chunk = make_chunk(
"main",
vec![
Op::NewComposite(NewCompositeOperand::Boxed {
kind: CompositeKind::Struct,
count: 0,
meta: 0,
}),
Op::Return,
],
BlockType::Func,
);
let module = make_module(vec![chunk]);
assert!(
verify(&module).is_err(),
"expected the verifier to reject the out-of-range struct template index"
);
}
#[test]
fn valid_if_else() {
let chunk = make_chunk(
"main",
vec![
Op::PushImmediate(1), Op::If(4), Op::Const(0), Op::Else(5), Op::Const(0), Op::EndIf, Op::Return, ],
BlockType::Func,
);
let module = make_module(vec![chunk]);
assert!(verify(&module).is_ok());
}
#[test]
fn obligations_key_if_else_to_their_positions() {
let chunk = make_chunk(
"main",
vec![
Op::PushImmediate(1), Op::If(4), Op::Const(0), Op::Else(5), Op::Const(0), Op::EndIf, Op::Return, ],
BlockType::Func,
);
let module = make_module(vec![chunk.clone()]);
assert!(verify(&module).is_ok());
let obs = chunk_verification_obligations(&chunk, &module);
let has = |op: u32, property: &str| {
obs.iter()
.any(|o| o.op_index == op && o.property == property)
};
assert!(has(1, "if-branch-target-in-bounds"));
assert!(has(3, "else-preceded-by-matching-if"));
assert!(has(3, "else-target-in-bounds"));
assert!(has(3, "else-target-is-endif"));
assert!(has(5, "endif-closes-open-if"));
assert!(has(0, "all-blocks-closed"));
assert!(has(0, "func-has-no-yield"));
assert!(has(0, "func-has-no-stream"));
assert!(has(0, "func-has-no-reset"));
assert!(obs.iter().all(|o| o.pass != "productive-divergence"));
}
#[test]
fn obligations_record_stream_productive_divergence() {
let chunk = make_chunk(
"tick",
vec![
Op::Stream, Op::PushImmediate(1), Op::Yield, Op::Reset, ],
BlockType::Stream,
);
let module = make_module(vec![chunk.clone()]);
assert!(verify(&module).is_ok());
let obs = chunk_verification_obligations(&chunk, &module);
assert!(obs.iter().any(|o| o.op_index == 0
&& o.pass == "productive-divergence"
&& o.property == "every-stream-to-reset-path-yields"));
assert!(
obs.iter()
.any(|o| o.op_index == 0 && o.property == "stream-has-exactly-one-stream")
);
assert!(
obs.iter()
.any(|o| o.op_index == 3 && o.property == "stream-has-exactly-one-reset")
);
assert!(
obs.iter()
.any(|o| o.op_index == 2 && o.property == "stream-has-yield")
);
}
#[test]
fn obligations_truncate_at_first_failing_check() {
let chunk = make_chunk(
"main",
vec![
Op::If(1), Op::EndIf, Op::Loop(99), Op::EndLoop(3), ],
BlockType::Func,
);
let module = make_module(vec![chunk.clone()]);
assert!(verify(&module).is_err());
let obs = chunk_verification_obligations(&chunk, &module);
let has = |property: &str| obs.iter().any(|o| o.property == property);
assert!(has("if-branch-target-in-bounds"));
assert!(has("endif-closes-open-if"));
assert!(!has("loop-exit-target-in-bounds"));
assert!(!has("all-blocks-closed"));
assert!(obs.iter().all(|o| o.pass != "block-type-constraints"));
}
#[test]
fn whole_chunk_resource_bounds_accept_func_and_reentrant_reject_stream() {
let func = make_chunk(
"main",
vec![Op::PushImmediate(1), Op::Return],
BlockType::Func,
);
assert!(wcet_whole_chunk(&func).is_ok());
assert!(wcmu_whole_chunk(&func).is_ok());
let reentrant = make_chunk(
"gen",
vec![Op::PushImmediate(1), Op::Yield, Op::Return],
BlockType::Reentrant,
);
assert!(wcet_whole_chunk(&reentrant).is_ok());
assert!(wcmu_whole_chunk(&reentrant).is_ok());
let stream = make_chunk(
"tick",
vec![Op::Stream, Op::PushImmediate(1), Op::Yield, Op::Reset],
BlockType::Stream,
);
assert!(wcet_whole_chunk(&stream).is_err());
assert!(wcmu_whole_chunk(&stream).is_err());
}
#[test]
fn reentrant_wcet_is_max_segment_for_top_level_yields() {
let cm = &crate::bytecode::NOMINAL_COST_MODEL;
let reentrant = make_chunk(
"gen",
vec![
Op::PushImmediate(1), Op::PushImmediate(1), Op::Yield, Op::PushImmediate(1), Op::PushImmediate(1), Op::Yield, Op::PushImmediate(1), Op::Return, ],
BlockType::Reentrant,
);
let segmented = reentrant_segmented_wcet(&reentrant, cm, &[])
.expect("no loop bound error")
.expect("top-level yields segment");
let mut breaks = Vec::new();
let extra = crate::text_size::chunk_text_wcet_cycles(&reentrant, cm.text_byte_cycles);
let cumulative = wcet_region(
&reentrant,
0,
reentrant.ops.len(),
&mut breaks,
cm,
false,
&extra,
)
.unwrap()
.unwrap_or(0);
assert!(
segmented < cumulative,
"per-segment max {segmented} should be tighter than cumulative {cumulative}"
);
assert_eq!(wcet_whole_chunk(&reentrant).unwrap(), segmented);
}
#[test]
fn productive_loop_predicate_guards() {
let body = [Op::PushImmediate(1), Op::Yield, Op::BreakIf(0)];
assert!(loop_body_all_paths_yield_no_inner_loop(
&body,
0,
body.len()
));
let inner = [Op::Loop(2), Op::Yield, Op::EndLoop(1)];
assert!(!loop_body_all_paths_yield_no_inner_loop(
&inner,
0,
inner.len()
));
let none = [Op::PushImmediate(1)];
assert!(!loop_body_all_paths_yield_no_inner_loop(
&none,
0,
none.len()
));
}
#[test]
fn productive_loop_predicate_clamps_out_of_range_bounds() {
let body = [Op::PushImmediate(1), Op::Yield, Op::BreakIf(0)];
let _ = loop_body_all_paths_yield_no_inner_loop(&body, 0, 9999);
let _ = loop_body_all_paths_yield_no_inner_loop(&body, 5000, 9999);
let _ = loop_body_all_paths_yield_no_inner_loop(&body, 2, 1);
}
#[test]
fn reentrant_nested_productive_yield_loop_clamps_to_one_iteration() {
let cm = &crate::bytecode::NOMINAL_COST_MODEL;
let chunk = make_chunk(
"gen",
vec![
Op::Loop(5), Op::PushImmediate(1), Op::Yield, Op::BreakIf(5), Op::EndLoop(1), Op::Return, ],
BlockType::Reentrant,
);
assert_eq!(reentrant_segmented_wcet(&chunk, cm, &[]).unwrap(), None);
let clamped = wcet_whole_chunk_with_cost_model(&chunk, cm, &[])
.expect("clamp succeeds for productive loop");
assert!(clamped > 0);
let mut breaks = Vec::new();
let extra = crate::text_size::chunk_text_wcet_cycles(&chunk, cm.text_byte_cycles);
assert!(
wcet_region(&chunk, 0, chunk.ops.len(), &mut breaks, cm, false, &extra).is_err(),
"unclamped whole-body cost requires a for-range bound"
);
}
#[test]
fn reentrant_wcet_falls_back_to_cumulative_for_nested_yield() {
let cm = &crate::bytecode::NOMINAL_COST_MODEL;
let nested = make_chunk(
"gen",
vec![
Op::Loop(4), Op::Yield, Op::Break(4), Op::EndLoop(1), Op::Return, ],
BlockType::Reentrant,
);
assert_eq!(
reentrant_segmented_wcet(&nested, cm, &[]).expect("no bound error"),
None,
"a nested yield declines per-segment analysis"
);
}
#[test]
fn external_native_wcet_dedups_and_multiplies_by_invocations() {
let chunk = make_chunk(
"main",
alloc::vec![
Op::CallExternalNative(0, 0),
Op::PopN(1),
Op::CallExternalNative(0, 0),
Op::PopN(1),
Op::CallVerifiedNative(1, 0),
Op::PopN(1),
Op::Return,
],
BlockType::Func,
);
let bounds = alloc::vec![
NativeIterationBound {
per_call_wcmu_bytes: 0,
per_call_wcet_cycles: 50,
max_invocations: Some(4),
},
NativeIterationBound {
per_call_wcmu_bytes: 0,
per_call_wcet_cycles: 100,
max_invocations: None,
},
];
assert_eq!(external_native_wcet(&chunk, &bounds), 200);
}
#[test]
fn chunk_wcet_extra_folds_verified_native_per_call_at_each_site() {
let chunk = make_chunk(
"main",
alloc::vec![
Op::CallVerifiedNative(0, 0),
Op::PopN(1),
Op::CallVerifiedNative(0, 0),
Op::PopN(1),
Op::Return,
],
BlockType::Func,
);
let bounds = alloc::vec![NativeIterationBound {
per_call_wcmu_bytes: 0,
per_call_wcet_cycles: 100,
max_invocations: None,
}];
let extra = chunk_wcet_extra(&chunk, &crate::bytecode::NOMINAL_COST_MODEL, &bounds);
assert_eq!(extra[0], 100, "first verified-native call site");
assert_eq!(extra[2], 100, "second verified-native call site");
assert_eq!(extra[1], 0, "non-call op contributes no native cost");
let none = chunk_wcet_extra(&chunk, &crate::bytecode::NOMINAL_COST_MODEL, &[]);
assert_eq!(none[0], 0);
}
#[test]
fn valid_loop() {
let chunk = make_chunk(
"main",
vec![
Op::Loop(4), Op::PushImmediate(1), Op::BreakIf(4), Op::EndLoop(1), Op::PushImmediate(0), Op::Return, ],
BlockType::Func,
);
let module = make_module(vec![chunk]);
assert!(verify(&module).is_ok());
}
#[test]
fn valid_stream_chunk() {
let chunk = make_chunk(
"tick",
vec![
Op::Stream, Op::GetLocal(0), Op::Yield, Op::PopN(1), Op::Reset, ],
BlockType::Stream,
);
let module = make_module(vec![chunk]);
assert!(verify(&module).is_ok());
}
#[test]
fn valid_reentrant_chunk() {
let chunk = make_chunk(
"gen",
vec![
Op::GetLocal(0), Op::Yield, Op::PopN(1), Op::Return, ],
BlockType::Reentrant,
);
let module = make_module(vec![chunk]);
assert!(verify(&module).is_ok());
}
#[test]
fn func_with_yield_fails() {
let chunk = make_chunk(
"bad",
vec![Op::PushImmediate(0), Op::Yield, Op::Return],
BlockType::Func,
);
let module = make_module(vec![chunk]);
let err = verify(&module).unwrap_err();
assert!(err.message.contains("Yield"));
}
#[test]
fn func_with_stream_fails() {
let chunk = make_chunk("bad", vec![Op::Stream, Op::Return], BlockType::Func);
let module = make_module(vec![chunk]);
let err = verify(&module).unwrap_err();
assert!(err.message.contains("Stream"));
}
#[test]
fn func_with_reset_fails() {
let chunk = make_chunk("bad", vec![Op::Reset], BlockType::Func);
let module = make_module(vec![chunk]);
let err = verify(&module).unwrap_err();
assert!(err.message.contains("Reset"));
}
#[test]
fn reentrant_without_yield_fails() {
let chunk = make_chunk(
"bad",
vec![Op::PushImmediate(0), Op::Return],
BlockType::Reentrant,
);
let module = make_module(vec![chunk]);
let err = verify(&module).unwrap_err();
assert!(err.message.contains("Yield"));
}
#[test]
fn reentrant_with_stream_fails() {
let chunk = make_chunk(
"bad",
vec![Op::Stream, Op::PushImmediate(0), Op::Yield, Op::Return],
BlockType::Reentrant,
);
let module = make_module(vec![chunk]);
let err = verify(&module).unwrap_err();
assert!(err.message.contains("Stream"));
}
#[test]
fn stream_without_yield_fails() {
let chunk = make_chunk(
"bad",
vec![Op::Stream, Op::PushImmediate(0), Op::Reset],
BlockType::Stream,
);
let module = make_module(vec![chunk]);
let err = verify(&module).unwrap_err();
assert!(err.message.contains("Yield"));
}
#[test]
fn stream_missing_reset_fails() {
let chunk = make_chunk(
"bad",
vec![Op::Stream, Op::PushImmediate(0), Op::Yield, Op::PopN(1)],
BlockType::Stream,
);
let module = make_module(vec![chunk]);
let err = verify(&module).unwrap_err();
assert!(err.message.contains("Reset"));
}
#[test]
fn stream_missing_stream_fails() {
let chunk = make_chunk(
"bad",
vec![Op::PushImmediate(0), Op::Yield, Op::PopN(1), Op::Reset],
BlockType::Stream,
);
let module = make_module(vec![chunk]);
let err = verify(&module).unwrap_err();
assert!(err.message.contains("Stream"));
}
#[test]
fn unclosed_if_fails() {
let chunk = make_chunk(
"bad",
vec![
Op::PushImmediate(1),
Op::If(3), Op::PushImmediate(0),
Op::Return, ],
BlockType::Func,
);
let module = make_module(vec![chunk]);
let err = verify(&module).unwrap_err();
assert!(err.message.contains("If") || err.message.contains("expected"));
}
#[test]
fn break_outside_loop_fails() {
let chunk = make_chunk("bad", vec![Op::Break(1), Op::Return], BlockType::Func);
let module = make_module(vec![chunk]);
let err = verify(&module).unwrap_err();
assert!(err.message.contains("outside"));
}
#[test]
fn breakif_outside_loop_fails() {
let chunk = make_chunk(
"bad",
vec![Op::PushImmediate(1), Op::BreakIf(2), Op::Return],
BlockType::Func,
);
let module = make_module(vec![chunk]);
let err = verify(&module).unwrap_err();
assert!(err.message.contains("outside"));
}
#[test]
fn endloop_bad_backedge_fails() {
let chunk = make_chunk(
"bad",
vec![
Op::Loop(4), Op::PushImmediate(1), Op::BreakIf(4), Op::EndLoop(0), Op::PushImmediate(0), Op::Return, ],
BlockType::Func,
);
let module = make_module(vec![chunk]);
let err = verify(&module).unwrap_err();
assert!(err.message.contains("back-edge"));
}
#[test]
fn else_targets_wrong_op_fails() {
let chunk = make_chunk(
"bad",
vec![
Op::PushImmediate(1), Op::If(4), Op::PushImmediate(0), Op::Else(5), Op::PushImmediate(0), Op::PushImmediate(0), Op::Return, ],
BlockType::Func,
);
let module = make_module(vec![chunk]);
let err = verify(&module).unwrap_err();
assert!(err.message.contains("expected EndIf"));
}
#[test]
fn interior_if_targeting_back_edge_rejected() {
let chunk = make_chunk(
"hostile",
vec![
Op::Loop(5), Op::PushImmediate(1), Op::If(4), Op::EndIf, Op::EndLoop(1), Op::Return, ],
BlockType::Func,
);
let module = make_module(vec![chunk]);
let err = verify(&module).unwrap_err();
assert!(
err.message.contains("must target its EndIf"),
"expected an If-target rejection, got: {}",
err.message
);
}
#[test]
fn breakif_targeting_back_edge_rejected() {
let chunk = make_chunk(
"hostile",
vec![
Op::Loop(4), Op::PushImmediate(1), Op::BreakIf(3), Op::EndLoop(1), Op::Return, ],
BlockType::Func,
);
let module = make_module(vec![chunk]);
let err = verify(&module).unwrap_err();
assert!(
err.message.contains("enclosing loop exits to"),
"expected a Break-target rejection, got: {}",
err.message
);
}
#[test]
fn loop_exit_zero_rejected_without_panic() {
let chunk = make_chunk(
"hostile",
vec![
Op::Loop(0), Op::EndLoop(1), Op::Return, ],
BlockType::Func,
);
let module = make_module(vec![chunk]);
let err = verify(&module).unwrap_err();
assert!(
err.message.contains("expected exit"),
"expected a Loop-exit rejection, got: {}",
err.message
);
}
#[test]
fn loop_exit_not_after_endloop_rejected() {
let chunk = make_chunk(
"hostile",
vec![
Op::Loop(3), Op::EndLoop(1), Op::Return, ],
BlockType::Func,
);
let module = make_module(vec![chunk]);
let err = verify(&module).unwrap_err();
assert!(
err.message.contains("expected exit"),
"expected a Loop-exit rejection, got: {}",
err.message
);
}
#[test]
fn out_of_range_branch_target_does_not_panic_in_standalone_cost_pass() {
let bad_if = make_chunk(
"bad_if",
vec![Op::Const(0), Op::If(9999), Op::Return],
BlockType::Func,
);
let _ = wcet_whole_chunk(&bad_if);
let _ = wcmu_whole_chunk(&bad_if);
let bad_else = make_chunk(
"bad_else",
vec![
Op::Const(0),
Op::If(3),
Op::Else(9999),
Op::EndIf,
Op::Return,
],
BlockType::Func,
);
let _ = wcet_whole_chunk(&bad_else);
let _ = wcmu_whole_chunk(&bad_else);
}
#[test]
fn mismatched_if_endloop_fails() {
let chunk = make_chunk(
"bad",
vec![
Op::PushImmediate(1), Op::If(3), Op::PushImmediate(0), Op::EndLoop(0), ],
BlockType::Func,
);
let module = make_module(vec![chunk]);
assert!(verify(&module).is_err());
}
#[test]
fn verify_compiled_programs() {
use crate::compiler::compile;
use crate::lexer::tokenize;
use crate::parser::parse;
let programs = [
"fn main() -> Word { 42 }",
"fn main() -> Word { if true { 1 } else { 2 } }",
"fn main() -> Word { let sum = 0; for i in 0..5 { let x = sum + i; } sum }",
"fn double(x: Word) -> Word { x * 2 }\nfn main() -> Word { double(21) }",
"fn main() -> Text { let x = 1; match x { 1 => \"one\", _ => \"other\" } }",
"loop tick(x: Word) -> Word { let x = yield x * 2; x }",
];
for src in &programs {
let tokens = tokenize(src).expect("lex error");
let program = parse(&tokens).expect("parse error");
let module = compile(&program).expect("compile error");
if let Err(e) = verify(&module) {
panic!(
"verification failed for {:?}: {}: {}",
src, e.chunk_name, e.message
);
}
}
}
#[test]
fn productivity_linear_yield() {
let chunk = make_chunk(
"tick",
vec![
Op::Stream, Op::GetLocal(0), Op::Yield, Op::PopN(1), Op::Reset, ],
BlockType::Stream,
);
let module = make_module(vec![chunk]);
assert!(verify(&module).is_ok());
}
#[test]
fn productivity_yield_both_branches() {
let chunk = make_chunk(
"tick",
vec![
Op::Stream, Op::PushImmediate(1), Op::If(7), Op::GetLocal(0), Op::Yield, Op::PopN(1), Op::Else(10), Op::GetLocal(0), Op::Yield, Op::PopN(1), Op::EndIf, Op::Reset, ],
BlockType::Stream,
);
let module = make_module(vec![chunk]);
assert!(verify(&module).is_ok());
}
#[test]
fn productivity_yield_before_if() {
let chunk = make_chunk(
"tick",
vec![
Op::Stream, Op::GetLocal(0), Op::Yield, Op::PopN(1), Op::PushImmediate(1), Op::If(9), Op::PushImmediate(0), Op::PopN(1), Op::Else(11), Op::PushImmediate(0), Op::PopN(1), Op::EndIf, Op::Reset, ],
BlockType::Stream,
);
let module = make_module(vec![chunk]);
assert!(verify(&module).is_ok());
}
#[test]
fn productivity_yield_only_in_then_fails() {
let chunk = make_chunk(
"tick",
vec![
Op::Stream, Op::PushImmediate(1), Op::If(6), Op::GetLocal(0), Op::Yield, Op::Else(9), Op::PushImmediate(0), Op::PopN(1), Op::PushImmediate(0), Op::EndIf, Op::PopN(1), Op::Reset, ],
BlockType::Stream,
);
let module = make_module(vec![chunk]);
let err = verify(&module).unwrap_err();
assert!(err.message.contains("productivity violation"));
}
#[test]
fn productivity_no_yield_path_fails() {
let chunk = make_chunk(
"tick",
vec![
Op::Stream, Op::PushImmediate(1), Op::If(6), Op::GetLocal(0), Op::Yield, Op::PopN(1), Op::EndIf, Op::Reset, ],
BlockType::Stream,
);
let module = make_module(vec![chunk]);
let err = verify(&module).unwrap_err();
assert!(err.message.contains("productivity violation"));
}
#[test]
fn productivity_yield_in_loop_fails() {
let chunk = make_chunk(
"tick",
vec![
Op::Stream, Op::Loop(8), Op::PushImmediate(1), Op::BreakIf(8), Op::GetLocal(0), Op::Yield, Op::PopN(1), Op::EndLoop(2), Op::Reset, ],
BlockType::Stream,
);
let module = make_module(vec![chunk]);
let err = verify(&module).unwrap_err();
assert!(err.message.contains("productivity violation"));
}
#[test]
fn productivity_yield_before_loop() {
let chunk = make_chunk(
"tick",
vec![
Op::Stream, Op::GetLocal(0), Op::Yield, Op::PopN(1), Op::Loop(8), Op::PushImmediate(1), Op::BreakIf(8), Op::EndLoop(5), Op::Reset, ],
BlockType::Stream,
);
let module = make_module(vec![chunk]);
assert!(verify(&module).is_ok());
}
#[test]
fn productivity_compiled_stream() {
use crate::compiler::compile;
use crate::lexer::tokenize;
use crate::parser::parse;
let src = "loop tick(x: Word) -> Word { let x = yield x * 2; x }";
let tokens = tokenize(src).expect("lex error");
let program = parse(&tokens).expect("parse error");
let module = compile(&program).expect("compile error");
assert!(verify(&module).is_ok());
}
#[test]
fn cost_basic_ops() {
assert_eq!(Op::Const(0).cost(), 1);
assert_eq!(Op::PushImmediate(0).cost(), 1);
assert_eq!(Op::GetLocal(0).cost(), 1);
assert_eq!(Op::SetLocal(0).cost(), 1);
assert_eq!(Op::PopN(1).cost(), 1);
assert_eq!(Op::Not.cost(), 1);
assert_eq!(Op::Add.cost(), 2);
assert_eq!(Op::Sub.cost(), 2);
assert_eq!(Op::Mul.cost(), 2);
assert_eq!(Op::CmpEq.cost(), 2);
assert_eq!(Op::Return.cost(), 2);
assert_eq!(Op::Div.cost(), 3);
assert_eq!(Op::Mod.cost(), 3);
assert_eq!(
Op::GetField(crate::bytecode::StructField::Boxed { name_const: 0 }).cost(),
3
);
assert_eq!(
Op::NewComposite(crate::bytecode::NewCompositeOperand::Flat {
kind: crate::value_layout::CompositeKind::Array,
count: 0,
byte_size: 0,
})
.cost(),
5
);
assert_eq!(Op::Call(0, 0).cost(), 10);
assert_eq!(Op::CallVerifiedNative(0, 0).cost(), 10);
assert_eq!(Op::CallExternalNative(0, 0).cost(), 10);
}
#[test]
fn wcet_linear_stream() {
let chunk = make_chunk(
"tick",
vec![
Op::Stream, Op::GetLocal(0), Op::Add, Op::Yield, Op::PopN(1), Op::Reset, ],
BlockType::Stream,
);
let cost = wcet_stream_iteration(&chunk).unwrap();
assert_eq!(cost, 7);
}
#[test]
fn wcet_branching_takes_max() {
let chunk = make_chunk(
"tick",
vec![
Op::Stream, Op::PushImmediate(1), Op::If(5), Op::Add, Op::Else(7), Op::Div, Op::Mul, Op::EndIf, Op::Yield, Op::PopN(1), Op::Reset, ],
BlockType::Stream,
);
let cost = wcet_stream_iteration(&chunk).unwrap();
assert_eq!(cost, 11);
}
#[test]
fn wcet_non_stream_errors() {
let chunk = make_chunk(
"main",
vec![Op::PushImmediate(0), Op::Return],
BlockType::Func,
);
let err = wcet_stream_iteration(&chunk).unwrap_err();
assert!(err.message.contains("Stream"));
}
#[test]
fn wcet_compiled_stream() {
use crate::compiler::compile;
use crate::lexer::tokenize;
use crate::parser::parse;
let src = "loop tick(x: Word) -> Word { let x = yield x * 2; x }";
let tokens = tokenize(src).expect("lex error");
let program = parse(&tokens).expect("parse error");
let module = compile(&program).expect("compile error");
let stream_chunk = module
.chunks
.iter()
.find(|c| c.block_type == BlockType::Stream)
.expect("no stream chunk found");
let cost = wcet_stream_iteration(stream_chunk).unwrap();
assert!(cost > 0, "WCET should be positive, got {}", cost);
}
#[test]
fn data_slot_out_of_bounds_fails() {
use crate::bytecode::{DataLayout, DataSlot};
let chunk = make_chunk("main", vec![Op::GetData(5), Op::Return], BlockType::Func);
let module = Module {
schema_hash: 0,
enum_layouts: alloc::vec::Vec::new(),
signatures: alloc::vec::Vec::new(),
native_return_shapes: alloc::vec::Vec::new(),
chunks: vec![chunk],
native_names: Vec::new(),
entry_point: Some(0),
word_bits_log2: crate::bytecode::RUNTIME_WORD_BITS_LOG2,
addr_bits_log2: crate::bytecode::RUNTIME_ADDRESS_BITS_LOG2,
float_bits_log2: crate::bytecode::RUNTIME_FLOAT_BITS_LOG2,
wcet_cycles: 0,
wcmu_bytes: 0,
aux_arena_bytes: 0,
persistent_composite_bytes: 0,
flags: 0,
shared_data_bytes: 0,
private_data_bytes: 0,
data_layout: Some(DataLayout {
slots: vec![DataSlot {
name: String::from("ctx.x"),
visibility: crate::bytecode::SlotVisibility::Shared,
}],
shared_layout: Vec::new(),
private_composite_layout: Vec::new(),
}),
};
let err = verify(&module).unwrap_err();
assert!(err.message.contains("slot"));
}
#[test]
fn data_no_layout_fails() {
let chunk = make_chunk("main", vec![Op::GetData(0), Op::Return], BlockType::Func);
let module = make_module(vec![chunk]);
let err = verify(&module).unwrap_err();
assert!(err.message.contains("no data layout"));
}
#[test]
fn data_valid_slot_passes() {
use crate::bytecode::{DataLayout, DataSlot};
let chunk = make_chunk(
"main",
vec![
Op::GetData(0),
Op::SetData(1),
Op::PushImmediate(0),
Op::Return,
],
BlockType::Func,
);
let module = Module {
schema_hash: 0,
enum_layouts: alloc::vec::Vec::new(),
signatures: alloc::vec::Vec::new(),
native_return_shapes: alloc::vec::Vec::new(),
chunks: vec![chunk],
native_names: Vec::new(),
entry_point: Some(0),
word_bits_log2: crate::bytecode::RUNTIME_WORD_BITS_LOG2,
addr_bits_log2: crate::bytecode::RUNTIME_ADDRESS_BITS_LOG2,
float_bits_log2: crate::bytecode::RUNTIME_FLOAT_BITS_LOG2,
wcet_cycles: 0,
wcmu_bytes: 0,
aux_arena_bytes: 0,
persistent_composite_bytes: 0,
flags: 0,
shared_data_bytes: 16,
private_data_bytes: 0,
data_layout: Some(DataLayout {
slots: vec![
DataSlot {
name: String::from("ctx.a"),
visibility: crate::bytecode::SlotVisibility::Shared,
},
DataSlot {
name: String::from("ctx.b"),
visibility: crate::bytecode::SlotVisibility::Shared,
},
],
shared_layout: vec![
crate::bytecode::SharedSlotLayout {
offset: 0,
kind: crate::value_layout::ScalarKind::Int.to_tag(),
len: 0,
},
crate::bytecode::SharedSlotLayout {
offset: 8,
kind: crate::value_layout::ScalarKind::Int.to_tag(),
len: 0,
},
],
private_composite_layout: Vec::new(),
}),
};
assert!(verify(&module).is_ok());
}
#[test]
fn wcmu_stream_simple() {
use crate::bytecode::VALUE_SLOT_SIZE_BYTES;
let chunk = make_chunk(
"tick",
vec![
Op::Stream, Op::GetLocal(0), Op::Yield, Op::PopN(1), Op::Reset, ],
BlockType::Stream,
);
let mut chunk = chunk;
chunk.local_count = 1;
let (stack, heap) = wcmu_stream_iteration(&chunk).unwrap();
assert_eq!(stack, 2 * VALUE_SLOT_SIZE_BYTES);
assert_eq!(heap, 0);
}
#[test]
fn wcmu_branching_takes_max() {
use crate::bytecode::VALUE_SLOT_SIZE_BYTES;
let mut chunk = make_chunk(
"tick",
vec![
Op::Stream, Op::PushImmediate(1), Op::If(7), Op::Const(0), Op::Const(0), Op::Const(0), Op::Else(9), Op::Const(0), Op::PopN(1), Op::EndIf, Op::PopN(1), Op::PopN(1), Op::PopN(1), Op::GetLocal(0), Op::Yield, Op::PopN(1), Op::Reset, ],
BlockType::Stream,
);
chunk.local_count = 1;
chunk.constants = vec![ConstValue::Int(0)];
let (stack, _heap) = wcmu_stream_iteration(&chunk).unwrap();
assert!(stack >= 3 * VALUE_SLOT_SIZE_BYTES);
}
#[test]
fn wcmu_new_struct_heap() {
let mut chunk = make_chunk(
"tick",
vec![
Op::Stream, Op::Const(0), Op::Const(0), Op::NewComposite(crate::bytecode::NewCompositeOperand::Flat {
kind: crate::value_layout::CompositeKind::Struct,
count: 2,
byte_size: 16,
}), Op::Yield, Op::Reset, ],
BlockType::Stream,
);
chunk.local_count = 0;
chunk.constants = vec![ConstValue::Int(0)];
let (_stack, heap) = wcmu_stream_iteration(&chunk).unwrap();
assert_eq!(heap, 16);
}
#[test]
fn wcmu_new_array_heap() {
let mut chunk = make_chunk(
"tick",
vec![
Op::Stream,
Op::Const(0),
Op::Const(0),
Op::Const(0),
Op::NewComposite(crate::bytecode::NewCompositeOperand::Flat {
kind: crate::value_layout::CompositeKind::Array,
count: 3,
byte_size: 24,
}),
Op::Yield,
Op::Reset,
],
BlockType::Stream,
);
chunk.constants = vec![ConstValue::Int(0)];
let (_stack, heap) = wcmu_stream_iteration(&chunk).unwrap();
assert_eq!(heap, 24);
}
#[test]
fn wcmu_non_stream_errors() {
let chunk = make_chunk(
"main",
vec![Op::PushImmediate(0), Op::Return],
BlockType::Func,
);
let err = wcmu_stream_iteration(&chunk).unwrap_err();
assert!(err.message.contains("Stream"));
}
#[test]
fn verify_resource_bounds_passes() {
let chunk = make_chunk(
"tick",
vec![
Op::Stream,
Op::PushImmediate(0),
Op::Yield,
Op::PopN(1),
Op::Reset,
],
BlockType::Stream,
);
let module = make_module(vec![chunk]);
let result = verify_resource_bounds(&module, 1024 * 1024);
assert!(result.is_ok());
}
#[test]
fn verify_resource_bounds_rejects_oversized() {
let mut chunk = make_chunk(
"tick",
vec![
Op::Stream,
Op::Const(0),
Op::Const(0),
Op::NewComposite(crate::bytecode::NewCompositeOperand::Flat {
kind: crate::value_layout::CompositeKind::Array,
count: 2,
byte_size: 64,
}),
Op::Yield,
Op::PopN(1),
Op::Reset,
],
BlockType::Stream,
);
chunk.local_count = 4;
chunk.constants = vec![ConstValue::Int(0)];
let module = make_module(vec![chunk]);
let err = verify_resource_bounds(&module, 16).unwrap_err();
assert!(err.message.contains("WCMU"));
assert!(err.message.contains("exceeds arena capacity"));
}
#[test]
fn verify_resource_bounds_skips_non_stream() {
let chunk = make_chunk(
"util",
vec![Op::PushImmediate(0), Op::Return],
BlockType::Func,
);
let module = make_module(vec![chunk]);
let result = verify_resource_bounds(&module, 16);
assert!(result.is_ok());
}
#[test]
fn module_wcmu_returns_per_chunk_results() {
let chunk = make_chunk(
"tick",
vec![
Op::Stream,
Op::PushImmediate(0),
Op::Yield,
Op::PopN(1),
Op::Reset,
],
BlockType::Stream,
);
let module = make_module(vec![chunk]);
let results = module_wcmu(&module, &[]).unwrap();
assert_eq!(results.len(), 1);
let (stack_bytes, heap_bytes) = results[0];
assert!(stack_bytes > 0);
assert_eq!(heap_bytes, 0);
}
#[test]
fn module_wcmu_includes_transitive_call_heap() {
let mut callee = make_chunk(
"alloc_array",
vec![
Op::Const(0),
Op::Const(0),
Op::Const(0),
Op::NewComposite(crate::bytecode::NewCompositeOperand::Flat {
kind: crate::value_layout::CompositeKind::Array,
count: 3,
byte_size: 24,
}),
Op::PopN(1),
Op::PushImmediate(0),
Op::Return,
],
BlockType::Func,
);
callee.constants = vec![ConstValue::Int(0)];
let stream_chunk = make_chunk(
"tick",
vec![
Op::Stream, Op::Call(0, 0), Op::PopN(1), Op::PushImmediate(0), Op::Yield, Op::PopN(1), Op::Reset, ],
BlockType::Stream,
);
let module = make_module(vec![callee, stream_chunk]);
let results = module_wcmu(&module, &[]).unwrap();
let (_stream_stack, stream_heap) = results[1];
let (_callee_stack, callee_heap) = results[0];
assert!(callee_heap > 0, "callee heap should be > 0");
assert!(
stream_heap >= callee_heap,
"stream heap should include callee heap"
);
}
#[test]
fn module_wcmu_uses_native_attestation() {
let stream_chunk = make_chunk(
"tick",
vec![
Op::Stream, Op::CallVerifiedNative(0, 0), Op::PopN(1), Op::PushImmediate(0), Op::Yield, Op::PopN(1), Op::Reset, ],
BlockType::Stream,
);
let mut module = make_module(vec![stream_chunk]);
module.native_names = vec![String::from("host::alloc")];
let results = module_wcmu(&module, &[]).unwrap();
let (_, heap_no_attest) = results[0];
assert_eq!(heap_no_attest, 0);
let results = module_wcmu(&module, &[256]).unwrap();
let (_, heap_with_attest) = results[0];
assert_eq!(heap_with_attest, 256);
}
#[test]
fn verify_resource_bounds_with_natives_rejects_attested_overflow() {
let stream_chunk = make_chunk(
"tick",
vec![
Op::Stream,
Op::CallVerifiedNative(0, 0),
Op::PopN(1),
Op::PushImmediate(0),
Op::Yield,
Op::PopN(1),
Op::Reset,
],
BlockType::Stream,
);
let mut module = make_module(vec![stream_chunk]);
module.native_names = vec![String::from("host::alloc")];
let err = verify_resource_bounds_with_natives(&module, 16, &[1024]).unwrap_err();
assert!(err.message.contains("exceeds arena capacity"));
}
#[test]
fn module_wcmu_with_bounds_verified_matches_per_site_sum() {
let stream_chunk = make_chunk(
"tick",
vec![
Op::Stream,
Op::CallVerifiedNative(0, 0),
Op::PopN(1),
Op::CallVerifiedNative(0, 0),
Op::PopN(1),
Op::PushImmediate(0),
Op::Yield,
Op::PopN(1),
Op::Reset,
],
BlockType::Stream,
);
let mut module = make_module(vec![stream_chunk]);
module.native_names = vec![String::from("host::alloc")];
let bounds = alloc::vec![NativeIterationBound {
per_call_wcmu_bytes: 100,
per_call_wcet_cycles: 0,
max_invocations: None,
}];
let results =
module_wcmu_with_bounds(&module, &bounds, crate::bytecode::VALUE_SLOT_SIZE_BYTES)
.unwrap();
let (_, heap) = results[0];
assert_eq!(heap, 200, "two verified call sites at 100 each");
}
#[test]
fn module_wcmu_with_bounds_external_uses_max_invocations() {
let stream_chunk = make_chunk(
"tick",
vec![
Op::Stream,
Op::CallExternalNative(0, 0),
Op::PopN(1),
Op::CallExternalNative(0, 0),
Op::PopN(1),
Op::PushImmediate(0),
Op::Yield,
Op::PopN(1),
Op::Reset,
],
BlockType::Stream,
);
let mut module = make_module(vec![stream_chunk]);
module.native_names = vec![String::from("host::log_event")];
let bounds = alloc::vec![NativeIterationBound {
per_call_wcmu_bytes: 100,
per_call_wcet_cycles: 0,
max_invocations: Some(50),
}];
let results =
module_wcmu_with_bounds(&module, &bounds, crate::bytecode::VALUE_SLOT_SIZE_BYTES)
.unwrap();
let (_, heap) = results[0];
assert_eq!(heap, 5000);
}
#[test]
fn module_wcmu_with_bounds_mixed_classifications() {
let stream_chunk = make_chunk(
"tick",
vec![
Op::Stream,
Op::CallVerifiedNative(0, 0),
Op::PopN(1),
Op::CallExternalNative(1, 0),
Op::PopN(1),
Op::PushImmediate(0),
Op::Yield,
Op::PopN(1),
Op::Reset,
],
BlockType::Stream,
);
let mut module = make_module(vec![stream_chunk]);
module.native_names = vec![String::from("host::alloc"), String::from("host::log_event")];
let bounds = alloc::vec![
NativeIterationBound {
per_call_wcmu_bytes: 256,
per_call_wcet_cycles: 0,
max_invocations: None,
},
NativeIterationBound {
per_call_wcmu_bytes: 64,
per_call_wcet_cycles: 0,
max_invocations: Some(10),
},
];
let results =
module_wcmu_with_bounds(&module, &bounds, crate::bytecode::VALUE_SLOT_SIZE_BYTES)
.unwrap();
let (_, heap) = results[0];
assert_eq!(heap, 896);
}
#[test]
fn module_wcmu_topological_handles_chain() {
let leaf = make_chunk(
"leaf",
vec![Op::PushImmediate(0), Op::Return],
BlockType::Func,
);
let helper = make_chunk("helper", vec![Op::Call(0, 0), Op::Return], BlockType::Func);
let stream = make_chunk(
"tick",
vec![
Op::Stream,
Op::Call(1, 0),
Op::PopN(1),
Op::PushImmediate(0),
Op::Yield,
Op::PopN(1),
Op::Reset,
],
BlockType::Stream,
);
let module = make_module(vec![leaf, helper, stream]);
let results = module_wcmu(&module, &[]).unwrap();
assert_eq!(results.len(), 3);
}
#[test]
fn for_range_loop_multiplies_heap() {
use crate::compiler::compile;
use crate::lexer::tokenize;
use crate::parser::parse;
let src = "loop main(input: Word) -> Word { \
for i in 0..5 { \
let _arr = [1, 2, 3, 4]; \
} \
let _ignored = yield input; \
input \
}";
let tokens = tokenize(src).expect("lex error");
let program = parse(&tokens).expect("parse error");
let module = compile(&program).expect("compile error");
let stream_chunk = module
.chunks
.iter()
.find(|c| c.block_type == BlockType::Stream)
.expect("no stream chunk");
let (_stack_bytes, heap_bytes) = wcmu_stream_iteration(stream_chunk).unwrap();
let word_bytes = (1usize << module.word_bits_log2) / 8;
let expected = 5 * 4 * word_bytes as u32;
assert_eq!(heap_bytes, expected);
}
#[test]
fn for_range_loop_multiplies_wcet() {
use crate::compiler::compile;
use crate::lexer::tokenize;
use crate::parser::parse;
let src = "loop main(input: Word) -> Word { \
for i in 0..3 { \
let _x = i + 1; \
} \
let _ignored = yield input; \
input \
}";
let tokens = tokenize(src).expect("lex error");
let program = parse(&tokens).expect("parse error");
let module = compile(&program).expect("compile error");
let stream_chunk = module
.chunks
.iter()
.find(|c| c.block_type == BlockType::Stream)
.expect("no stream chunk");
let cost_with_loop = wcet_stream_iteration(stream_chunk).unwrap();
let src_no_loop = "loop main(input: Word) -> Word { \
let _x = input + 1; \
let _ignored = yield input; \
input \
}";
let tokens = tokenize(src_no_loop).expect("lex error");
let program = parse(&tokens).expect("parse error");
let module2 = compile(&program).expect("compile error");
let stream_chunk2 = module2
.chunks
.iter()
.find(|c| c.block_type == BlockType::Stream)
.expect("no stream chunk");
let cost_without_loop = wcet_stream_iteration(stream_chunk2).unwrap();
assert!(
cost_with_loop > cost_without_loop,
"loop cost {} should exceed non-loop cost {}",
cost_with_loop,
cost_without_loop
);
}
#[test]
fn extract_loop_iteration_bound_matches_canonical() {
let mut chunk = make_chunk(
"test",
vec![
Op::Const(0), Op::SetLocal(0), Op::Const(1), Op::SetLocal(1), Op::Loop(15), Op::GetLocal(0), Op::GetLocal(1), Op::CmpGe, Op::BreakIf(15), Op::GetLocal(0), Op::Const(2), Op::CheckedAdd, Op::PopN(2), Op::SetLocal(0), Op::EndLoop(5), Op::Return, ],
BlockType::Func,
);
chunk.constants = vec![ConstValue::Int(0), ConstValue::Int(10), ConstValue::Int(1)];
let count = extract_loop_iteration_bound(&chunk, 4);
assert_eq!(count, Some(10));
}
#[test]
fn extract_loop_iteration_bound_none_without_increment() {
let mut chunk = make_chunk(
"test",
vec![
Op::Const(0), Op::SetLocal(0), Op::Const(1), Op::SetLocal(1), Op::Loop(11), Op::GetLocal(0), Op::GetLocal(1), Op::CmpGe, Op::BreakIf(11), Op::EndLoop(5), Op::Return, ],
BlockType::Func,
);
chunk.constants = vec![ConstValue::Int(0), ConstValue::Int(10)];
assert_eq!(extract_loop_iteration_bound(&chunk, 4), None);
}
#[test]
fn extract_loop_iteration_bound_returns_none_for_non_canonical() {
let chunk = make_chunk(
"test",
vec![
Op::Loop(4),
Op::PushImmediate(1),
Op::BreakIf(4),
Op::EndLoop(1),
],
BlockType::Func,
);
let count = extract_loop_iteration_bound(&chunk, 0);
assert_eq!(count, None);
}
#[test]
fn strict_mode_rejects_non_extractable_loop() {
let chunk = make_chunk(
"tick",
vec![
Op::Stream, Op::Loop(7), Op::PushImmediate(1), Op::BreakIf(7), Op::PushImmediate(0), Op::PopN(1), Op::EndLoop(2), Op::Yield, Op::PopN(1), Op::Reset, ],
BlockType::Stream,
);
let err = wcmu_stream_iteration(&chunk).unwrap_err();
assert!(
err.message.contains("strict mode") || err.message.contains("iteration bound"),
"expected strict mode error, got: {}",
err.message
);
}
#[test]
fn strict_mode_accepts_match_via_trap_exit() {
let mut chunk = make_chunk(
"tick",
vec![
Op::Stream, Op::Loop(5), Op::Trap(0), Op::EndLoop(2), Op::PushImmediate(0), Op::Yield, Op::PopN(1), Op::Reset, ],
BlockType::Stream,
);
chunk.constants = vec![ConstValue::StaticStr(String::from("trap"))];
let result = wcmu_stream_iteration(&chunk);
assert!(result.is_ok(), "expected acceptance, got: {:?}", result);
}
#[test]
fn for_range_zero_iterations_yields_zero_heap() {
let mut chunk = make_chunk(
"tick",
vec![
Op::Stream,
Op::Const(0), Op::SetLocal(0), Op::Const(1), Op::SetLocal(1), Op::Loop(20), Op::GetLocal(0), Op::GetLocal(1), Op::CmpGe, Op::BreakIf(20), Op::Const(2), Op::Const(2), Op::NewComposite(crate::bytecode::NewCompositeOperand::Flat {
kind: crate::value_layout::CompositeKind::Array,
count: 2,
byte_size: 16,
}), Op::PopN(1), Op::GetLocal(0), Op::Const(3), Op::CheckedAdd, Op::PopN(2), Op::SetLocal(0), Op::EndLoop(6), Op::PushImmediate(0), Op::Yield, Op::PopN(1), Op::Reset, ],
BlockType::Stream,
);
chunk.constants = vec![
ConstValue::Int(5),
ConstValue::Int(5),
ConstValue::Int(0),
ConstValue::Int(1),
];
chunk.local_count = 2;
let (_stack, heap) = wcmu_stream_iteration(&chunk).unwrap();
assert_eq!(heap, 0);
}
}