mirror of
https://github.com/allaunthefox/SilverSight.git
synced 2026-07-31 01:25:21 +00:00
Python port rewritten to match spec: - Added Q0_16, PUSH_Q0, PUSH_BOOL as separate opcodes - Added V6 comparison (lt_q16_v6) - Added floor division (Lean Int.ediv) - Added stack depth limit (AVM_MAX_STACK = 1024) - Added type checking in exec_prim - All 10 tests passing Go AVM port: added test_avm_test.go with 8 test cases Milestone: Python → ✅, Go → 🔄
199 lines
7.7 KiB
Python
199 lines
7.7 KiB
Python
"""
|
|
AVM ISA v1 — Python Port
|
|
Strict functional: step(state, program) -> state, run(state, program, fuel) -> state
|
|
"""
|
|
from dataclasses import dataclass, field
|
|
from typing import List, Union, Tuple
|
|
|
|
# ── Constants ────────────────────────────────────────────────────────
|
|
Q16_SCALE = 65536
|
|
AVM_CLAMP_MIN = -2147483647
|
|
AVM_CLAMP_MAX = 2147483647
|
|
AVM_Q0_MIN = -32767
|
|
AVM_Q0_MAX = 32767
|
|
AVM_MAX_STACK = 1024
|
|
|
|
def avm_clamp(x):
|
|
return max(AVM_CLAMP_MIN, min(AVM_CLAMP_MAX, int(x)))
|
|
|
|
def avm_q0_clamp(x):
|
|
return max(AVM_Q0_MIN, min(AVM_Q0_MAX, int(x)))
|
|
|
|
def floor_div(a, b):
|
|
if b == 0: raise ValueError("div by zero")
|
|
q, r = divmod(a, b)
|
|
return q - 1 if r and (a ^ b) < 0 else q
|
|
|
|
def lt_q16_v6(a, b):
|
|
sa, sb = a < 0, b < 0
|
|
return sa if sa != sb else a < b
|
|
|
|
def q16_mul(a, b):
|
|
return floor_div(a * b, Q16_SCALE)
|
|
|
|
def q16_div(a, b):
|
|
if b == 0: raise ValueError("div by zero")
|
|
return floor_div(a * Q16_SCALE, b)
|
|
|
|
# ── Types ────────────────────────────────────────────────────────────
|
|
class Prim:
|
|
ADD_SAT_Q0, SUB_SAT_Q0, ADD_SAT_Q16, SUB_SAT_Q16, \
|
|
MUL_SAT_Q16, DIV_SAT_Q16, LT_Q16, EQ_Q16, AND, OR, NOT = range(11)
|
|
|
|
prim_arity = {Prim.NOT: 1}.get # all others are 2
|
|
|
|
class AvmVal:
|
|
def __init__(self, ty, val):
|
|
self.ty = ty
|
|
self.val = val
|
|
@staticmethod
|
|
def q0(x): return AvmVal('q0', avm_q0_clamp(x))
|
|
@staticmethod
|
|
def q16(x): return AvmVal('q16', avm_clamp(x))
|
|
@staticmethod
|
|
def bool(x): return AvmVal('bool', bool(x))
|
|
|
|
class Instr:
|
|
PUSH, PUSH_BOOL, PUSH_Q0, POP, DUP, SWAP, LOAD, STORE, \
|
|
JUMP, JUMP_IF, PRIM, HALT = range(12)
|
|
|
|
Q0_OPS = {Prim.ADD_SAT_Q0, Prim.SUB_SAT_Q0}
|
|
Q16_BIN_OPS = {Prim.ADD_SAT_Q16, Prim.SUB_SAT_Q16, Prim.MUL_SAT_Q16, Prim.DIV_SAT_Q16, Prim.LT_Q16, Prim.EQ_Q16}
|
|
BOOL_BIN_OPS = {Prim.AND, Prim.OR}
|
|
|
|
def check_type(val_ty, expected, label):
|
|
if val_ty != expected:
|
|
raise TypeError(f"type mismatch: expected {expected}, got {val_ty}")
|
|
|
|
def make_instr(op, arg=None, arg2=None):
|
|
return {'op': op, 'arg': arg, 'arg2': arg2}
|
|
|
|
def push_q16(x): return make_instr(Instr.PUSH, avm_clamp(x))
|
|
def push_bool(x): return make_instr(Instr.PUSH_BOOL, 0, x)
|
|
def push_q0(x): return make_instr(Instr.PUSH_Q0, avm_q0_clamp(x))
|
|
def pop(): return make_instr(Instr.POP)
|
|
def dup(): return make_instr(Instr.DUP)
|
|
def swap(): return make_instr(Instr.SWAP)
|
|
def load(i): return make_instr(Instr.LOAD, i)
|
|
def store(i): return make_instr(Instr.STORE, i)
|
|
def jump(t): return make_instr(Instr.JUMP, t)
|
|
def jump_if(t): return make_instr(Instr.JUMP_IF, t)
|
|
def prim(p): return make_instr(Instr.PRIM, p)
|
|
def halt(): return make_instr(Instr.HALT)
|
|
|
|
@dataclass
|
|
class State:
|
|
pc: int = 0
|
|
stack: list = field(default_factory=list)
|
|
locals: list = field(default_factory=lambda: [None] * 16)
|
|
halted: bool = False
|
|
|
|
# ── Primitive execution ──────────────────────────────────────────────
|
|
def exec_prim(op, a, b=None):
|
|
if op == Prim.ADD_SAT_Q0:
|
|
check_type(a.ty, 'q0', 'a'); check_type(b.ty, 'q0', 'b')
|
|
return AvmVal.q0(a.val + b.val)
|
|
if op == Prim.SUB_SAT_Q0:
|
|
check_type(a.ty, 'q0', 'a'); check_type(b.ty, 'q0', 'b')
|
|
return AvmVal.q0(a.val - b.val)
|
|
if op == Prim.ADD_SAT_Q16:
|
|
check_type(a.ty, 'q16', 'a'); check_type(b.ty, 'q16', 'b')
|
|
return AvmVal.q16(a.val + b.val)
|
|
if op == Prim.SUB_SAT_Q16:
|
|
check_type(a.ty, 'q16', 'a'); check_type(b.ty, 'q16', 'b')
|
|
return AvmVal.q16(a.val - b.val)
|
|
if op == Prim.MUL_SAT_Q16:
|
|
check_type(a.ty, 'q16', 'a'); check_type(b.ty, 'q16', 'b')
|
|
return AvmVal.q16(q16_mul(a.val, b.val))
|
|
if op == Prim.DIV_SAT_Q16:
|
|
check_type(a.ty, 'q16', 'a'); check_type(b.ty, 'q16', 'b')
|
|
if b.val == 0: raise ValueError("division by zero")
|
|
return AvmVal.q16(q16_div(a.val, b.val))
|
|
if op == Prim.LT_Q16:
|
|
check_type(a.ty, 'q16', 'a'); check_type(b.ty, 'q16', 'b')
|
|
return AvmVal.bool(lt_q16_v6(a.val, b.val))
|
|
if op == Prim.EQ_Q16:
|
|
check_type(a.ty, 'q16', 'a'); check_type(b.ty, 'q16', 'b')
|
|
return AvmVal.bool(a.val == b.val)
|
|
if op == Prim.AND:
|
|
check_type(a.ty, 'bool', 'a'); check_type(b.ty, 'bool', 'b')
|
|
return AvmVal.bool(a.val and b.val)
|
|
if op == Prim.OR:
|
|
check_type(a.ty, 'bool', 'a'); check_type(b.ty, 'bool', 'b')
|
|
return AvmVal.bool(a.val or b.val)
|
|
if op == Prim.NOT:
|
|
check_type(a.ty, 'bool', 'a')
|
|
return AvmVal.bool(not a.val)
|
|
raise TypeError(f"unknown prim {op}")
|
|
|
|
# ── Step ─────────────────────────────────────────────────────────────
|
|
def step(state, program):
|
|
if state.halted: raise ValueError("halted")
|
|
if state.pc < 0 or state.pc >= len(program):
|
|
return State(state.pc, list(state.stack), list(state.locals), True)
|
|
|
|
instr = program[state.pc]
|
|
stack = list(state.stack)
|
|
locals = list(state.locals)
|
|
pc = state.pc + 1
|
|
halted = False
|
|
|
|
growing = instr['op'] in (Instr.PUSH, Instr.PUSH_BOOL, Instr.PUSH_Q0, Instr.DUP, Instr.LOAD)
|
|
if growing and len(stack) >= AVM_MAX_STACK:
|
|
raise ValueError("stack overflow")
|
|
|
|
if instr['op'] == Instr.PUSH:
|
|
stack.append(AvmVal.q16(instr['arg']))
|
|
elif instr['op'] == Instr.PUSH_BOOL:
|
|
stack.append(AvmVal.bool(instr['arg2']))
|
|
elif instr['op'] == Instr.PUSH_Q0:
|
|
stack.append(AvmVal.q0(instr['arg']))
|
|
elif instr['op'] == Instr.POP:
|
|
if not stack: raise ValueError("empty stack")
|
|
stack.pop()
|
|
elif instr['op'] == Instr.DUP:
|
|
if not stack: raise ValueError("empty stack")
|
|
stack.append(stack[-1])
|
|
elif instr['op'] == Instr.SWAP:
|
|
if len(stack) < 2: raise ValueError("stack underflow")
|
|
stack[-1], stack[-2] = stack[-2], stack[-1]
|
|
elif instr['op'] == Instr.LOAD:
|
|
i = instr['arg']
|
|
if i >= len(locals) or locals[i] is None: raise ValueError("missing local")
|
|
stack.append(locals[i])
|
|
elif instr['op'] == Instr.STORE:
|
|
if not stack: raise ValueError("empty stack")
|
|
i = instr['arg']
|
|
if i >= len(locals): raise ValueError("missing local")
|
|
locals[i] = stack.pop()
|
|
elif instr['op'] == Instr.JUMP:
|
|
if instr['arg'] < 0 or instr['arg'] >= len(program): raise ValueError("jump OOB")
|
|
pc = instr['arg']
|
|
elif instr['op'] == Instr.JUMP_IF:
|
|
if not stack: raise ValueError("empty stack")
|
|
v = stack.pop()
|
|
if v.ty != 'bool': raise TypeError("type mismatch")
|
|
if v.val:
|
|
if instr['arg'] < 0 or instr['arg'] >= len(program): raise ValueError("jump OOB")
|
|
pc = instr['arg']
|
|
elif instr['op'] == Instr.PRIM:
|
|
p = instr['arg']
|
|
arity = 1 if p == Prim.NOT else 2
|
|
if len(stack) < arity: raise ValueError("stack underflow")
|
|
b = stack.pop() if arity >= 2 else None
|
|
a = stack.pop()
|
|
stack.append(exec_prim(p, a, b))
|
|
elif instr['op'] == Instr.HALT:
|
|
halted = True
|
|
else:
|
|
raise ValueError(f"unknown instr {instr['op']}")
|
|
|
|
return State(pc, stack, locals, halted)
|
|
|
|
# ── Run (fuel-bounded) ──────────────────────────────────────────────
|
|
def run(initial, program, fuel=1000):
|
|
state = initial
|
|
for _ in range(fuel):
|
|
if state.halted: return state
|
|
state = step(state, program)
|
|
return state
|