diff --git a/tests/test_vmcore.py b/tests/test_vmcore.py new file mode 100644 index 000000000..a6047565f --- /dev/null +++ b/tests/test_vmcore.py @@ -0,0 +1,60 @@ +import pytest +from tinyvm.core import Memory + + +def test_memory_unsigned(): + m = Memory() + m.set_byte(1000, 1) + m.set_byte(1001, 2) + m.set_byte(1002, 3) + m.set_byte(1003, 4) + m.set_byte(2000, 252) + m.set_byte(2001, 253) + m.set_byte(2002, 254) + m.set_byte(2003, 255) + assert 1 == m.get_byte(1000) + assert 2 == m.get_byte(1001) + assert 3 == m.get_byte(1002) + assert 4 == m.get_byte(1003) + assert 252 == m.get_byte(2000) + assert 253 == m.get_byte(2001) + assert 254 == m.get_byte(2002) + assert 255 == m.get_byte(2003) + assert b"\x01\x02\x03\x04" == m.get_bytes(1000, 4) + assert 0x0201 == m.get_word(1000) + assert 0xfffe == m.get_word(2002) + m.set_word(2002, 40000) + assert 40000 == m.get_word(2002) + assert 0x40 == m.get_byte(2002) + assert 0x9c == m.get_byte(2003) + + +def test_memory_signed(): + m = Memory() + m.set_byte(1000, 1) + m.set_byte(1001, 2) + m.set_byte(1002, 3) + m.set_byte(1003, 4) + m.set_byte(2000, 252) + m.set_byte(2001, 253) + m.set_byte(2002, 254) + m.set_byte(2003, 255) + assert 1 == m.get_sbyte(1000) + assert 2 == m.get_sbyte(1001) + assert 3 == m.get_sbyte(1002) + assert 4 == m.get_sbyte(1003) + assert -4 == m.get_sbyte(2000) + assert -3 == m.get_sbyte(2001) + assert -2 == m.get_sbyte(2002) + assert -1 == m.get_sbyte(2003) + assert 0x0201 == m.get_sword(1000) + assert -2 == m.get_sword(2002) + m.set_sword(2002, 30000) + assert 30000 == m.get_sword(2002) + assert 0x30 == m.get_sbyte(2002) + assert 0x75 == m.get_sbyte(2003) + m.set_sword(2002, -30000) + assert -30000 == m.get_sword(2002) + assert 0x8ad0 == m.get_word(2002) + assert 0xd0 == m.get_byte(2002) + assert 0x8a == m.get_byte(2003) diff --git a/tinyvm/core.py b/tinyvm/core.py new file mode 100644 index 000000000..9b6824b08 --- /dev/null +++ b/tinyvm/core.py @@ -0,0 +1,110 @@ +""" +Simplistic 8/16 bit Virtual Machine to execute a stack based instruction language. +Core data structures and definitions. + +Written by Irmen de Jong (irmen@razorvine.net) - license: GNU GPL 3.0 +""" + +import enum +import struct +from typing import Callable +from il65.emit import mflpt5_to_float, to_mflpt5 + + +class DataType(enum.IntEnum): + BOOL = 1 + BYTE = 2 + SBYTE = 3 + WORD = 4 + SWORD = 5 + FLOAT = 6 + ARRAY_BYTE = 7 + ARRAY_SBYTE = 8 + ARRAY_WORD = 9 + ARRAY_SWORD = 10 + MATRIX_BYTE = 11 + MATRIX_SBYTE = 12 + + +class ExecutionError(Exception): + pass + + +class TerminateExecution(SystemExit): + pass + + +class MemoryAccessError(Exception): + pass + + +class Memory: + def __init__(self): + self.mem = bytearray(65536) + self.readonly = bytearray(65536) + self.mmap_io_charout_addr = -1 + self.mmap_io_charout_callback = None + self.mmap_io_charin_addr = -1 + self.mmap_io_charin_callback = None + + def mark_readonly(self, start: int, end: int) -> None: + self.readonly[start:end+1] = [1] * (end-start+1) + + def memmapped_io_charout(self, address: int, callback: Callable) -> None: + self.mmap_io_charout_addr = address + self.mmap_io_charout_callback = callback + + def memmapped_io_charin(self, address: int, callback: Callable) -> None: + self.mmap_io_charin_addr = address + self.mmap_io_charin_callback = callback + + def get_byte(self, index: int) -> int: + if self.mmap_io_charin_addr == index: + self.mem[index] = self.mmap_io_charin_callback() + return self.mem[index] + + def get_bytes(self, startindex: int, amount: int) -> bytearray: + return self.mem[startindex: startindex+amount] + + def get_sbyte(self, index: int) -> int: + if self.mmap_io_charin_addr == index: + self.mem[index] = self.mmap_io_charin_callback() + return struct.unpack("b", self.mem[index:index+1])[0] + + def get_word(self, index: int) -> int: + return self.mem[index] + 256 * self.mem[index+1] + + def get_sword(self, index: int) -> int: + return struct.unpack(" float: + return mflpt5_to_float(self.mem[index: index+5]) + + def set_byte(self, index: int, value: int) -> None: + if self.readonly[index]: + raise MemoryAccessError("read-only", index) + self.mem[index] = value + if self.mmap_io_charout_addr == index: + self.mmap_io_charout_callback(value) + + def set_sbyte(self, index: int, value: int) -> None: + if self.readonly[index]: + raise MemoryAccessError("read-only", index) + self.mem[index] = struct.pack("b", bytes([value]))[0] + if self.mmap_io_charout_addr == index: + self.mmap_io_charout_callback(self.mem[index]) + + def set_word(self, index: int, value: int) -> None: + if self.readonly[index] or self.readonly[index+1]: + raise MemoryAccessError("read-only", index) + self.mem[index], self.mem[index + 1] = struct.pack(" None: + if self.readonly[index] or self.readonly[index+1]: + raise MemoryAccessError("read-only", index) + self.mem[index], self.mem[index + 1] = struct.pack(" None: + if any(self.readonly[index:index+5]): + raise MemoryAccessError("read-only", index) + self.mem[index: index+5] = to_mflpt5(value) diff --git a/tinyvm/examples/printiovm.txt b/tinyvm/examples/printiovm.txt new file mode 100644 index 000000000..86b589460 --- /dev/null +++ b/tinyvm/examples/printiovm.txt @@ -0,0 +1,26 @@ +; source code for a tinyvm program; memory mapped I/O +%block b1 +%vardefs +const byte chr_i 105 +const byte chr_r 114 +const byte chr_m 109 +const byte chr_e 101 +const byte chr_n 110 +const byte chr_EOL 10 +const word chrout 53248 +const word chrin 53249 +%end_vardefs +%instructions +loop: + push chrin + syscall memread_byte + push chrout + swap + syscall memwrite_byte + push chrout + push chr_EOL + syscall memwrite_byte + syscall delay + jump loop +%end_instructions +%end_block ;b1 diff --git a/testvm-timer.txt b/tinyvm/examples/testvm-timer.txt similarity index 100% rename from testvm-timer.txt rename to tinyvm/examples/testvm-timer.txt diff --git a/testvm.txt b/tinyvm/examples/testvm.txt similarity index 100% rename from testvm.txt rename to tinyvm/examples/testvm.txt diff --git a/tinyvm/parse.py b/tinyvm/parse.py index 179a07907..31bf5f94c 100644 --- a/tinyvm/parse.py +++ b/tinyvm/parse.py @@ -7,8 +7,9 @@ Written by Irmen de Jong (irmen@razorvine.net) - license: GNU GPL 3.0 import array from typing import Optional, List, Tuple, Dict, Any -from .program import DataType, Opcode, Program, Block, Variable, Instruction, Value +from .program import Opcode, Program, Block, Variable, Instruction, Value from .vm import StackValueType +from .core import DataType class ParseError(Exception): diff --git a/tinyvm/program.py b/tinyvm/program.py index 2b6090111..2b161169d 100644 --- a/tinyvm/program.py +++ b/tinyvm/program.py @@ -9,6 +9,7 @@ import enum import array import operator from typing import List, Dict, Optional, Union, Callable, Any +from .core import DataType class Opcode(enum.IntEnum): @@ -45,21 +46,6 @@ class Opcode(enum.IntEnum): SYSCALL = 205 -class DataType(enum.IntEnum): - BOOL = 1 - BYTE = 2 - SBYTE = 3 - WORD = 4 - SWORD = 5 - FLOAT = 6 - ARRAY_BYTE = 7 - ARRAY_SBYTE = 8 - ARRAY_WORD = 9 - ARRAY_SWORD = 10 - MATRIX_BYTE = 11 - MATRIX_SBYTE = 12 - - class Value: __slots__ = ["dtype", "value", "length", "height"] diff --git a/tinyvm/vm.py b/tinyvm/vm.py index ee887dbfb..c5eeb72fc 100644 --- a/tinyvm/vm.py +++ b/tinyvm/vm.py @@ -27,7 +27,7 @@ Written by Irmen de Jong (irmen@razorvine.net) - license: GNU GPL 3.0 # read [byte/bytearray from keyboard], # wait [till any input comes available], @todo # check [if input is available) @todo -# or via memory-mapped I/O (text screen matrix, keyboard scan register @todo) +# 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. @@ -73,76 +73,8 @@ import pprint import tkinter import tkinter.font from typing import Dict, List, Tuple, Union, no_type_check -from il65.emit import mflpt5_to_float, to_mflpt5 -from .program import Instruction, Variable, Block, Program, Opcode, Value, DataType - - -class ExecutionError(Exception): - pass - - -class TerminateExecution(SystemExit): - pass - - -class MemoryAccessError(Exception): - pass - - -class Memory: - def __init__(self): - self.mem = bytearray(65536) - self.readonly = bytearray(65536) - - def mark_readonly(self, start: int, end: int) -> None: - self.readonly[start:end+1] = [1] * (end-start+1) - - def get_byte(self, index: int) -> int: - return self.mem[index] - - def get_bytes(self, startindex: int, amount: int) -> int: - return self.mem[startindex: startindex+amount] - - 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: - if self.readonly[index]: - raise MemoryAccessError("read-only", index) - self.mem[index] = value - - def set_sbyte(self, index: int, value: int) -> None: - if self.readonly[index]: - raise MemoryAccessError("read-only", index) - self.mem[index] = value + 256 - - def set_word(self, index: int, value: int) -> None: - if self.readonly[index] or self.readonly[index+1]: - raise MemoryAccessError("read-only", index) - hi, lo = divmod(value, 256) - self.mem[index] = lo - self.mem[index+1] = hi - - def set_sword(self, index: int, value: int) -> None: - if self.readonly[index] or self.readonly[index+1]: - raise MemoryAccessError("read-only", index) - hi, lo = divmod(value + 65536, 256) - self.mem[index] = lo - self.mem[index+1] = hi - - def set_float(self, index: int, value: float) -> None: - if any(self.readonly[index:index+5]): - raise MemoryAccessError("read-only", index) - self.mem[index: index+5] = to_mflpt5(value) +from .program import Instruction, Variable, Block, Program, Opcode, Value +from .core import Memory, DataType, TerminateExecution class CallFrameMarker: @@ -231,6 +163,8 @@ class VM: str_alt_encoding = "iso-8859-15" readonly_mem_ranges = [] # type: List[Tuple[int, int]] timer_irq_resolution = 1/30 + charout_address = 0xd000 + charin_address = 0xd001 def __init__(self, program: Program, timerprogram: Program=None) -> None: opcode_names = [oc.name for oc in Opcode] @@ -246,6 +180,8 @@ class VM: if oc not in self.dispatch_table: raise NotImplementedError("no dispatch entry in table for " + oc.name) self.memory = Memory() + self.memory.memmapped_io_charout(self.charout_address, self.memmapped_charout) + self.memory.memmapped_io_charin(self.charin_address, self.memmapped_charin) for start, end in self.readonly_mem_ranges: self.memory.mark_readonly(start, end) self.main_stack = Stack() @@ -259,6 +195,7 @@ class VM: self.charscreen_address = 0 self.charscreen_width = 0 self.charscreen_height = 0 + self.keyboard_scancode = 0 self.system = System(self) assert all(i.next for i in self.main_program if i.opcode != Opcode.TERMINATE), "main: all instrs next must be set" @@ -341,7 +278,7 @@ class VM: def run(self) -> None: if self.charscreen_address: threading.Thread(target=ScreenViewer.create, - args=(self.memory, self.system, self.charscreen_address, self.charscreen_width, self.charscreen_height), + args=(self, self.charscreen_address, self.charscreen_width, self.charscreen_height), name="screenviewer", daemon=True).start() time.sleep(0.05) @@ -405,6 +342,13 @@ class VM: if self.pc is not None: print("* instruction:", self.pc) + def memmapped_charout(self, value: int) -> None: + string = self.system.decodestr(bytearray([value])) + print(string, end="") + + def memmapped_charin(self) -> int: + return self.keyboard_scancode + def assign_variable(self, variable: Variable, value: Value) -> None: assert not variable.const, "cannot modify a const" assert isinstance(value, Value) @@ -742,6 +686,13 @@ class System: self.vm.memory.set_byte(address+i, b) # type: ignore return True + def syscall_memread_byte(self) -> bool: + address = self.vm.stack.pop() + assert isinstance(address, Value) + assert address.dtype == DataType.WORD + self.vm.stack.push(Value(DataType.BYTE, self.vm.memory.get_byte(address.value))) # type: ignore + return True + def syscall_smalldelay(self) -> bool: time.sleep(1/100) return True @@ -752,12 +703,11 @@ class System: class ScreenViewer(tkinter.Tk): - def __init__(self, memory: Memory, system: System, screen_addr: int, screen_width: int, screen_height: int) -> None: + def __init__(self, vm: VM, screen_addr: int, screen_width: int, screen_height: int) -> None: super().__init__() self.title("IL65 tinyvm") self.fontsize = 16 - self.memory = memory - self.system = system + self.vm = vm self.address = screen_addr self.width = screen_width self.height = screen_height @@ -765,19 +715,42 @@ class ScreenViewer(tkinter.Tk): cw = self.monospace.measure("x")*self.width+8 self.canvas = tkinter.Canvas(self, width=cw, height=self.fontsize*self.height+8, bg="blue") self.canvas.pack() + self.bind("", self.keypress) + self.bind("", self.keyrelease) self.after(10, self.update_screen) - def update_screen(self): + def keypress(self, e) -> None: + key = e.char or e.keysym + if len(key) == 1: + self.vm.keyboard_scancode = self.vm.system.encodestr(key)[0] + elif len(key) > 1: + code = 0 + if key == "Up": + code = ord("w") + elif key == "Down": + code = ord("s") + elif key == "Left": + code = ord("a") + elif key == "Right": + code = ord("d") + self.vm.keyboard_scancode = code + else: + self.vm.keyboard_scancode = 0 + + def keyrelease(self, e) -> None: + self.vm.keyboard_scancode = 0 + + def update_screen(self) -> None: self.canvas.delete(tkinter.ALL) lines = [] for y in range(self.height): - line = self.system.decodestr(self.memory.get_bytes(self.address+y*self.width, self.width)) + line = self.vm.system.decodestr(self.vm.memory.get_bytes(self.address+y*self.width, self.width)) lines.append("".join(c if c.isprintable() else " " for c in line)) for y, line in enumerate(lines): self.canvas.create_text(4, self.fontsize*y, text=line, fill="white", font=self.monospace, anchor=tkinter.NW) self.after(30, self.update_screen) @classmethod - def create(cls, memory: Memory, system: System, screen_addr: int, screen_width: int, screen_height: int) -> None: - viewer = cls(memory, system, screen_addr, screen_width, screen_height) + def create(cls, vm: VM, screen_addr: int, screen_width: int, screen_height: int) -> None: + viewer = cls(vm, screen_addr, screen_width, screen_height) viewer.mainloop()