SilverSight/rust/src/avm/mod.rs
allaun b533b8d6ca fix(avm-review): resolve adversarial review findings
Rust fixes:
- Remove Q0_16 Not arm (was silently returning Bool(false) instead of type error)
- Replace inline floor division with floor_div() calls in MulSatQ16/DivSatQ16
- Fix comment ranges for Q0_16 [-32767, 32767] and Q16_16 [-2147483647, 2147483647]

Go fixes:
- Add euclideanDiv() matching Lean Int.ediv (remainder ≥ 0)
- Use euclideanDiv for MulSatQ16 and DivSatQ16
- Change Locals from []*Val to []Val (dangling pointer fix)
- Step on halted state returns &s, nil (Lean returns Ok s)
- OOB PC returns error instead of silent halt
- Halt does not increment PC (Lean: PC unchanged)

All Go tests pass (9/9)
2026-06-30 19:01:47 -05:00

383 lines
14 KiB
Rust

//! AVM ISA v1 — Strict Functional Execution Engine
//!
//! Mirrors `formal/SilverSight/AVMIsa/` (Lean 4).
//! The instruction set IS the execution — no interpreter dispatch table.
//!
//! Design rules:
//! - Closed-world type universe: Q0_16, Q16_16, Bool
//! - Pure functions: step(State) -> State, run(State) -> State
//! - No Float in compute paths (Q16_16 fixed-point only)
//! - Fuel-bounded run loop (total termination)
use crate::q16::*;
// ── AVM-specific constants ──────────────────────────────────────────
// AVM uses symmetric clamping [-2147483647, 2147483647] (excludes INT32_MIN)
// to guarantee negation is a perfect involution.
const AVM_CLAMP_MIN: i32 = -2147483647;
const AVM_CLAMP_MAX: i32 = 2147483647;
const AVM_Q0_MIN: i32 = -32767;
const AVM_Q0_MAX: i32 = 32767;
const AVM_MAX_STACK: usize = 1024;
/// Floor division matching Lean `Int.ediv` (rounds toward negative infinity).
fn floor_div(a: i32, b: i32) -> i32 {
let d = a / b;
let r = a % b;
if r != 0 && ((a ^ b) < 0) { d - 1 } else { d }
}
/// AVM saturating clamp: [-2147483647, 2147483647]
fn avm_clamp(v: i64) -> i32 {
if v > AVM_CLAMP_MAX as i64 { AVM_CLAMP_MAX }
else if v < AVM_CLAMP_MIN as i64 { AVM_CLAMP_MIN }
else { v as i32 }
}
/// V6 sign-decomposition comparison for Q16_16.
fn lt_q16_v6(a: i32, b: i32) -> bool {
let sa = a < 0;
let sb = b < 0;
if sa != sb { sa } else { a < b }
}
// ── Types ───────────────────────────────────────────────────────────
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AvmTy { Q0_16, Q16_16, Bool }
// ── Values ──────────────────────────────────────────────────────────
#[derive(Debug, Clone, PartialEq)]
pub enum AvmVal {
Q0_16(i32), // raw Q0_16: [-32767, 32767] symmetric
Q16_16(i32), // raw Q16_16: [-2147483647, 2147483647] symmetric
Bool(bool),
}
impl AvmVal {
pub fn ty(&self) -> AvmTy {
match self {
AvmVal::Q0_16(_) => AvmTy::Q0_16,
AvmVal::Q16_16(_) => AvmTy::Q16_16,
AvmVal::Bool(_) => AvmTy::Bool,
}
}
}
// ── Primitives ──────────────────────────────────────────────────────
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Prim {
AddSatQ0, SubSatQ0,
AddSatQ16, SubSatQ16, MulSatQ16, DivSatQ16,
LtQ16, EqQ16,
And, Or, Not,
}
// ── Instructions ────────────────────────────────────────────────────
#[derive(Debug, Clone, PartialEq)]
pub enum Instr {
Push(AvmVal),
Pop,
Dup,
Swap,
Load(usize),
Store(usize),
Jump(usize),
JumpIf(usize),
Prim(Prim),
Halt,
}
// ── Step Error ─────────────────────────────────────────────────────
#[derive(Debug, Clone, PartialEq)]
pub enum StepError {
EmptyStack,
StackUnderflow(&'static str), // operation needs N items, has <N
TypeMismatch { expected: AvmTy, got: AvmTy },
MissingLocal(usize),
DivisionByZero,
JumpOutOfBounds(usize),
StackOverflow,
Halted,
}
// ── State ──────────────────────────────────────────────────────────
#[derive(Debug, Clone, PartialEq)]
pub struct State {
pub pc: usize,
pub stack: Vec<AvmVal>,
pub locals: Vec<Option<AvmVal>>,
pub halted: bool,
}
impl State {
pub fn new(locals_size: usize) -> Self {
Self {
pc: 0,
stack: Vec::new(),
locals: vec![None; locals_size],
halted: false,
}
}
}
// ── Primitive execution (strict functional) ─────────────────────────
fn exec_prim(op: Prim, a: &AvmVal, b: Option<&AvmVal>) -> Result<AvmVal, StepError> {
match (op, a, b) {
// Q0_16 binary (symmetric clamp)
(Prim::AddSatQ0, AvmVal::Q0_16(x), Some(AvmVal::Q0_16(y))) => {
let r = (*x as i64) + (*y as i64);
Ok(AvmVal::Q0_16(if r > AVM_Q0_MAX as i64 { AVM_Q0_MAX }
else if r < AVM_Q0_MIN as i64 { AVM_Q0_MIN } else { r as i32 }))
}
(Prim::SubSatQ0, AvmVal::Q0_16(x), Some(AvmVal::Q0_16(y))) => {
let r = (*x as i64) - (*y as i64);
Ok(AvmVal::Q0_16(if r > AVM_Q0_MAX as i64 { AVM_Q0_MAX }
else if r < AVM_Q0_MIN as i64 { AVM_Q0_MIN } else { r as i32 }))
}
// Q16_16 binary (symmetric clamp, floor division)
(Prim::AddSatQ16, AvmVal::Q16_16(x), Some(AvmVal::Q16_16(y))) => {
Ok(AvmVal::Q16_16(avm_clamp((*x as i64) + (*y as i64))))
}
(Prim::SubSatQ16, AvmVal::Q16_16(x), Some(AvmVal::Q16_16(y))) => {
Ok(AvmVal::Q16_16(avm_clamp((*x as i64) - (*y as i64))))
}
(Prim::MulSatQ16, AvmVal::Q16_16(x), Some(AvmVal::Q16_16(y))) => {
Ok(AvmVal::Q16_16(avm_clamp(floor_div((*x as i64) * (*y as i64), Q16_SCALE as i64))))
}
(Prim::DivSatQ16, AvmVal::Q16_16(x), Some(AvmVal::Q16_16(y))) => {
if *y == 0 { return Err(StepError::DivisionByZero); }
Ok(AvmVal::Q16_16(avm_clamp(floor_div((*x as i64) * Q16_SCALE as i64, *y as i64))))
}
// Comparisons (V6 sign-decomposition)
(Prim::LtQ16, AvmVal::Q16_16(x), Some(AvmVal::Q16_16(y))) => {
Ok(AvmVal::Bool(lt_q16_v6(*x, *y)))
}
(Prim::EqQ16, AvmVal::Q16_16(x), Some(AvmVal::Q16_16(y))) => {
Ok(AvmVal::Bool(x == y))
}
// Boolean
(Prim::And, AvmVal::Bool(x), Some(AvmVal::Bool(y))) => {
Ok(AvmVal::Bool(*x && *y))
}
(Prim::Or, AvmVal::Bool(x), Some(AvmVal::Bool(y))) => {
Ok(AvmVal::Bool(*x || *y))
}
(Prim::Not, AvmVal::Bool(x), None) => {
Ok(AvmVal::Bool(!*x))
}
// Type mismatches
_ => {
let expected = match op {
Prim::AddSatQ0 | Prim::SubSatQ0 => AvmTy::Q0_16,
Prim::AddSatQ16 | Prim::SubSatQ16
| Prim::MulSatQ16 | Prim::DivSatQ16
| Prim::LtQ16 | Prim::EqQ16 => AvmTy::Q16_16,
Prim::And | Prim::Or | Prim::Not => AvmTy::Bool,
};
Err(StepError::TypeMismatch { expected, got: a.ty() })
}
}
}
// ── Step (pure function: State → Result<State, StepError>) ─────────
pub fn step(s: &State, program: &[Instr]) -> Result<State, StepError> {
if s.halted { return Err(StepError::Halted); }
if s.pc >= program.len() {
return Ok(State {
pc: s.pc,
stack: s.stack.clone(),
locals: s.locals.clone(),
halted: true,
});
}
let instr = &program[s.pc];
let mut stack = s.stack.clone();
let mut locals = s.locals.clone();
let mut pc = s.pc + 1;
let mut halted = false;
fn check_stack(stack: &[AvmVal]) -> Result<(), StepError> {
if stack.len() >= AVM_MAX_STACK {
return Err(StepError::StackOverflow);
}
Ok(())
}
match instr {
Instr::Push(v) => {
check_stack(&stack)?;
stack.push(v.clone());
}
Instr::Pop => {
stack.pop().ok_or(StepError::EmptyStack)?;
}
Instr::Dup => {
stack.last().ok_or(StepError::EmptyStack)?;
check_stack(&stack)?;
let v = stack.last().ok_or(StepError::EmptyStack)?.clone();
stack.push(v);
}
Instr::Swap => {
let a = stack.pop().ok_or(StepError::StackUnderflow("swap"))?;
let b = stack.pop().ok_or(StepError::StackUnderflow("swap"))?;
stack.push(a);
stack.push(b);
}
Instr::Load(i) => {
let v = locals.get(*i).ok_or(StepError::MissingLocal(*i))?
.clone().ok_or(StepError::MissingLocal(*i))?;
check_stack(&stack)?;
stack.push(v);
}
Instr::Store(i) => {
let v = stack.pop().ok_or(StepError::EmptyStack)?;
if *i >= locals.len() {
return Err(StepError::MissingLocal(*i));
}
locals[*i] = Some(v);
}
Instr::Jump(target) => {
if *target >= program.len() {
return Err(StepError::JumpOutOfBounds(*target));
}
pc = *target;
}
Instr::JumpIf(target) => {
let cond = stack.pop().ok_or(StepError::EmptyStack)?;
match cond {
AvmVal::Bool(true) => {
if *target >= program.len() {
return Err(StepError::JumpOutOfBounds(*target));
}
pc = *target;
}
AvmVal::Bool(false) => { /* fallthrough */ }
_ => return Err(StepError::TypeMismatch { expected: AvmTy::Bool, got: cond.ty() }),
}
}
Instr::Prim(p) => {
let arity = match p {
Prim::Not => 1,
_ => 2,
};
let b = if arity >= 2 { Some(stack.pop().ok_or(StepError::StackUnderflow("prim"))?) } else { None };
let a = stack.pop().ok_or(StepError::StackUnderflow("prim"))?;
let result = exec_prim(*p, &a, b.as_ref())?;
stack.push(result);
}
Instr::Halt => {
halted = true;
}
}
Ok(State { pc, stack, locals, halted })
}
// ── Run (fuel-bounded, pure) ──────────────────────────────────────
pub fn run(initial: &State, program: &[Instr], fuel: usize) -> Result<State, StepError> {
let mut state = initial.clone();
for _ in 0..fuel {
if state.halted { return Ok(state); }
state = step(&state, program)?;
}
Ok(state)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_program() -> (Vec<Instr>, State) {
// Program: push Q16(5), push Q16(3), add, halt
let prog = vec![
Instr::Push(AvmVal::Q16_16(5 * 65536)),
Instr::Push(AvmVal::Q16_16(3 * 65536)),
Instr::Prim(Prim::AddSatQ16),
Instr::Halt,
];
let state = State::new(0);
(prog, state)
}
#[test]
fn test_add_q16() {
let (prog, state) = make_program();
let final_state = run(&state, &prog, 100).unwrap();
assert!(final_state.halted);
assert_eq!(final_state.stack.len(), 1);
assert_eq!(final_state.stack[0], AvmVal::Q16_16(8 * 65536));
}
#[test]
fn test_jump_if() {
// Program: push Bool(true), jump_if(4), push Q16(0), halt, push Q16(1), halt
let prog = vec![
Instr::Push(AvmVal::Bool(true)),
Instr::JumpIf(4),
Instr::Push(AvmVal::Q16_16(0)),
Instr::Halt,
Instr::Push(AvmVal::Q16_16(65536)),
Instr::Halt,
];
let state = State::new(0);
let final_state = run(&state, &prog, 100).unwrap();
assert!(final_state.halted);
assert_eq!(final_state.stack[0], AvmVal::Q16_16(65536));
assert_eq!(final_state.pc, 6);
}
#[test]
fn test_store_load() {
// Program: push 42, store(0), load(0), halt
let prog = vec![
Instr::Push(AvmVal::Q16_16(42 * 65536)),
Instr::Store(0),
Instr::Load(0),
Instr::Halt,
];
let state = State::new(1);
let final_state = run(&state, &prog, 100).unwrap();
assert_eq!(final_state.stack[0], AvmVal::Q16_16(42 * 65536));
}
#[test]
fn test_prim_type_checking() {
// Program: push Bool(true), push Q16(5), add → type error
let prog = vec![
Instr::Push(AvmVal::Bool(true)),
Instr::Push(AvmVal::Q16_16(65536)),
Instr::Prim(Prim::AddSatQ16),
Instr::Halt,
];
let state = State::new(0);
let result = run(&state, &prog, 100);
assert!(result.is_err());
match result {
Err(StepError::TypeMismatch { .. }) => {} // expected
_ => panic!("Expected TypeMismatch, got {:?}", result),
}
}
#[test]
fn test_division_by_zero() {
let prog = vec![
Instr::Push(AvmVal::Q16_16(65536)),
Instr::Push(AvmVal::Q16_16(0)),
Instr::Prim(Prim::DivSatQ16),
Instr::Halt,
];
let state = State::new(0);
let result = run(&state, &prog, 100);
assert_eq!(result, Err(StepError::DivisionByZero));
}
}