Create a generic interpreter API

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

View file

@ -56,6 +56,94 @@ class InterpreterRestart(Exception):
self.state = state 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): def handledispatch(func):
dispatcher = singledispatch(func) dispatcher = singledispatch(func)
def wrapper(self, state, opcode): def wrapper(self, state, opcode):
@ -67,31 +155,28 @@ def handledispatch(func):
return wrapper return wrapper
class Interpreter(object): class Interpreter(BaseInterpreter):
"""A shit .interpretere instruction pointer based interpreter.""" """A fairly naive concrete interpreter based on singledispatch."""
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
def step(self, state: InterpreterState, opcode: isa.Opcode) -> InterpreterState: 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: return self._handle_opcode(state, opcode)
raise InterpreterError(state, "Unsupported operation: {opcode}")
def handle_fault(self, state, opcode, message, cause=None) -> InterpreterState: ####################################################################################################################
raise InterpreterError(state, message, cause) # BEGIN INSTRUCTION IMPLEMENTATIONS AT LENGTH
####################################################################################################################
@handledispatch @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) 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: def _handle_identifierc(self, state: InterpreterState, opcode: isa.IDENTIFIERC) -> InterpreterState:
name = opcode.val name = opcode.val
if not (name in state.module.functions if not (name in state.module.functions
@ -103,7 +188,7 @@ class Interpreter(object):
state.stackframe._ip += 1 state.stackframe._ip += 1
return state return state
@handle_opcode.register(isa.TYPEREF) @_handle_opcode.register(isa.TYPEREF)
def _handle_typeref(self, state, opcode) -> InterpreterState: def _handle_typeref(self, state, opcode) -> InterpreterState:
id = state.stackframe.pop() id = state.stackframe.pop()
if not isinstance(id, Identifier): if not isinstance(id, Identifier):
@ -116,7 +201,7 @@ class Interpreter(object):
state.stackframe._ip += 1 state.stackframe._ip += 1
return state return state
@handle_opcode.register(isa.ARMREF) @_handle_opcode.register(isa.ARMREF)
def _handle_armref(self, state, opcode) -> InterpreterState: def _handle_armref(self, state, opcode) -> InterpreterState:
id: Identifier = state.stackframe.pop() id: Identifier = state.stackframe.pop()
if not isinstance(id, Identifier): if not isinstance(id, Identifier):
@ -134,7 +219,7 @@ class Interpreter(object):
state.stackframe._ip += 1 state.stackframe._ip += 1
return state return state
@handle_opcode.register(isa.ARM) @_handle_opcode.register(isa.ARM)
def _handle_arm(self, state: InterpreterState, opcode: isa.ARM) -> InterpreterState: def _handle_arm(self, state: InterpreterState, opcode: isa.ARM) -> InterpreterState:
armref: VariantRef = state.stackframe.pop() armref: VariantRef = state.stackframe.pop()
if not isinstance(armref, VariantRef): if not isinstance(armref, VariantRef):
@ -155,7 +240,7 @@ class Interpreter(object):
state.stackframe._ip += 1 state.stackframe._ip += 1
return state return state
@handle_opcode.register(isa.ATEST) @_handle_opcode.register(isa.ATEST)
def _handle_atest(self, state: InterpreterState, opcode: isa.ATEST) -> InterpreterState: def _handle_atest(self, state: InterpreterState, opcode: isa.ATEST) -> InterpreterState:
armref: VariantRef = state.stackframe.pop() armref: VariantRef = state.stackframe.pop()
if not isinstance(armref, VariantRef): if not isinstance(armref, VariantRef):
@ -172,14 +257,14 @@ class Interpreter(object):
return state return state
@handle_opcode.register(isa.GOTO) @_handle_opcode.register(isa.GOTO)
def _handle_goto(self, state, opcode: isa.GOTO) -> InterpreterState: def _handle_goto(self, state, opcode: isa.GOTO) -> InterpreterState:
if (opcode.target < 0): if (opcode.target < 0):
return self.handle_fault(state, opcode, "Illegal branch target") return self.handle_fault(state, opcode, "Illegal branch target")
state.stackframe.goto(opcode.target) state.stackframe.goto(opcode.target)
return state return state
@handle_opcode.register(isa.DUP) @_handle_opcode.register(isa.DUP)
def _handle_dupe(self, state, opcode: isa.DUP) -> InterpreterState: def _handle_dupe(self, state, opcode: isa.DUP) -> InterpreterState:
if (opcode.nargs > len(state.stackframe)): if (opcode.nargs > len(state.stackframe)):
return self.handle_fault(state, opcode, "Stack size violation") return self.handle_fault(state, opcode, "Stack size violation")
@ -188,7 +273,7 @@ class Interpreter(object):
state.stackframe._ip += 1 state.stackframe._ip += 1
return state return state
@handle_opcode.register(isa.ROT) @_handle_opcode.register(isa.ROT)
def _handle_rot(self, state, opcode: isa.DUP) -> InterpreterState: def _handle_rot(self, state, opcode: isa.DUP) -> InterpreterState:
if (opcode.nargs > len(state.stackframe)): if (opcode.nargs > len(state.stackframe)):
return self.handle_fault(state, opcode, "Stack size violation") return self.handle_fault(state, opcode, "Stack size violation")
@ -197,7 +282,7 @@ class Interpreter(object):
state.stackframe._ip += 1 state.stackframe._ip += 1
return state return state
@handle_opcode.register(isa.DROP) @_handle_opcode.register(isa.DROP)
def _handle_drop(self, state, opcode: isa.DROP) -> InterpreterState: def _handle_drop(self, state, opcode: isa.DROP) -> InterpreterState:
if (opcode.nargs > len(state.stackframe)): if (opcode.nargs > len(state.stackframe)):
return self.handle_fault(state, opcode, "Stack size violation") return self.handle_fault(state, opcode, "Stack size violation")
@ -206,7 +291,7 @@ class Interpreter(object):
state.stackframe._ip += 1 state.stackframe._ip += 1
return state return state
@handle_opcode.register(isa.SLOT) @_handle_opcode.register(isa.SLOT)
def _handle_slot(self, state, opcode: isa.SLOT) -> InterpreterState: def _handle_slot(self, state, opcode: isa.SLOT) -> InterpreterState:
if (opcode.target < 0): if (opcode.target < 0):
return self.handle_fault(state, opcode, "SLOT must have a positive reference") return self.handle_fault(state, opcode, "SLOT must have a positive reference")
@ -218,7 +303,7 @@ class Interpreter(object):
state.stackframe._ip += 1 state.stackframe._ip += 1
return state return state
@handle_opcode.register(isa.FUNREF) @_handle_opcode.register(isa.FUNREF)
def _handle_funref(self, state, opcode) -> InterpreterState: def _handle_funref(self, state, opcode) -> InterpreterState:
id = state.stackframe.pop() id = state.stackframe.pop()
if not isinstance(id, Identifier): if not isinstance(id, Identifier):
@ -232,7 +317,7 @@ class Interpreter(object):
state.stackframe._ip += 1 state.stackframe._ip += 1
return state return state
@handle_opcode.register(isa.CALLF) @_handle_opcode.register(isa.CALLF)
def _handle_callf(self, state, opcode: isa.CALLF) -> InterpreterState: def _handle_callf(self, state, opcode: isa.CALLF) -> InterpreterState:
sig = state.stackframe.pop() sig = state.stackframe.pop()
if not isinstance(sig, FunctionRef): if not isinstance(sig, FunctionRef):
@ -253,7 +338,7 @@ class Interpreter(object):
return state return state
@handle_opcode.register(isa.RETURN) @_handle_opcode.register(isa.RETURN)
def _handle_return(self, state, opcode: isa.RETURN) -> InterpreterState: def _handle_return(self, state, opcode: isa.RETURN) -> InterpreterState:
n = 1 # FIXME: clean this up n = 1 # FIXME: clean this up
if (n > len(state.stackframe)): if (n > len(state.stackframe)):
@ -268,7 +353,7 @@ class Interpreter(object):
state.stackframe = state.stackframe.ret(n) state.stackframe = state.stackframe.ret(n)
return state return state
@handle_opcode.register(isa.CLOSUREF) @_handle_opcode.register(isa.CLOSUREF)
def _handle_closuref(self, state: InterpreterState, opcode: isa.CLOSUREF) -> InterpreterState: def _handle_closuref(self, state: InterpreterState, opcode: isa.CLOSUREF) -> InterpreterState:
n = opcode.nargs n = opcode.nargs
@ -292,7 +377,7 @@ class Interpreter(object):
state.stackframe._ip += 1 state.stackframe._ip += 1
return state return state
@handle_opcode.register(isa.CLOSUREC) @_handle_opcode.register(isa.CLOSUREC)
def _handle_closurec(self, state, opcode: isa.CLOSUREC) -> InterpreterState: def _handle_closurec(self, state, opcode: isa.CLOSUREC) -> InterpreterState:
n = opcode.nargs n = opcode.nargs
@ -316,7 +401,7 @@ class Interpreter(object):
state.stackframe._ip += 1 state.stackframe._ip += 1
return state return state
@handle_opcode.register(isa.CALLC) @_handle_opcode.register(isa.CALLC)
def _handle_callc(self, state, opcode: isa.CALLC) -> InterpreterState: def _handle_callc(self, state, opcode: isa.CALLC) -> InterpreterState:
n = opcode.nargs n = opcode.nargs
c = state.stackframe.pop() c = state.stackframe.pop()
@ -340,37 +425,6 @@ class Interpreter(object):
state.stackframe = state.stackframe.call(fun, ip) state.stackframe = state.stackframe.call(fun, ip)
return state return state
@handle_opcode.register(isa.BREAK) @_handle_opcode.register(isa.BREAK)
def _handle_break(self, state, _) -> InterpreterState: def _handle_break(self, state, _) -> InterpreterState:
raise InterpreterReturn(state.stackframe._stack) 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 import isa
from ichor.bootstrap import BOOTSTRAP from ichor.bootstrap import BOOTSTRAP
from ichor.interpreter import ( from ichor.interpreter import (
BaseInterpreter,
Interpreter, Interpreter,
InterpreterState, InterpreterState,
) )
@ -25,5 +26,5 @@ class LoggingInterpreter(Interpreter):
@pytest.fixture @pytest.fixture
def vm(): def vm() -> BaseInterpreter:
return LoggingInterpreter(BOOTSTRAP) return LoggingInterpreter(BOOTSTRAP)