Create a generic interpreter API

This commit is contained in:
Reid 'arrdem' McKenzie 2022-08-12 23:46:34 -06:00
parent 9feae8df4c
commit ce23c6b43b
2 changed files with 120 additions and 65 deletions

View file

@ -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(";<main>;;", 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(";<main>;;", 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

View file

@ -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)