From ce23c6b43b976b5a4cb864596e9c24013beba839 Mon Sep 17 00:00:00 2001 From: Reid 'arrdem' McKenzie Date: Fri, 12 Aug 2022 23:46:34 -0600 Subject: [PATCH] Create a generic interpreter API --- .../shoggoth/src/python/ichor/interpreter.py | 182 ++++++++++++------ .../shoggoth/test/python/ichor/fixtures.py | 3 +- 2 files changed, 120 insertions(+), 65 deletions(-) diff --git a/projects/shoggoth/src/python/ichor/interpreter.py b/projects/shoggoth/src/python/ichor/interpreter.py index d0eb2b3..0a64c48 100644 --- a/projects/shoggoth/src/python/ichor/interpreter.py +++ b/projects/shoggoth/src/python/ichor/interpreter.py @@ -56,6 +56,94 @@ class InterpreterRestart(Exception): self.state = state +class BaseInterpreter(object): + """A generic base class providing a common interface for Ichor interpreters/evaluators.""" + + def __init__(self, bootstrap_module: Module): + self.bootstrap = bootstrap_module + + def pre_instr(self, state: InterpreterState, opcode: isa.Opcode) -> InterpreterState: + """Hook. Runs after fetch and before every instruction. + + Expected to return the working interpreter state, but may transform it if needed. + Implementations MUST call the superclass implementation. + + """ + + return state + + def post_instr(self, state: InterpreterState, opcode: isa.Opcode) -> InterpreterState: + """Hook. Runs after instruction handling and before the cycle completes. + + Expected to return the working interpreter state, but may transform it if needed. + Implementations MUST call the superclass implementation. + + """ + + state.clock += 1 + return state + + def handle_fault(self, state, opcode, message, cause=None) -> InterpreterState: + """Handler for interpreter errors. + + Called when the interpreter encounters an illegal or incorrect state. + Not called for unhandled exceptions. + May perform arbitrary effects or interface with the user. + May resume the interpreter by raising an InterpreterRestart, or returning. + Need not call the superclass implementation. + + """ + + raise InterpreterError(state, message, cause) + + def handle_unknown(self, state, opcode) -> InterpreterState: + """Handler for unknown opcodes. + + Called when the interpreter encounters an unsupported operation. + May perform arbitrary effects or interface with the user. + May resume the interpreter by raising an InterpreterRestart, or returning. + Need not call the superclass implementation. + + """ + + raise InterpreterError(state, "Unsupported operation: {opcode}") + + def step(self, state: InterpreterState, opcode: isa.Opcode) -> InterpreterState: + """Execute an instruction, producing the next state of the VM.""" + + raise NotImplementedError() + + def run(self, opcodes, stack=[]): + """Directly interpret some opcodes in the configured environment.""" + + _mod = self.bootstrap.copy() + _main = _mod.define_function(";
;;", opcodes) + _main_fun = _mod.functions[_main] + _main_ip = _mod.labels[_main] + + state = InterpreterState( + _mod, Stackframe(_main_fun, _main_ip, stack) + ) + + while True: + try: + opcode = state.module.codepage[state.stackframe._ip] + self.pre_instr(state, opcode) + state = self.step(state, opcode) + self.post_instr(state, opcode) + + # FIXME: This case analysis isn't super obvious. + except InterpreterReturn as r: + return r.val + + except InterpreterRestart as r: + state = r.state + continue + + except Exception as e: + raise e + + def handledispatch(func): dispatcher = singledispatch(func) def wrapper(self, state, opcode): @@ -67,31 +155,28 @@ def handledispatch(func): return wrapper -class Interpreter(object): - """A shit .interpretere instruction pointer based interpreter.""" - def __init__(self, bootstrap_module: Module): - self.bootstrap = bootstrap_module - - def pre_instr(self, state: InterpreterState, opcode: isa.Opcode) -> InterpreterState: - return state - - def post_instr(self, state: InterpreterState, opcode: isa.Opcode) -> InterpreterState: - return state +class Interpreter(BaseInterpreter): + """A fairly naive concrete interpreter based on singledispatch.""" def step(self, state: InterpreterState, opcode: isa.Opcode) -> InterpreterState: - return self.handle_opcode(state, opcode) + """Execute an instruction, producing the next state of the VM.""" - def handle_unknown(self, state: InterpreterState, opcode: isa.Opcode) -> InterpreterState: - raise InterpreterError(state, "Unsupported operation: {opcode}") + return self._handle_opcode(state, opcode) - def handle_fault(self, state, opcode, message, cause=None) -> InterpreterState: - raise InterpreterError(state, message, cause) + #################################################################################################################### + # BEGIN INSTRUCTION IMPLEMENTATIONS AT LENGTH + #################################################################################################################### @handledispatch - def handle_opcode(self, state: InterpreterState, opcode: isa.Opcode) -> InterpreterState: + def _handle_opcode(self, state: InterpreterState, opcode: isa.Opcode) -> InterpreterState: + """Generic entrypoint. Exists to establish a type signature and provide singledispatch handling.""" return self.handle_unknown(state, opcode) - @handle_opcode.register(isa.IDENTIFIERC) + @_handle_opcode.register(object) + def _handle_unknown(self, state: InterpreterState, opcode: isa.Opcode) -> InterpreterState: + return self.handle_unknown(state, opcode) + + @_handle_opcode.register(isa.IDENTIFIERC) def _handle_identifierc(self, state: InterpreterState, opcode: isa.IDENTIFIERC) -> InterpreterState: name = opcode.val if not (name in state.module.functions @@ -103,7 +188,7 @@ class Interpreter(object): state.stackframe._ip += 1 return state - @handle_opcode.register(isa.TYPEREF) + @_handle_opcode.register(isa.TYPEREF) def _handle_typeref(self, state, opcode) -> InterpreterState: id = state.stackframe.pop() if not isinstance(id, Identifier): @@ -116,7 +201,7 @@ class Interpreter(object): state.stackframe._ip += 1 return state - @handle_opcode.register(isa.ARMREF) + @_handle_opcode.register(isa.ARMREF) def _handle_armref(self, state, opcode) -> InterpreterState: id: Identifier = state.stackframe.pop() if not isinstance(id, Identifier): @@ -134,7 +219,7 @@ class Interpreter(object): state.stackframe._ip += 1 return state - @handle_opcode.register(isa.ARM) + @_handle_opcode.register(isa.ARM) def _handle_arm(self, state: InterpreterState, opcode: isa.ARM) -> InterpreterState: armref: VariantRef = state.stackframe.pop() if not isinstance(armref, VariantRef): @@ -155,7 +240,7 @@ class Interpreter(object): state.stackframe._ip += 1 return state - @handle_opcode.register(isa.ATEST) + @_handle_opcode.register(isa.ATEST) def _handle_atest(self, state: InterpreterState, opcode: isa.ATEST) -> InterpreterState: armref: VariantRef = state.stackframe.pop() if not isinstance(armref, VariantRef): @@ -172,14 +257,14 @@ class Interpreter(object): return state - @handle_opcode.register(isa.GOTO) + @_handle_opcode.register(isa.GOTO) def _handle_goto(self, state, opcode: isa.GOTO) -> InterpreterState: if (opcode.target < 0): return self.handle_fault(state, opcode, "Illegal branch target") state.stackframe.goto(opcode.target) return state - @handle_opcode.register(isa.DUP) + @_handle_opcode.register(isa.DUP) def _handle_dupe(self, state, opcode: isa.DUP) -> InterpreterState: if (opcode.nargs > len(state.stackframe)): return self.handle_fault(state, opcode, "Stack size violation") @@ -188,7 +273,7 @@ class Interpreter(object): state.stackframe._ip += 1 return state - @handle_opcode.register(isa.ROT) + @_handle_opcode.register(isa.ROT) def _handle_rot(self, state, opcode: isa.DUP) -> InterpreterState: if (opcode.nargs > len(state.stackframe)): return self.handle_fault(state, opcode, "Stack size violation") @@ -197,7 +282,7 @@ class Interpreter(object): state.stackframe._ip += 1 return state - @handle_opcode.register(isa.DROP) + @_handle_opcode.register(isa.DROP) def _handle_drop(self, state, opcode: isa.DROP) -> InterpreterState: if (opcode.nargs > len(state.stackframe)): return self.handle_fault(state, opcode, "Stack size violation") @@ -206,7 +291,7 @@ class Interpreter(object): state.stackframe._ip += 1 return state - @handle_opcode.register(isa.SLOT) + @_handle_opcode.register(isa.SLOT) def _handle_slot(self, state, opcode: isa.SLOT) -> InterpreterState: if (opcode.target < 0): return self.handle_fault(state, opcode, "SLOT must have a positive reference") @@ -218,7 +303,7 @@ class Interpreter(object): state.stackframe._ip += 1 return state - @handle_opcode.register(isa.FUNREF) + @_handle_opcode.register(isa.FUNREF) def _handle_funref(self, state, opcode) -> InterpreterState: id = state.stackframe.pop() if not isinstance(id, Identifier): @@ -232,7 +317,7 @@ class Interpreter(object): state.stackframe._ip += 1 return state - @handle_opcode.register(isa.CALLF) + @_handle_opcode.register(isa.CALLF) def _handle_callf(self, state, opcode: isa.CALLF) -> InterpreterState: sig = state.stackframe.pop() if not isinstance(sig, FunctionRef): @@ -253,7 +338,7 @@ class Interpreter(object): return state - @handle_opcode.register(isa.RETURN) + @_handle_opcode.register(isa.RETURN) def _handle_return(self, state, opcode: isa.RETURN) -> InterpreterState: n = 1 # FIXME: clean this up if (n > len(state.stackframe)): @@ -268,7 +353,7 @@ class Interpreter(object): state.stackframe = state.stackframe.ret(n) return state - @handle_opcode.register(isa.CLOSUREF) + @_handle_opcode.register(isa.CLOSUREF) def _handle_closuref(self, state: InterpreterState, opcode: isa.CLOSUREF) -> InterpreterState: n = opcode.nargs @@ -292,7 +377,7 @@ class Interpreter(object): state.stackframe._ip += 1 return state - @handle_opcode.register(isa.CLOSUREC) + @_handle_opcode.register(isa.CLOSUREC) def _handle_closurec(self, state, opcode: isa.CLOSUREC) -> InterpreterState: n = opcode.nargs @@ -316,7 +401,7 @@ class Interpreter(object): state.stackframe._ip += 1 return state - @handle_opcode.register(isa.CALLC) + @_handle_opcode.register(isa.CALLC) def _handle_callc(self, state, opcode: isa.CALLC) -> InterpreterState: n = opcode.nargs c = state.stackframe.pop() @@ -340,37 +425,6 @@ class Interpreter(object): state.stackframe = state.stackframe.call(fun, ip) return state - @handle_opcode.register(isa.BREAK) + @_handle_opcode.register(isa.BREAK) def _handle_break(self, state, _) -> InterpreterState: raise InterpreterReturn(state.stackframe._stack) - - def run(self, opcodes, stack=[]): - """Directly interpret some opcodes in the configured environment.""" - - _mod = self.bootstrap.copy() - _main = _mod.define_function(";
;;", opcodes) - _main_fun = _mod.functions[_main] - _main_ip = _mod.labels[_main] - - state = InterpreterState( - _mod, Stackframe(_main_fun, _main_ip, stack) - ) - - while True: - try: - opcode = state.module.codepage[state.stackframe._ip] - self.pre_instr(state, opcode) - state = self.handle_opcode(state, opcode) - self.post_instr(state, opcode) - state.clock += 1 - - # FIXME: This case analysis isn't super obvious. - except InterpreterReturn as r: - return r.val - - except InterpreterRestart as r: - state = r.state - continue - - except Exception as e: - raise e diff --git a/projects/shoggoth/test/python/ichor/fixtures.py b/projects/shoggoth/test/python/ichor/fixtures.py index 32c449a..08c6a5b 100644 --- a/projects/shoggoth/test/python/ichor/fixtures.py +++ b/projects/shoggoth/test/python/ichor/fixtures.py @@ -5,6 +5,7 @@ from textwrap import indent from ichor import isa from ichor.bootstrap import BOOTSTRAP from ichor.interpreter import ( + BaseInterpreter, Interpreter, InterpreterState, ) @@ -25,5 +26,5 @@ class LoggingInterpreter(Interpreter): @pytest.fixture -def vm(): +def vm() -> BaseInterpreter: return LoggingInterpreter(BOOTSTRAP)