From 13273ce6ccb7917a8efff9b9361866a3baadb0c9 Mon Sep 17 00:00:00 2001 From: Irmen de Jong Date: Tue, 27 Feb 2018 22:16:45 +0100 Subject: [PATCH] timer 'irq' in vm --- testvm.txt | 63 +++++++---------- tinyvm/core.py | 60 ++++++++-------- tinyvm/main.py | 8 ++- tinyvm/parse.py | 7 +- tinyvm/vm.py | 179 +++++++++++++++++++++++++++++++----------------- 5 files changed, 178 insertions(+), 139 deletions(-) diff --git a/testvm.txt b/testvm.txt index 12489db45..e07359c4e 100644 --- a/testvm.txt +++ b/testvm.txt @@ -1,50 +1,35 @@ ; source code for a tinyvm program %block b1 %vardefs -var byte vb 1 -var word vw 2233 -var sbyte vbs -2 -var sword vws -3344 -var float vf 1.234 -var array_byte ab 3 1 -var array_sbyte asb 3 -2 -var array_word aw 3 4455 -var array_sword asw 3 -5566 -var matrix_byte mb 2 3 1 -var matrix_sbyte msb 2 3 -2 -var array_byte ab2 5 1 -var array_sbyte asb2 5 -2 -var array_word aw2 5 6677 -var array_sword asw2 5 -8899 -var matrix_byte mb2 4 5 1 -var matrix_sbyte msb2 4 5 -2 +var byte teller 1 +const byte one 1 +const byte thousand 1000 +var array_byte newlinestr 1 10 %end_vardefs %instructions - push vw - pop vb - terminate + push teller + syscall decimalstr_signed + syscall printstr + push newlinestr + syscall printstr +back: + push teller + push one + add + pop teller + push teller + syscall decimalstr_signed + syscall printstr + push newlinestr + syscall printstr + push teller + push thousand + cmp_lt + jump_if_true back + return %end_instructions %subblocks - -%block b2 -%vardefs -%end_vardefs -%end_block ; b2 - %end_subblocks %end_block ;b1 - - - -%block b3 -%vardefs -%end_vardefs -%instructions - nop - nop -l2: - return -%end_instructions -%end_block ; b3 diff --git a/tinyvm/core.py b/tinyvm/core.py index 2d216752e..3ae61d7d6 100644 --- a/tinyvm/core.py +++ b/tinyvm/core.py @@ -3,36 +3,34 @@ from typing import Any, List, Dict, Optional class Opcode(enum.IntEnum): - NOP = 0 - TERMINATE = 1 + TERMINATE = 0 + NOP = 1 PUSH = 10 PUSH2 = 11 PUSH3 = 12 - POP = 20 - POP2 = 21 - POP3 = 22 - ADD = 100 - SUB = 101 - MUL = 102 - DIV = 103 - AND = 200 - OR = 201 - XOR = 202 - NOT = 203 - CMP_EQ = 300 - CMP_LT = 301 - CMP_GT = 302 - CMP_LTE = 303 - CMP_GTE = 304 - TEST = 305 - RETURN = 500 - JUMP = 501 - JUMP_IF_TRUE = 502 - JUMP_IF_FALSE = 503 - SYSCALL = 504 - - -CONDITIONAL_OPCODES = {Opcode.JUMP_IF_FALSE, Opcode.JUMP_IF_TRUE} + POP = 13 + POP2 = 14 + POP3 = 15 + ADD = 50 + SUB = 51 + MUL = 52 + DIV = 53 + AND = 70 + OR = 71 + XOR = 72 + NOT = 73 + TEST = 100 + CMP_EQ = 101 + CMP_LT = 102 + CMP_GT = 103 + CMP_LTE = 104 + CMP_GTE = 105 + CALL = 200 + RETURN = 201 + JUMP = 202 + JUMP_IF_TRUE = 203 + JUMP_IF_FALSE = 204 + SYSCALL = 205 class DataType(enum.IntEnum): @@ -63,13 +61,13 @@ class Variable: class Instruction: - __slots__ = ["opcode", "args", "next", "condnext"] + __slots__ = ["opcode", "args", "next", "alt_next"] - def __init__(self, opcode: Opcode, args: List[Any], nxt: Optional['Instruction'], condnxt: Optional['Instruction']) -> None: + def __init__(self, opcode: Opcode, args: List[Any], nxt: Optional['Instruction'], alt_next: Optional['Instruction']) -> None: self.opcode = opcode self.args = args - self.next = nxt # regular next statement, None=end - self.condnext = condnxt # alternate next statement (for condition nodes) + self.next = nxt # regular next statement, None=end + self.alt_next = alt_next # alternate next statement (for condition nodes, and return instruction for call nodes) def __str__(self) -> str: return "".format(self.opcode.name, self.args) diff --git a/tinyvm/main.py b/tinyvm/main.py index 9a6604e7b..681cacb54 100644 --- a/tinyvm/main.py +++ b/tinyvm/main.py @@ -1,6 +1,6 @@ import sys from .parse import Parser -from .core import Program +from .core import Program, Opcode, Block, Instruction from .vm import VM @@ -8,8 +8,10 @@ source = open(sys.argv[1]).read() parser = Parser(source) program = parser.parse() -timerprogram = Program([]) +timerprogram = Program([Block("timer", None, [], [ + Instruction(Opcode.RETURN, [], None, None) +], {}, [])]) # zero page and hardware stack of a 6502 cpu are off limits for now VM.readonly_mem_ranges = [(0x00, 0xff), (0x100, 0x1ff), (0xa000, 0xbfff), (0xe000, 0xffff)] -vm = VM(program, timerprogram) +vm = VM(program) vm.run() diff --git a/tinyvm/parse.py b/tinyvm/parse.py index 5713ba1e3..9940c8e73 100644 --- a/tinyvm/parse.py +++ b/tinyvm/parse.py @@ -125,10 +125,13 @@ class Parser: label = line[:-1].rstrip() self.lineno += 1 line = self.source[self.lineno] - labels[label] = parse_instruction(line) + next_instruction = parse_instruction(line) + labels[label] = next_instruction + instructions.append(next_instruction) + self.lineno += 1 else: instructions.append(parse_instruction(line)) - self.lineno += 1 + self.lineno += 1 self.skip_empty() assert self.source[self.lineno].startswith("%end_instructions") self.lineno += 1 diff --git a/tinyvm/vm.py b/tinyvm/vm.py index bae083c79..28066a632 100644 --- a/tinyvm/vm.py +++ b/tinyvm/vm.py @@ -51,17 +51,19 @@ # call function (arguments are on stack) # enter / exit (function call frame) # -# TIMER INTERRUPT: triggered every 1/60th of a second. +# TIMER INTERRUPT: triggered around each 1/30th of a second. # executes on a DIFFERENT stack and with a different PROGRAM LIST, # but with access to ALL THE SAME DYNAMIC VARIABLES. +# This suspends the main program until the timer program RETURNs! # import time import itertools import collections import array +import threading import pprint -from .core import Instruction, Variable, Block, Program, Opcode, CONDITIONAL_OPCODES +from .core import Instruction, Variable, Block, Program, Opcode from typing import Dict, List, Tuple, Union from il65.emit import mflpt5_to_float, to_mflpt5 @@ -131,7 +133,18 @@ class Memory: self.mem[index: index+5] = to_mflpt5(value) -StackValueType = Union[bool, int, float, bytearray, array.array] +class CallFrameMarker: + pass + + +class ReturnInstruction: + __slots__ = ["instruction"] + + def __init__(self, instruction: Instruction) -> None: + self.instruction = instruction + + +StackValueType = Union[bool, int, float, bytearray, array.array, CallFrameMarker, ReturnInstruction] class Stack: @@ -188,8 +201,8 @@ class Stack: self.stack[-2] = x def _typecheck(self, value: StackValueType): - if type(value) not in (bool, int, float, bytearray, array.array): - raise TypeError("stack can only contain bool, int, float, (byte)array") + if type(value) not in (bool, int, float, bytearray, array.array, CallFrameMarker, ReturnInstruction): + raise TypeError("invalid item type pushed") # noinspection PyPep8Naming,PyUnusedLocal,PyMethodMayBeStatic @@ -197,8 +210,11 @@ class VM: str_encoding = "iso-8859-15" str_alt_encoding = "iso-8859-15" readonly_mem_ranges = [] # type: List[Tuple[int, int]] + timer_irq_resolution = 1/30 + timer_irq_interlock = threading.Lock() + timer_irq_event = threading.Event() - def __init__(self, program: Program, timerprogram: Program) -> None: + def __init__(self, program: Program, timerprogram: Program=Program([])) -> None: opcode_names = [oc.name for oc in Opcode] for ocname in opcode_names: if not hasattr(self, "opcode_" + ocname): @@ -212,33 +228,39 @@ class VM: self.memory.mark_readonly(start, end) self.main_stack = Stack() self.timer_stack = Stack() - (self.main_program, self.timer_program), self.variables, self.labels = self.flatten_programs(program, timerprogram) + self.main_program, self.timer_program, self.variables, self.labels = self.flatten_programs(program, timerprogram) + print("MAIN PROGRAM"); pprint.pprint([str(i) for i in self.main_program]) 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 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" 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" + if i.opcode not in (Opcode.TERMINATE, Opcode.RETURN)), "timer: all instrs next must be set" + assert all(i.alt_next for i in self.main_program + if i.opcode in (Opcode.CALL, Opcode.JUMP_IF_FALSE, Opcode.JUMP_IF_TRUE)), "main: alt_nexts must be set" + assert all(i.alt_next for i in self.timer_program + if i.opcode in (Opcode.CALL, Opcode.JUMP_IF_FALSE, Opcode.JUMP_IF_TRUE)), "timer: alt_nexts must be set" + threading.Thread(target=self.timer_irq, name="timer_irq", daemon=True).start() print("[TinyVM starting up.]") - 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_programs(self, main: Program, timer: Program) \ + -> Tuple[List[Instruction], List[Instruction], Dict[str, Variable], Dict[str, Instruction]]: + variables = {} # type: Dict[str, Variable] + labels = {} # type: Dict[str, Instruction] + instructions_main = [] # type: List[Instruction] + instructions_timer = [] # type: List[Instruction] + for block in main.blocks: + flat = self.flatten(block, variables, labels) + instructions_main.extend(flat) + instructions_main.append(Instruction(Opcode.TERMINATE, [], None, None)) + for block in timer.blocks: + flat = self.flatten(block, variables, labels) + instructions_timer.extend(flat) + return instructions_main, instructions_timer, variables, labels def flatten(self, block: Block, variables: Dict[str, Variable], labels: Dict[str, Instruction]) -> List[Instruction]: def block_prefix(b: Block) -> str: @@ -269,7 +291,6 @@ class VM: 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 @@ -279,42 +300,65 @@ class VM: 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]] + if i.opcode in (Opcode.JUMP_IF_TRUE, Opcode.JUMP_IF_FALSE): + i.next = nexti # normal flow target + i.alt_next = self.labels[i.args[0]] # conditional jump target + elif i.opcode == Opcode.JUMP: + i.next = self.labels[i.args[0]] # jump target + elif i.opcode == Opcode.CALL: + i.next = self.labels[i.args[0]] # call target + i.alt_next = nexti # return instruction + else: + i.next = nexti def run(self) -> None: - last_timer = time.time() self.pc = self.program[0] # first instruction of the main program - steps = 1 + self.stack.push(ReturnInstruction(None)) # sentinel + self.stack.push(CallFrameMarker()) # enter the call frame so the timer program can end with a RETURN 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 - next_pc = getattr(self, "opcode_" + self.pc.opcode.name)(self.pc) - if next_pc: - self.pc = self.pc.next - steps += 1 + while self.pc is not None: + with self.timer_irq_interlock: + next_pc = getattr(self, "opcode_" + self.pc.opcode.name)(self.pc) + if next_pc: + self.pc = self.pc.next except TerminateExecution as x: why = str(x) - print("[TinyVM execution halted{:s}]\n".format(": "+why if why else ".")) + print("[TinyVM execution terminated{:s}]\n".format(": "+why if why else ".")) return except Exception as x: print("EXECUTION ERROR") self.debug_stack(5) raise + else: + print("[TinyVM execution ended.]") + + def timer_irq(self) -> None: + resolution = 1/30 + wait_time = resolution + while True: + self.timer_irq_event.wait(wait_time) + self.timer_irq_event.clear() + start = time.perf_counter() + if self.timer_program: + with self.timer_irq_interlock: + previous_pc = self.pc + previous_program = self.program + previous_stack = self.stack + self.stack = self.timer_stack + self.program = self.timer_program + self.pc = self.program[0] + self.stack.push(ReturnInstruction(None)) # sentinel + self.stack.push(CallFrameMarker()) # enter the call frame so the timer program can end with a RETURN + while self.pc is not None: + next_pc = getattr(self, "opcode_" + self.pc.opcode.name)(self.pc) + if next_pc: + self.pc = self.pc.next + self.pc = previous_pc + self.program = previous_program + self.stack = previous_stack + current = time.perf_counter() + duration, previously = current - start, current + wait_time = max(0, resolution - duration) def debug_stack(self, size: int=5) -> None: stack = self.stack.debug_peek(size) @@ -424,15 +468,15 @@ class VM: self.stack.push(not self.stack.pop()) return True + def opcode_TEST(self, instruction: Instruction) -> bool: + self.stack.push(bool(self.stack.pop())) + return True + def opcode_CMP_EQ(self, instruction: Instruction) -> bool: second, first = self.stack.pop2() self.stack.push(first == second) return True - def opcode_TEST(self, instruction: Instruction) -> bool: - self.stack.push(bool(self.stack.pop())) - return True - def opcode_CMP_LT(self, instruction: Instruction) -> bool: second, first = self.stack.pop2() self.stack.push(first < second) # type: ignore @@ -453,29 +497,36 @@ class VM: self.stack.push(first >= second) # 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 + def opcode_CALL(self, instruction: Instruction) -> bool: + self.stack.push(ReturnInstruction(instruction.alt_next)) + self.stack.push(CallFrameMarker()) return True + def opcode_RETURN(self, instruction: Instruction) -> bool: + # unwind the function call frame + item = self.stack.pop() + while not isinstance(item, CallFrameMarker): + item = self.stack.pop() + returninstruction = self.stack.pop() + assert isinstance(returninstruction, ReturnInstruction) + self.pc = returninstruction.instruction + return False + + def opcode_JUMP(self, instruction: Instruction) -> bool: + return True # jump simply points to the next instruction elsewhere + 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 + self.pc = self.pc.alt_next # alternative next instruction 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 + self.pc = self.pc.alt_next # alternative next instruction return False def opcode_SYSCALL(self, instruction: Instruction) -> bool: @@ -498,7 +549,7 @@ class System: def syscall_printstr(self) -> bool: value = self.vm.stack.pop() - if isinstance(value, bytearray): + if isinstance(value, (bytearray, array.array)): print(self._decodestr(value), end="") return True else: