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;
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 && 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 - 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 wcet_region(
chunk: &Chunk,
start: usize,
end: usize,
break_costs: &mut Vec<u32>,
) -> Result<Option<u32>, VerifyError> {
let ops = &chunk.ops;
let mut cost: u32 = 0;
let mut ip = start;
while ip < end {
match &ops[ip] {
Op::Break(_) => {
cost += ops[ip].cost();
break_costs.push(cost);
return Ok(None);
}
Op::Trap(_) => {
cost += ops[ip].cost();
let _ = cost;
return Ok(None);
}
Op::BreakIf(_) => {
cost += ops[ip].cost();
break_costs.push(cost);
ip += 1;
}
Op::If(target) => {
cost += ops[ip].cost();
let target = *target as usize;
if target > 0 && 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)?;
let else_cost = wcet_region(chunk, target, endif_pos, break_costs)?;
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)?;
match then_cost {
Some(c) => cost += c,
None => {
}
}
ip = target + 1;
}
}
Op::Loop(target) => {
cost += ops[ip].cost();
let loop_exit_target = *target as usize;
let endloop_ip = loop_exit_target - 1;
let mut loop_break_costs: Vec<u32> = Vec::new();
let body_cost = wcet_region(chunk, ip + 1, endloop_ip, &mut loop_break_costs)?;
if loop_break_costs.is_empty() && body_cost.is_none() {
return Ok(None);
}
let iter_count = if body_cost.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_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 += ops[ip].cost();
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 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 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::NewArray(n) = &ops[ip - 1] {
return Some(*n 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],
}
impl<'a> CallResolver<'a> {
fn empty() -> Self {
Self {
chunk_wcmu: &[],
native_wcmu: &[],
}
}
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 wcmu_region(
chunk: &Chunk,
start: usize,
end: usize,
break_results: &mut Vec<McuResult>,
resolver: &CallResolver,
) -> 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;
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 += op.heap_alloc(chunk);
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 += op.heap_alloc(chunk);
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 += op.heap_alloc(chunk);
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 += op.heap_alloc(chunk);
current_offset += growth - shrink;
let target = *target as usize;
if target > 0 && 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,
)?;
let else_branch = wcmu_subregion(
chunk,
target,
endif_pos,
current_offset,
break_results,
resolver,
)?;
match (then_branch, else_branch) {
(Some(a), Some(b)) => {
peak = peak.max(a.peak_above_initial).max(b.peak_above_initial);
heap += 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 += a.heap_total;
current_offset = a.delta;
}
(None, Some(b)) => {
peak = peak.max(b.peak_above_initial);
heap += 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,
)?;
if let Some(a) = then_branch {
peak = peak.max(a.peak_above_initial);
heap += 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 += op.heap_alloc(chunk);
current_offset += growth - shrink;
let loop_exit_target = *target as usize;
let endloop_ip = loop_exit_target - 1;
let mut loop_breaks: Vec<McuResult> = Vec::new();
let body = wcmu_subregion(
chunk,
ip + 1,
endloop_ip,
current_offset,
&mut loop_breaks,
resolver,
)?;
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 += 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 / crate::bytecode::VALUE_SLOT_SIZE_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 += callee_heap_bytes;
current_offset += 1 - n;
ip += 1;
}
Op::CallNative(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 += 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 += op.heap_alloc(chunk);
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,
) -> Result<Option<McuResult>, VerifyError> {
let mut sub_breaks: Vec<McuResult> = Vec::new();
let result = wcmu_region(chunk, start, end, &mut sub_breaks, resolver)?;
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> {
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)?
.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 * crate::bytecode::VALUE_SLOT_SIZE_BYTES;
Ok((stack_bytes, body_heap))
}
pub fn wcet_stream_iteration(chunk: &Chunk) -> 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 body_cost = wcet_region(chunk, stream_pos + 1, reset_pos, &mut break_costs)?;
let overhead = ops[stream_pos].cost() + ops[reset_pos].cost();
let region_cost = body_cost.unwrap_or(0);
Ok(overhead + region_cost)
}
pub fn module_wcmu(module: &Module, native_wcmu: &[u32]) -> Result<Vec<(u32, u32)>, VerifyError> {
for chunk in &module.chunks {
for op in &chunk.ops {
match op {
Op::MakeRecursiveClosure(target, _) => {
return Err(VerifyError {
chunk_name: chunk.name.clone(),
message: alloc::format!(
"MakeRecursiveClosure(chunk={}) introduces unbounded recursion that cannot be statically bounded for WCET/WCMU analysis. Recursive closures are not admitted by `verify_resource_bounds`.",
target
),
});
}
Op::CallIndirect(_) => {
return Err(VerifyError {
chunk_name: chunk.name.clone(),
message: alloc::string::String::from(
"CallIndirect resolves its target chunk at runtime and cannot be statically bounded for WCET/WCMU analysis. First-class function dispatch is not admitted by `verify_resource_bounds`. Restrict the program to direct calls.",
),
});
}
_ => {}
}
}
}
let n = module.chunks.len();
let mut chunk_wcmu: Vec<Option<(u32, u32)>> = alloc::vec![None; 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,
};
let result = compute_chunk_wcmu(chunk, &resolver)?;
chunk_wcmu[chunk_idx] = Some(result);
}
Ok(chunk_wcmu
.into_iter()
.map(|o| o.unwrap_or((0, 0)))
.collect())
}
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) -> Result<(u32, u32), 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)?.unwrap_or(McuResult::empty());
let stack_slots = chunk.local_count as u32 + body.peak_above_initial;
let stack_bytes = stack_slots * crate::bytecode::VALUE_SLOT_SIZE_BYTES;
Ok((stack_bytes, body.heap_total))
}
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(module, arena_capacity, native_wcmu)
}
pub fn verify_resource_bounds_with_natives(
module: &Module,
arena_capacity: usize,
native_wcmu: &[u32],
) -> Result<(), VerifyError> {
let chunk_wcmu = module_wcmu(module, native_wcmu)?;
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 verify(module: &Module) -> Result<(), VerifyError> {
for chunk in &module.chunks {
let name = &chunk.name;
let ops = &chunk.ops;
let mut block_stack: Vec<(BlockKind, usize)> = Vec::new();
let mut loop_depth: usize = 0;
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));
}
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
),
});
}
}
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()
),
});
}
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]
),
});
}
}
Op::EndIf => 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),
});
}
},
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;
}
Op::EndLoop(target) => {
let t = *target as usize;
match block_stack.pop() {
Some((BlockKind::Loop, loop_ip)) => {
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
),
});
}
}
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),
});
}
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()
),
});
}
}
Op::BreakIf(target) => {
if loop_depth == 0 {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!("BreakIf at {} outside any Loop block", ip),
});
}
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()
),
});
}
}
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
),
});
}
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
),
});
}
}
_ => {}
}
}
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),
});
}
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,
_ => {}
}
}
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
),
});
}
if stream_count > 0 {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"Func block contains {} Stream instruction(s)",
stream_count
),
});
}
if reset_count > 0 {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"Func block contains {} Reset instruction(s)",
reset_count
),
});
}
}
BlockType::Reentrant => {
if yield_count == 0 {
return Err(VerifyError {
chunk_name: name.clone(),
message: String::from("Reentrant block must contain at least one Yield"),
});
}
if stream_count > 0 {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"Reentrant block contains {} Stream instruction(s)",
stream_count
),
});
}
if reset_count > 0 {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"Reentrant block contains {} Reset instruction(s)",
reset_count
),
});
}
}
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
),
});
}
if reset_count != 1 {
return Err(VerifyError {
chunk_name: name.clone(),
message: alloc::format!(
"Stream block must contain exactly one Reset, found {}",
reset_count
),
});
}
if yield_count == 0 {
return Err(VerifyError {
chunk_name: name.clone(),
message: String::from("Stream block must contain at least one 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",
),
});
}
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bytecode::{BlockType, Chunk, ConstValue, Module, Op};
use alloc::vec;
fn make_module(chunks: Vec<Chunk>) -> Module {
Module {
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,
}
}
fn make_chunk(name: &str, ops: Vec<Op>, block_type: BlockType) -> Chunk {
Chunk {
name: String::from(name),
ops,
constants: Vec::new(),
struct_templates: Vec::new(),
local_count: 0,
param_count: 0,
block_type,
}
}
#[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 valid_if_else() {
let chunk = make_chunk(
"main",
vec![
Op::PushTrue, Op::If(5), Op::Const(0), Op::Const(0), Op::Else(6), Op::Const(0), Op::EndIf, Op::Return, ],
BlockType::Func,
);
let module = make_module(vec![chunk]);
assert!(verify(&module).is_ok());
}
#[test]
fn valid_loop() {
let chunk = make_chunk(
"main",
vec![
Op::Loop(4), Op::PushTrue, Op::BreakIf(4), Op::EndLoop(1), Op::PushUnit, 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::Pop, 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::Pop, 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::PushUnit, 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::PushUnit, 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::PushUnit, 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::PushUnit, 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::PushUnit, Op::Yield, Op::Pop],
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::PushUnit, Op::Yield, Op::Pop, 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::PushTrue,
Op::If(3), Op::PushUnit,
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::PushTrue, 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::PushTrue, Op::BreakIf(4), Op::EndLoop(0), Op::PushUnit, 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::PushTrue, Op::If(3), Op::PushUnit, Op::Else(5), Op::PushUnit, Op::PushUnit, Op::Return, ],
BlockType::Func,
);
let module = make_module(vec![chunk]);
let err = verify(&module).unwrap_err();
assert!(err.message.contains("expected EndIf"));
}
#[test]
fn mismatched_if_endloop_fails() {
let chunk = make_chunk(
"bad",
vec![
Op::PushTrue, Op::If(3), Op::PushUnit, 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() -> i64 { 42 }",
"fn main() -> i64 { if true { 1 } else { 2 } }",
"fn main() -> i64 { let sum = 0; for i in 0..5 { let x = sum + i; } sum }",
"fn double(x: i64) -> i64 { x * 2 }\nfn main() -> i64 { double(21) }",
"fn main() -> String { let x = 1; match x { 1 => \"one\", _ => \"other\" } }",
"loop tick(x: i64) -> i64 { 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::Pop, 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::PushTrue, Op::If(6), Op::GetLocal(0), Op::Yield, Op::Else(9), Op::GetLocal(0), Op::Yield, Op::Pop, Op::EndIf, Op::Pop, 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::Pop, Op::PushTrue, Op::If(8), Op::PushUnit, Op::Else(10), Op::PushUnit, Op::Pop, 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::PushTrue, Op::If(6), Op::GetLocal(0), Op::Yield, Op::Else(9), Op::PushUnit, Op::Pop, Op::PushUnit, Op::EndIf, Op::Pop, 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::PushTrue, Op::If(6), Op::GetLocal(0), Op::Yield, Op::Pop, 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::PushTrue, Op::BreakIf(8), Op::GetLocal(0), Op::Yield, Op::Pop, 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::Pop, Op::Loop(9), Op::PushTrue, Op::BreakIf(9), Op::PushUnit, 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: i64) -> i64 { 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::PushUnit.cost(), 1);
assert_eq!(Op::GetLocal(0).cost(), 1);
assert_eq!(Op::SetLocal(0).cost(), 1);
assert_eq!(Op::Pop.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(0).cost(), 3);
assert_eq!(Op::NewStruct(0).cost(), 5);
assert_eq!(Op::NewArray(0).cost(), 5);
assert_eq!(Op::NewTuple(0).cost(), 5);
assert_eq!(Op::Call(0, 0).cost(), 10);
assert_eq!(Op::CallNative(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::Pop, 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::PushTrue, Op::If(5), Op::Add, Op::Else(7), Op::Div, Op::Mul, Op::EndIf, Op::Yield, Op::Pop, 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::PushUnit, 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: i64) -> i64 { 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 {
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,
data_layout: Some(DataLayout {
slots: vec![DataSlot {
name: String::from("ctx.x"),
}],
}),
};
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::PushUnit, Op::Return],
BlockType::Func,
);
let module = Module {
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,
data_layout: Some(DataLayout {
slots: vec![
DataSlot {
name: String::from("ctx.a"),
},
DataSlot {
name: String::from("ctx.b"),
},
],
}),
};
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::Pop, 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::PushTrue, Op::If(7), Op::Const(0), Op::Const(0), Op::Const(0), Op::Else(9), Op::Const(0), Op::Pop, Op::EndIf, Op::Pop, Op::Pop, Op::Pop, Op::GetLocal(0), Op::Yield, Op::Pop, 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() {
use crate::bytecode::{StructTemplate, VALUE_SLOT_SIZE_BYTES};
let mut chunk = make_chunk(
"tick",
vec![
Op::Stream, Op::Const(0), Op::Const(0), Op::NewStruct(0), Op::Yield, Op::Reset, ],
BlockType::Stream,
);
chunk.local_count = 0;
chunk.constants = vec![ConstValue::Int(0)];
chunk.struct_templates = vec![StructTemplate {
type_name: String::from("Point"),
field_names: vec![String::from("x"), String::from("y")],
}];
let (_stack, heap) = wcmu_stream_iteration(&chunk).unwrap();
assert_eq!(heap, 2 * VALUE_SLOT_SIZE_BYTES);
}
#[test]
fn wcmu_new_array_heap() {
use crate::bytecode::VALUE_SLOT_SIZE_BYTES;
let mut chunk = make_chunk(
"tick",
vec![
Op::Stream,
Op::Const(0),
Op::Const(0),
Op::Const(0),
Op::NewArray(3),
Op::Yield,
Op::Reset,
],
BlockType::Stream,
);
chunk.constants = vec![ConstValue::Int(0)];
let (_stack, heap) = wcmu_stream_iteration(&chunk).unwrap();
assert_eq!(heap, 3 * VALUE_SLOT_SIZE_BYTES);
}
#[test]
fn wcmu_non_stream_errors() {
let chunk = make_chunk("main", vec![Op::PushUnit, 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::PushUnit, Op::Yield, Op::Pop, 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::NewArray(2),
Op::Yield,
Op::Pop,
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_rejects_recursive_closures() {
let chunk = make_chunk(
"tick",
vec![
Op::Stream,
Op::MakeRecursiveClosure(0, 0),
Op::Pop,
Op::PushUnit,
Op::Yield,
Op::Pop,
Op::Reset,
],
BlockType::Stream,
);
let module = make_module(vec![chunk]);
let err = verify_resource_bounds(&module, 1024 * 1024).unwrap_err();
assert!(
err.message.contains("MakeRecursiveClosure")
|| err.message.contains("unbounded recursion"),
"unexpected error message: {}",
err.message,
);
}
#[test]
fn verify_resource_bounds_rejects_call_indirect() {
let chunk = make_chunk(
"tick",
vec![
Op::Stream,
Op::PushFunc(0),
Op::CallIndirect(0),
Op::Pop,
Op::PushUnit,
Op::Yield,
Op::Pop,
Op::Reset,
],
BlockType::Stream,
);
let module = make_module(vec![chunk]);
let err = verify_resource_bounds(&module, 1024 * 1024).unwrap_err();
assert!(
err.message.contains("CallIndirect"),
"unexpected error message: {}",
err.message,
);
}
#[test]
fn verify_resource_bounds_admits_push_func_without_call_indirect() {
let chunk = make_chunk(
"tick",
vec![Op::Stream, Op::PushFunc(0), Op::Yield, Op::Pop, Op::Reset],
BlockType::Stream,
);
let module = make_module(vec![chunk]);
verify_resource_bounds(&module, 1024 * 1024).unwrap();
}
#[test]
fn verify_resource_bounds_skips_non_stream() {
let chunk = make_chunk("util", vec![Op::PushUnit, 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::PushUnit, Op::Yield, Op::Pop, 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::NewArray(3),
Op::Pop,
Op::PushUnit,
Op::Return,
],
BlockType::Func,
);
callee.constants = vec![ConstValue::Int(0)];
let stream_chunk = make_chunk(
"tick",
vec![
Op::Stream, Op::Call(0, 0), Op::Pop, Op::PushUnit, Op::Yield, Op::Pop, 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::CallNative(0, 0), Op::Pop, Op::PushUnit, Op::Yield, Op::Pop, 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::CallNative(0, 0),
Op::Pop,
Op::PushUnit,
Op::Yield,
Op::Pop,
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_topological_handles_chain() {
let leaf = make_chunk("leaf", vec![Op::PushUnit, 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::Pop,
Op::PushUnit,
Op::Yield,
Op::Pop,
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: i64) -> i64 { \
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 expected = 5 * 4 * crate::bytecode::VALUE_SLOT_SIZE_BYTES;
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: i64) -> i64 { \
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: i64) -> i64 { \
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(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)];
let count = extract_loop_iteration_bound(&chunk, 4);
assert_eq!(count, Some(10));
}
#[test]
fn extract_loop_iteration_bound_returns_none_for_non_canonical() {
let chunk = make_chunk(
"test",
vec![Op::Loop(4), Op::PushTrue, 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::PushTrue, Op::BreakIf(7), Op::PushUnit, Op::Pop, Op::EndLoop(2), Op::Yield, Op::Pop, 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::PushUnit, Op::Yield, Op::Pop, 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(15),
Op::GetLocal(0),
Op::GetLocal(1),
Op::CmpGe,
Op::BreakIf(15),
Op::Const(2),
Op::Const(2),
Op::NewArray(2), Op::Pop,
Op::EndLoop(6),
Op::PushUnit,
Op::Yield,
Op::Pop,
Op::Reset,
],
BlockType::Stream,
);
chunk.constants = vec![ConstValue::Int(5), ConstValue::Int(5), ConstValue::Int(0)];
chunk.local_count = 2;
let (_stack, heap) = wcmu_stream_iteration(&chunk).unwrap();
assert_eq!(heap, 0);
}
}