From 0d6d3319d8f9ad76cbc916759e880fd36f002521 Mon Sep 17 00:00:00 2001 From: "Reid D. 'arrdem' McKenzie" Date: Thu, 21 Apr 2022 00:17:59 -0600 Subject: [PATCH] Function references; stubs for closures --- .../src/python/shogoth/types/__init__.py | 63 ++------ .../src/python/shogoth/vm/bootstrap.py | 23 ++- .../shogoth/src/python/shogoth/vm/impl.py | 34 ++++- projects/shogoth/src/python/shogoth/vm/isa.py | 137 +++++++++++++++--- .../test/python/shogoth/vm/test_bootstrap.py | 22 ++- 5 files changed, 191 insertions(+), 88 deletions(-) diff --git a/projects/shogoth/src/python/shogoth/types/__init__.py b/projects/shogoth/src/python/shogoth/types/__init__.py index a645e01..91b3160 100644 --- a/projects/shogoth/src/python/shogoth/types/__init__.py +++ b/projects/shogoth/src/python/shogoth/types/__init__.py @@ -4,71 +4,26 @@ from .keyword import Keyword from .symbol import Symbol from abc import ABC -from typing import NamedTuple, List, Mapping +from typing import NamedTuple, List, Mapping, Any from uuid import UUID, uuid4 -class TypeExpr(ABC): - """A type expression""" - - bindings: [] - - -class TypeVariable(TypeExpr): +class TypeVariable(NamedTuple): name: str id: UUID = uuid4() -class PrimitiveExpr(object): - class Nat(TypeExpr): pass - class N8(Nat): pass - class N16(Nat): pass - class N32(Nat): pass - class N64(Nat): pass - class N128(Nat): pass - class N256(Nat): pass - class N512(Nat): pass - - class Unsigned(TypeExpr): pass - class U8(Unsigned): pass - class U16(Unsigned): pass - class U32(Unsigned): pass - class U64(Unsigned): pass - class U128(Unsigned): pass - class U256(Unsigned): pass - class U512(): pass - - class Integer(TypeExpr): pass - class I8(Integer): pass - class I16(Integer): pass - class I32(Integer): pass - class I64(Integer): pass - class I128(Integer): pass - class I256(Integer): pass - class I512(Integer): pass - - class Floating(TypeExpr): pass - class F8(Floating): pass - class F16(Floating): pass - class F32(Floating): pass - class F64(Floating): pass - class F128(Floating): pass - class F256(Floating): pass - class F512(Floating): pass +class ArrayExpr(NamedTuple): + child: Any - -class ArrayExpr(TypeExpr): - child: TypeExpr +class SumExpr(NamedTuple): + children: List[Any] -class SumExpr(TypeExpr): - children: List[TypeExpr] - - -class ProductExpr(TypeExpr): - children: Mapping[str, TypeExpr] +class ProductExpr(NamedTuple): + children: Mapping[str, Any] #################################################################################################### @@ -79,7 +34,7 @@ class Function(NamedTuple): """The type of a function; a subset of its signature.""" -class FunctionSignature(NamedTuple): +class FunctionRef(NamedTuple): raw: str type_params: list name: str diff --git a/projects/shogoth/src/python/shogoth/vm/bootstrap.py b/projects/shogoth/src/python/shogoth/vm/bootstrap.py index 58d9de7..563e3a1 100644 --- a/projects/shogoth/src/python/shogoth/vm/bootstrap.py +++ b/projects/shogoth/src/python/shogoth/vm/bootstrap.py @@ -5,7 +5,7 @@ Hopefully no "real" interpreter ever uses this code, since it's obviously replac """ from .isa import Module, Opcode - +from shogoth.types import * BOOTSTRAP = Module() @@ -52,19 +52,32 @@ XOR = BOOTSTRAP.define_function( [ Opcode.DUP(nargs=2), # !A && B - Opcode.CALL(NOT), - Opcode.CALL(AND), + Opcode.CALLS(NOT), + Opcode.CALLS(AND), Opcode.IF(target=6), Opcode.TRUE(), Opcode.RETURN(1), # !B && A Opcode.ROT(2), - Opcode.CALL(NOT), - Opcode.CALL(AND), + Opcode.CALLS(NOT), + Opcode.CALLS(AND), Opcode.IF(target=12), Opcode.TRUE(), Opcode.RETURN(1), Opcode.FALSE(), + Opcode.RETURN(1), ], ) + +TRUE = BOOTSTRAP.define_type( + "true", ProductExpr([]), +) + +FALSE = BOOTSTRAP.define_type( + "false", ProductExpr([]), +) + +BOOL = BOOTSTRAP.define_type( + "bool", SumExpr([TRUE, FALSE]) +) diff --git a/projects/shogoth/src/python/shogoth/vm/impl.py b/projects/shogoth/src/python/shogoth/vm/impl.py index df63a5c..ceab3ca 100644 --- a/projects/shogoth/src/python/shogoth/vm/impl.py +++ b/projects/shogoth/src/python/shogoth/vm/impl.py @@ -18,7 +18,7 @@ context (a virtual machine) which DOES have an easily introspected and serialize from copy import deepcopy -from .isa import FunctionSignature, Opcode +from .isa import FunctionRef, Opcode def rotate(l): @@ -38,7 +38,7 @@ class Stackframe(object): def pop(self): return self.stack.pop(0) - def call(self, signature: FunctionSignature, ip): + def call(self, signature: FunctionRef, ip): print(signature) nargs = len(signature.args) args, self.stack = self.stack[:nargs], self.stack[nargs:] @@ -135,9 +135,9 @@ class Interpreter(object): stack.drop(n) - case Opcode.CALL(dest): + case Opcode.CALLS(dest): try: - sig = FunctionSignature.parse(dest) + sig = FunctionRef.parse(dest) except: _error("Invalid target") @@ -154,7 +154,7 @@ class Interpreter(object): _error("Stack size violation") if stack.parent: - sig = FunctionSignature.parse(stack.name) + sig = FunctionRef.parse(stack.name) if (len(sig.ret) != n): _error("Signature violation") @@ -169,6 +169,30 @@ class Interpreter(object): stack.ip = n continue + case Opcode.FUNREF(funref): + try: + # FIXME: Verify this statically + stack.push(FunctionRef.parse(funref)) + except: + _error("Invalid function ref") + + case Opcode.CALLF(n): + sig = stack.pop() + if not isinstance(sig, FunctionRef): + _error("CALLF requires a funref at top of stack") + if not n == len(sig.args): + _error("CALLF target violation; not enough arguments provided") + if n > len(stack): + _error("Stack size violation") + + try: + ip = mod.functions[sig.raw] + except KeyError: + _error("Unknown target") + + stack = stack.call(sig, ip) + continue + case _: raise Exception(f"Unhandled interpreter state {op}") diff --git a/projects/shogoth/src/python/shogoth/vm/isa.py b/projects/shogoth/src/python/shogoth/vm/isa.py index 25fe8b9..92d0007 100644 --- a/projects/shogoth/src/python/shogoth/vm/isa.py +++ b/projects/shogoth/src/python/shogoth/vm/isa.py @@ -3,7 +3,7 @@ from typing import NamedTuple -from shogoth.types import Function, FunctionSignature +from shogoth.types import Function, FunctionRef class Opcode: @@ -27,6 +27,9 @@ class Opcode: # not, and, or, xor etc. can all be functions given if. + #################################################################################################### + # Stack manipulation + #################################################################################################### class DUP(NamedTuple): """(A, B, ...) -> (A, B, ...) Duplicate the top N items of the stack. @@ -48,19 +51,28 @@ class Opcode: nargs: int = 1 - class CALL(NamedTuple): - """(*) -> () + #################################################################################################### + # Functional abstraction + #################################################################################################### + class CALLS(NamedTuple): + """(... A) -> (... B) + Call [static] + Branch to `target` pushing the current point onto the call stack. The callee will see a stack containg only the provided `nargs`. A subsequent RETURN will return execution to the next point. Executing a `CALL` pushes the name and module path of the current function. + + .. note:: + + CALLS is equvalent to `FUNREF; CALLF` """ funref: str class RETURN(NamedTuple): - """(*) -> () + """(... A) -> () Return to the source of the last `CALL`. The returnee will see the top `nargs` values of the present stack appended to theirs. All other values on the stack will be discarded. @@ -82,22 +94,100 @@ class Opcode: target: int anywhere: bool = False - class STRUCT(NamedTuple): - """(*) -> (T) - Consume the top N items of the stack, producing a struct of the type `structref`. - - The name and module path of the current function MUST match the name and module path of `structref`. + class FUNREF(NamedTuple): + """() -> (`FUNREF<... A to ... B>`) + Construct a reference to a static codepoint. """ - structref: str + funref: str + + class CALLF(NamedTuple): + """(`FUNREF<... A to ... B>`, ... A) -> (... B) + Call [funref] + + Make a dynamic call to the function reference at the top of stack. + The callee will see a stack containg only the provided `nargs`. + A subsequent RETURN will return execution to the next point. + + Executing a `CALL` pushes the name and module path of the current function. + """ + + nargs: int = 0 + + class CLOSUREF(NamedTuple): + """(`FUNREF`, A) -> (`CLOSURE<... B to ... C>`) + Construct a closure over the function reference at the top of the stack. + This may produce nullary closures. + """ + + class CLOSUREC(NamedTuple): + """(`CLOSURE`, A) -> (`CLOSURE<... B to ... C>`) + Further close over the closure at the top of the stack. + This may produce nullary closures. + """ + + class CALLC(NamedTuple): + """(`CLOSURE<... A, ... B>`, ... A) -> (... B) + Call [closure] + + Make a dynamic call to the closure at the top of stack. + The callee will see a stack containg only the provided `nargs` and closed-overs. + A subsequent RETURN will return execution to the next point. + + Executing a `CALL` pushes the name and module path of the current function. + """ + + nargs: int = 0 + + # #################################################################################################### + # # Structures + # #################################################################################################### + # class STRUCT(NamedTuple): + # """(*) -> (T) + # Consume the top N items of the stack, producing a struct of the type `structref`. + # + # The name and module path of the current function MUST match the name and module path of `structref`. + # """ + # + # structref: str + # nargs: int + + # class FLOAD(NamedTuple): + # """(A) -> (B) + # Consume the struct reference at the top of the stack, producing the value of the referenced field. + # """ + # + # fieldref: str + + # class FSTORE(NamedTuple): + # """(A) -> (B) + # Consume the struct reference at the top of the stack, producing the value of the referenced field. + # """ + # + # fieldref: str + + #################################################################################################### + # Arrays + #################################################################################################### + class ARRAY(NamedTuple): + """(*) -> (ARRAY) + Consume the top N items of the stack, producing an array of the type `typeref`. + """ + + typeref: str nargs: int - class FIELD(NamedTuple): - """(A) -> (B) - Consume the struct reference at the top of the stack, producing the value of the referenced field. + class ALOAD(NamedTuple): + """(NAT, ARRAY) -> (T) + Consume a reference to an array and an index, producing the value at that index. + FIXME: Or a fault/signal. """ - fieldref: str + class ASTORE(NamedTuple): + """(T, NAT, ARRAY) -> (ARRAY) + Consume a value T, storing it at an index in the given array. + Produces the updated array as the top of stack. + """ class Module(NamedTuple): @@ -115,12 +205,19 @@ class Module(NamedTuple): ) @staticmethod - def translate(offset: int, i: "Opcode"): + def translate(start: int, end: int, i: "Opcode"): + # FIXME: Consolidate bounds checks somehow match i: case Opcode.IF(t): - return Opcode.IF(t + offset) + d = t + start + assert start <= d < end + return Opcode.IF(d) + case Opcode.GOTO(t, anywhere=False): - return Opcode.GOTO(t + offset) + d = t + start + assert start <= d < end + return Opcode.GOTO(d) + case _: return i @@ -130,7 +227,7 @@ class Module(NamedTuple): # How to consume capabilities? try: - sig = FunctionSignature.parse(name) + sig = FunctionRef.parse(name) assert sig.name except: raise ValueError("Illegal name provided") @@ -138,9 +235,9 @@ class Module(NamedTuple): start = len(self.opcodes) self.functions[name] = start for op in opcodes: - self.opcodes.append(self.translate(start, op)) + self.opcodes.append(self.translate(start, start + len(opcodes), op)) return name - def define_struct(self, name, signature): + def define_type(self, name, signature): # FIXME: What in TARNATION is this going to do pass diff --git a/projects/shogoth/test/python/shogoth/vm/test_bootstrap.py b/projects/shogoth/test/python/shogoth/vm/test_bootstrap.py index c10c512..262196c 100644 --- a/projects/shogoth/test/python/shogoth/vm/test_bootstrap.py +++ b/projects/shogoth/test/python/shogoth/vm/test_bootstrap.py @@ -11,7 +11,7 @@ from .fixtures import * # noqa [[True], [False]], ]) def test_not(vm, stack, ret): - assert vm.run([Opcode.CALL(NOT)], stack = stack) == ret + assert vm.run([Opcode.CALLS(NOT)], stack = stack) == ret @pytest.mark.parametrize('stack,ret', [ @@ -21,7 +21,7 @@ def test_not(vm, stack, ret): [[True, True], [True]], ]) def test_or(vm, stack, ret): - assert vm.run([Opcode.CALL(OR)], stack = stack) == ret + assert vm.run([Opcode.CALLS(OR)], stack = stack) == ret @pytest.mark.parametrize('stack,ret', [ @@ -31,7 +31,7 @@ def test_or(vm, stack, ret): [[True, True], [True]], ]) def test_and(vm, stack, ret): - assert vm.run([Opcode.CALL(AND)], stack = stack) == ret + assert vm.run([Opcode.CALLS(AND)], stack = stack) == ret @pytest.mark.parametrize('stack,ret', [ @@ -41,4 +41,18 @@ def test_and(vm, stack, ret): [[True, True], [False]], ]) def test_xor(vm, stack, ret): - assert vm.run([Opcode.CALL(XOR)], stack = stack) == ret + assert vm.run([Opcode.CALLS(XOR)], stack = stack) == ret + + +@pytest.mark.parametrize('stack,ret', [ + [[], [FunctionRef.parse(NOT)]] +]) +def test_funref(vm, stack, ret): + assert vm.run([Opcode.FUNREF(NOT), Opcode.RETURN(1)], stack = stack) == ret + + +@pytest.mark.parametrize('stack,ret', [ + [[], [True]] +]) +def test_callf(vm, stack, ret): + assert vm.run([Opcode.FALSE(), Opcode.FUNREF(NOT), Opcode.CALLF(1)], stack = stack) == ret