mirror of
https://github.com/irmen/prog8.git
synced 2024-11-26 11:49:22 +00:00
timer 'irq' in vm
This commit is contained in:
parent
e9b5bc8200
commit
13273ce6cc
63
testvm.txt
63
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
|
||||
|
@ -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.alt_next = alt_next # alternate next statement (for condition nodes, and return instruction for call nodes)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "<Instruction {} args: {}>".format(self.opcode.name, self.args)
|
||||
|
@ -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()
|
||||
|
@ -125,7 +125,10 @@ 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
|
||||
|
165
tinyvm/vm.py
165
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]]:
|
||||
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]
|
||||
flat_programs = [] # type: List[List[Instruction]]
|
||||
for program in programs:
|
||||
for block in program.blocks:
|
||||
instructions_main = [] # type: List[Instruction]
|
||||
instructions_timer = [] # type: List[Instruction]
|
||||
for block in main.blocks:
|
||||
flat = self.flatten(block, variables, labels)
|
||||
flat_programs.append(flat)
|
||||
return flat_programs, 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):
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
steps += 1
|
||||
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:
|
||||
|
Loading…
Reference in New Issue
Block a user