prog8/tinyvm/vm.py
Irmen de Jong ed8267b1ec tinyvm
2018-02-25 16:43:00 +01:00

487 lines
19 KiB
Python

# 8/16 bit virtual machine
# machine specs:
# MEMORY: 64K bytes, treated as one single array, indexed per byte, ONLY DATA - NO CODE
# elements addressable as one of three elementary data types:
# 8-bit byte (singed and unsigned),
# 16-bit words (two 8-bit bytes, signed and unsigned) (stored in LSB order),
# 5-byte MFLPT floating point
# addressing is possible via byte index (for the $0000-$00ff range) or via an unsigned word.
#
# MEMORY ACCESS: via explicit load and store instructions,
# to put a value onto the stack or store the value on the top of the stack,
# or in one of the dynamic variables.
#
# I/O: either via programmed I/O routines:
# write [byte to text output/screen],
# read [byte from keyboard],
# wait [till any input comes available],
# check [if input is available)
# or via memory-mapped I/O (text screen matrix, keyboard scan register)
#
# CPU: stack based execution, no registers.
# unlimited dynamic variables (v0, v1, ...) that have a value and a type.
# types:
# 1-bit boolean (can be CONST),
# 8-bit byte (singed and unsigned) (can be CONST),
# 16-bit words (two 8-bit bytes, signed and unsigned) (can be CONST),
# floating point (can be CONST),
# array of bytes (signed and unsigned),
# array of words (signed and unsigned),
# matrix (2-dimensional array) of bytes (signed and unsigned).
#
# push (constant,
# mark, unwind to previous mark.
#
# CPU INSTRUCTIONS:
# stack manipulation mainly:
# nop
# push var / push2 var1, var2
# pop var / pop2 var1, var2
# various arithmetic operations, logical operations, boolean comparison operations
# jump label
# jump_if_true label, jump_if_false label
# jump_if_status_XX label special system dependent status register conditional check such as carry bit or overflow bit)
# return (return values on stack)
# syscall function (special system dependent implementation)
# call function (arguments are on stack)
# enter / exit (block for function, loop)
#
# TIMER INTERRUPT: triggered every 1/60th of a second.
# executes on a DIFFERENT stack and with a different PROGRAM LIST,
# but with the ALL THE SAME DYNAMIC VARIABLES.
#
import time
import itertools
from .core import Instruction, Variable, Block, Program, Opcode, CONDITIONAL_OPCODES
from typing import Dict, List, Tuple, Union
from il65.emit import mflpt5_to_float, to_mflpt5
class ExecutionError(Exception):
pass
class Memory:
def __init__(self):
self.mem = bytearray(65536)
def get_byte(self, index: int) -> int:
return self.mem[index]
def get_sbyte(self, index: int) -> int:
return 256 - self.mem[index]
def get_word(self, index: int) -> int:
return self.mem[index] + 256 * self.mem[index+1]
def get_sword(self, index: int) -> int:
return 65536 - (self.mem[index] + 256 * self.mem[index+1])
def get_float(self, index: int) -> float:
return mflpt5_to_float(self.mem[index: index+5])
def set_byte(self, index: int, value: int) -> None:
self.mem[index] = value
def set_sbyte(self, index: int, value: int) -> None:
self.mem[index] = value + 256
def set_word(self, index: int, value: int) -> None:
hi, lo = divmod(value, 256)
self.mem[index] = lo
self.mem[index+1] = hi
def set_sword(self, index: int, value: int) -> None:
hi, lo = divmod(value + 65536, 256)
self.mem[index] = lo
self.mem[index+1] = hi
def set_float(self, index: int, value: float) -> None:
self.mem[index: index+5] = to_mflpt5(value)
StackValueType = Union[bool, int, float, bytearray]
class Stack:
def __init__(self):
self.stack = []
def debug_peek(self, size: int) -> List[StackValueType]:
return self.stack[-size:]
def size(self) -> int:
return len(self.stack)
def pop(self) -> StackValueType:
return self.stack.pop()
def pop2(self) -> Tuple[StackValueType, StackValueType]:
return self.stack.pop(), self.stack.pop()
def pop3(self) -> Tuple[StackValueType, StackValueType, StackValueType]:
return self.stack.pop(), self.stack.pop(), self.stack.pop()
def push(self, item: StackValueType) -> None:
self._typecheck(item)
self.stack.append(item)
def push2(self, first: StackValueType, second: StackValueType) -> None:
self._typecheck(first)
self._typecheck(second)
self.stack.append(first)
self.stack.append(second)
def push3(self, first: StackValueType, second: StackValueType, third: StackValueType) -> None:
self._typecheck(first)
self._typecheck(second)
self._typecheck(third)
self.stack.extend([first, second, third])
def peek(self) -> StackValueType:
return self.stack[-1] if self.stack else None
def swap(self) -> None:
x = self.stack[-1]
self.stack[-1] = self.stack[-2]
self.stack[-2] = x
def _typecheck(self, value: StackValueType):
if type(value) not in (bool, int, float, bytearray):
raise TypeError("stack can only contain bool, int, float, bytearray")
# noinspection PyPep8Naming,PyUnusedLocal,PyMethodMayBeStatic
class VM:
str_encoding = "iso-8859-15" # @todo machine encoding via cbmcodecs or something?
str_alt_encoding = "iso-8859-15" # @todo machine encoding via cbmcodecs or something?
def __init__(self, program: Program, timerprogram: Program) -> None:
opcode_names = [oc.name for oc in Opcode]
for ocname in opcode_names:
if not hasattr(self, "opcode_" + ocname):
raise NotImplementedError("missing opcode method for " + ocname)
for method in dir(self):
if method.startswith("opcode_"):
if not method[7:] in opcode_names:
raise RuntimeError("opcode method for undefined opcode " + method)
self.memory = Memory()
self.main_stack = Stack()
self.timer_stack = Stack()
(self.main_program, self.timer_program), self.variables, self.labels = self.flatten_programs(program, timerprogram)
self.connect_instruction_pointers(self.main_program)
self.connect_instruction_pointers(self.timer_program)
self.program = self.main_program
self.stack = self.main_stack
self.pc = None # type: Instruction
self.previous_pc = None # type: Instruction
assert all(i.next for i in self.main_program if i.opcode != Opcode.TERMINATE), "main: all instrs next must be set"
assert all(i.next for i in self.timer_program if i.opcode != Opcode.TERMINATE), "main: all instrs next must be set"
assert all(i.condnext for i in self.main_program if i.opcode in CONDITIONAL_OPCODES), "timer: all conditional instrs condnext must be set"
assert all(i.condnext for i in self.timer_program if i.opcode in CONDITIONAL_OPCODES), "timer: all conditional instrs condnext must be set"
def flatten_programs(self, *programs: Program) -> Tuple[List[List[Instruction]], Dict[str, Variable], Dict[str, Instruction]]:
variables = {} # type: Dict[str, Variable]
labels = {} # type: Dict[str, Instruction]
flat_programs = [] # type: List[List[Instruction]]
for program in programs:
for block in program.blocks:
flat = self.flatten(block, variables, labels)
flat_programs.append(flat)
return flat_programs, variables, labels
def flatten(self, block: Block, variables: Dict[str, Variable], labels: Dict[str, Instruction]) -> List[Instruction]:
def block_prefix(b: Block) -> str:
if b.parent:
return block_prefix(b.parent) + "." + b.name
else:
return b.name
prefix = block_prefix(block)
instructions = block.instructions
for ins in instructions:
if ins.opcode == Opcode.SYSCALL:
continue
if ins.args:
newargs = []
for a in ins.args:
if type(a) is str:
newargs.append(prefix + "." + a)
else:
newargs.append(a)
ins.args = newargs
for vardef in block.variables:
vname = prefix + "." + vardef.name
assert vname not in variables
variables[vname] = vardef
for name, instr in block.labels.items():
name = prefix + "." + name
assert name not in labels
labels[name] = instr
for subblock in block.blocks:
instructions.extend(self.flatten(subblock, variables, labels))
instructions.append(Instruction(Opcode.TERMINATE, [], None, None))
del block.instructions
del block.variables
del block.labels
return instructions
def connect_instruction_pointers(self, instructions: List[Instruction]) -> None:
i1, i2 = itertools.tee(instructions)
next(i2, None)
for i, nexti in itertools.zip_longest(i1, i2):
i.next = nexti
if i.opcode in CONDITIONAL_OPCODES:
i.condnext = self.labels[i.args[0]]
def run(self) -> None:
last_timer = time.time()
self.pc = self.program[0] # first instruction of the main program
steps = 1
try:
while True:
now = time.time()
# if now - last_timer >= 1/60:
# last_timer = now
# # start running the timer interrupt program instead
# self.previous_pc = self.pc
# self.program = self.timer_program
# self.stack = self.timer_stack
# self.pc = 0
# while True:
# self.dispatch(self.program[self.pc])
# return True
# self.pc = self.previous_pc
# self.program = self.mainprogram
# self.stack = self.mainstack
print("Step", steps)
self.debug_stack()
next_pc = getattr(self, "opcode_" + self.pc.opcode.name)(self.pc)
if next_pc:
self.pc = self.pc.next
steps += 1
except Exception as x:
print("EXECUTION ERROR")
self.debug_stack(5)
raise
def debug_stack(self, size: int=5) -> None:
stack = self.stack.debug_peek(size)
if len(stack) > 0:
print(" stack (top {:d}):".format(size))
for i, value in enumerate(reversed(stack), start=1):
print(" {:d}. {:s} {!r}".format(i, type(value).__name__, value))
else:
print(" stack is empty.")
if self.pc is not None:
print(" instruction:", self.pc)
def _encodestr(self, string: str, alt: bool=False) -> bytearray:
return bytearray(string, self.str_alt_encoding if alt else self.str_encoding)
def _decodestr(self, bb: bytearray, alt: bool=False) -> str:
return str(bb, self.str_alt_encoding if alt else self.str_encoding)
def opcode_NOP(self, instruction: Instruction) -> bool:
# do nothing
return True
def opcode_TERMINATE(self, instruction: Instruction) -> bool:
# immediately terminate the VM
raise ExecutionError("virtual machine terminated")
def opcode_PUSH(self, instruction: Instruction) -> bool:
# push a value onto the stack
value = self.variables[instruction.args[0]].value
self.stack.push(value)
return True
def opcode_PUSH2(self, instruction: Instruction) -> bool:
# push two values onto the stack
value1 = self.variables[instruction.args[0]].value
value2 = self.variables[instruction.args[1]].value
self.stack.push2(value1, value2)
return True
def opcode_PUSH3(self, instruction: Instruction) -> bool:
# push three values onto the stack
value1 = self.variables[instruction.args[0]].value
value2 = self.variables[instruction.args[1]].value
value3 = self.variables[instruction.args[2]].value
self.stack.push3(value1, value2, value3)
return True
def opcode_POP(self, instruction: Instruction) -> bool:
# pop value from stack and store it in a variable
value = self.stack.pop()
variable = self.variables[instruction.args[0]]
assert not variable.const
variable.value = value
return True
def opcode_POP2(self, instruction: Instruction) -> bool:
# pop two values from tack and store it in two variables
value1, value2 = self.stack.pop2()
variable = self.variables[instruction.args[0]]
assert not variable.const
variable.value = value1
variable = self.variables[instruction.args[1]]
assert not variable.const
variable.value = value2
return True
def opcode_POP3(self, instruction: Instruction) -> bool:
# pop three values from tack and store it in two variables
value1, value2, value3 = self.stack.pop3()
variable = self.variables[instruction.args[0]]
assert not variable.const
variable.value = value1
variable = self.variables[instruction.args[1]]
assert not variable.const
variable.value = value2
variable = self.variables[instruction.args[2]]
assert not variable.const
variable.value = value3
return True
def opcode_ADD(self, instruction: Instruction) -> bool:
# add top to second value on stack and replace them with the result
v1, v2 = self.stack.pop2()
self.stack.push(v2 + v1) # type: ignore
return True
def opcode_SUB(self, instruction: Instruction) -> bool:
# subtract top from second value on stack and replace them with the result
v1, v2 = self.stack.pop2()
self.stack.push(v2 - v1) # type: ignore
return True
def opcode_MUL(self, instruction: Instruction) -> bool:
# multiply top with second value on stack and replace them with the result
v1, v2 = self.stack.pop2()
self.stack.push(v2 * v1) # type: ignore
return True
def opcode_DIV(self, instruction: Instruction) -> bool:
# divide second value by top value on stack and replace them with the result
v1, v2 = self.stack.pop2()
self.stack.push(v2 / v1) # type: ignore
return True
def opcode_AND(self, instruction: Instruction) -> bool:
# second value LOGICAL_AND top value on stack and replace them with the result
v1, v2 = self.stack.pop2()
self.stack.push(v2 and v1)
return True
def opcode_OR(self, instruction: Instruction) -> bool:
# second value LOGICAL_OR top value on stack and replace them with the result
v1, v2 = self.stack.pop2()
self.stack.push(v2 or v1)
return True
def opcode_XOR(self, instruction: Instruction) -> bool:
# second value LOGICAL_XOR top value on stack and replace them with the result
v1, v2 = self.stack.pop2()
i1 = 1 if v1 else 0
i2 = 1 if v2 else 0
self.stack.push(bool(i1 ^ i2))
return True
def opcode_NOT(self, instruction: Instruction) -> bool:
# replace top value on stack with its LOGICAL_NOT
self.stack.push(not self.stack.pop())
return True
def opcode_CMP_EQ(self, instruction: Instruction) -> bool:
# replace second and top value on stack with their == comparison
v1, v2 = self.stack.pop2()
self.stack.push(v2 == v1)
return True
def opcode_CMP_LT(self, instruction: Instruction) -> bool:
# replace second and top value on stack with their < comparison
v1, v2 = self.stack.pop2()
self.stack.push(v2 < v1) # type: ignore
return True
def opcode_CMP_GT(self, instruction: Instruction) -> bool:
# replace second and top value on stack with their > comparison
v1, v2 = self.stack.pop2()
self.stack.push(v2 > v1) # type: ignore
return True
def opcode_CMP_LTE(self, instruction: Instruction) -> bool:
# replace second and top value on stack with their <= comparison
v1, v2 = self.stack.pop2()
self.stack.push(v2 <= v1) # type: ignore
return True
def opcode_CMP_GTE(self, instruction: Instruction) -> bool:
# replace second and top value on stack with their >= comparison
v1, v2 = self.stack.pop2()
self.stack.push(v2 >= v1) # type: ignore
return True
def opcode_RETURN(self, instruction: Instruction) -> bool:
# returns from the current function call
# any return values have already been pushed on the stack
raise NotImplementedError("return")
def opcode_JUMP(self, instruction: Instruction) -> bool:
# jumps unconditionally by resetting the PC to the given instruction index value
return True
def opcode_JUMP_IF_TRUE(self, instruction: Instruction) -> bool:
# pops stack and jumps if that value is true, by resetting the PC to the given instruction index value
result = self.stack.pop()
if result:
self.pc = self.pc.condnext
return False
return True
def opcode_JUMP_IF_FALSE(self, instruction: Instruction) -> bool:
# pops stack and jumps if that value is false, by resetting the PC to the given instruction index value
result = self.stack.pop()
if result:
return True
self.pc = self.pc.condnext
return False
def opcode_SYSCALL(self, instruction: Instruction) -> bool:
call = getattr(self, "syscall_" + instruction.args[0], None)
if call:
return call()
else:
raise RuntimeError("no syscall method for " + instruction.args[0])
def syscall_printstr(self) -> bool:
value = self.stack.pop()
if isinstance(value, bytearray):
print(self._decodestr(value), end="")
return True
else:
raise TypeError("printstr expects bytearray value", value)
def syscall_decimalstr_signed(self) -> bool:
value = self.stack.pop()
if type(value) is int:
self.stack.push(self._encodestr(str(value)))
return True
else:
raise TypeError("decimalstr expects int value", value)
def syscall_hexstr_signed(self) -> bool:
value = self.stack.pop()
if type(value) is int:
if value >= 0: # type: ignore
strvalue = "${:x}".format(value)
else:
strvalue = "-${:x}".format(-value) # type: ignore
self.stack.push(self._encodestr(strvalue))
return True
else:
raise TypeError("hexstr expects int value", value)