diff --git a/projects/shoggoth/src/python/ichor/bootstrap.py b/projects/shoggoth/src/python/ichor/bootstrap.py index ce64973..d88161e 100644 --- a/projects/shoggoth/src/python/ichor/bootstrap.py +++ b/projects/shoggoth/src/python/ichor/bootstrap.py @@ -39,8 +39,16 @@ OR3 = BOOTSTRAP.define_function( ";or;bool,bool,bool;bool", [ # A B C - Opcode.CALLS(OR2), # A|B C - Opcode.CALLS(OR2), # A|B|C + Opcode.IDENTIFIERC(OR2), + Opcode.FUNREF(), + # FIXME: This could be tightened by using ROT maybe... + Opcode.SLOT(0), + Opcode.SLOT(1), + Opcode.SLOT(3), + Opcode.CALLF(2), # A|B + Opcode.SLOT(2), + Opcode.SLOT(3), + Opcode.CALLF(2), # A|B|C Opcode.RETURN(1), ] ) @@ -62,8 +70,15 @@ AND3 = BOOTSTRAP.define_function( ";and;bool,bool,bool;bool", [ # A B C - Opcode.CALLS(AND2), # A&B C - Opcode.CALLS(AND2), # A&B&C + Opcode.IDENTIFIERC(AND2), + Opcode.FUNREF(), + Opcode.SLOT(0), + Opcode.SLOT(1), + Opcode.SLOT(3), + Opcode.CALLF(2), # A&B C + Opcode.SLOT(2), + Opcode.SLOT(3), + Opcode.CALLF(2), # A&B&C Opcode.RETURN(1), ], ) @@ -71,18 +86,30 @@ AND3 = BOOTSTRAP.define_function( XOR2 = BOOTSTRAP.define_function( ";xor;bool,bool;bool", [ + Opcode.IDENTIFIERC(AND2), + Opcode.FUNREF(), + Opcode.IDENTIFIERC(NOT1), + Opcode.FUNREF(), + + Opcode.SLOT(0), + Opcode.SLOT(1), Opcode.DUP(nargs=2), + # !A && B - Opcode.CALLS(NOT1), - Opcode.CALLS(AND2), - Opcode.IF(target=6), + Opcode.SLOT(3), # not + Opcode.CALLF(1), + Opcode.SLOT(2), # and + Opcode.CALLF(2), + Opcode.IF(target=14), Opcode.TRUE(), Opcode.RETURN(1), # !B && A Opcode.ROT(2), - Opcode.CALLS(NOT1), - Opcode.CALLS(AND2), - Opcode.IF(target=12), + Opcode.SLOT(3), # not + Opcode.CALLF(1), + Opcode.SLOT(2), # and + Opcode.CALLF(2), + Opcode.IF(target=22), Opcode.TRUE(), Opcode.RETURN(1), Opcode.FALSE(), @@ -94,16 +121,27 @@ XOR2 = BOOTSTRAP.define_function( XOR3 = BOOTSTRAP.define_function( ";xor;bool,bool,bool;bool", [ + Opcode.IDENTIFIERC(XOR2), + Opcode.FUNREF(), + Opcode.IDENTIFIERC(OR2), + Opcode.FUNREF(), + + Opcode.SLOT(0), + Opcode.SLOT(1), + Opcode.SLOT(2), # A B C Opcode.ROT(nargs=3), # C A B Opcode.ROT(nargs=3), # B C A Opcode.DUP(nargs=1), # B B C A Opcode.ROT(nargs=4), # A B B C - Opcode.CALLS(XOR2), # A^B B C + Opcode.SLOT(3), + Opcode.CALLF(2), # A^B B C Opcode.ROT(nargs=3), # C A^B B Opcode.ROT(nargs=3), # B C A^B - Opcode.CALLS(XOR2), # B^C A^B - Opcode.CALLS(OR2), # A^B|B^C + Opcode.SLOT(3), + Opcode.CALLF(2), # B^C A^B + Opcode.SLOT(4), + Opcode.CALLF(2), # A^B|B^C Opcode.RETURN(1), ] ) diff --git a/projects/shoggoth/src/python/ichor/impl.py b/projects/shoggoth/src/python/ichor/impl.py index ac57eb9..6dce03b 100644 --- a/projects/shoggoth/src/python/ichor/impl.py +++ b/projects/shoggoth/src/python/ichor/impl.py @@ -11,12 +11,14 @@ context (a virtual machine) which DOES have an easily introspected and serialize import sys +from ichor.typing import Identifier + assert sys.version_info > (3, 10, 0), "`match` support is required" from copy import deepcopy -from .isa import Closure, FunctionRef, Opcode +from .isa import Closure, FunctionRef, Opcode, Identifier def rotate(l): @@ -120,6 +122,13 @@ class Interpreter(object): stack.ip = target continue + case Opcode.GOTO(n): + if (n < 0): + _error("Illegal branch target") + + stack.ip = n + continue + case Opcode.DUP(n): if (n > len(stack)): _error("Stack size violation") @@ -138,45 +147,26 @@ class Interpreter(object): stack.drop(n) - case Opcode.CALLS(dest): - try: - sig = FunctionRef.parse(dest) - except: - _error("Invalid target") - - try: - ip = mod.functions[dest] - except KeyError: - _error("Unknown target") - - stack = stack.call(sig, ip) - continue - - case Opcode.RETURN(n): - if (n > len(stack)): - _error("Stack size violation") - - if stack.depth == 0: - return stack[:n] - - sig = FunctionRef.parse(stack.name) - if (len(sig.ret) != n): - _error("Signature violation") - - stack = stack.ret(n) - continue - - case Opcode.GOTO(n, _): + case Opcode.SLOT(n): if (n < 0): - _error("Illegal branch target") + _error("SLOT must have a positive reference") + if (n > len(stack.stack) - 1): + _error("SLOT reference out of range") + stack.push(stack.stack[len(stack) - n - 1]) - stack.ip = n - continue + case Opcode.IDENTIFIERC(name): + if not (name in mod.functions or name in mod.types): + _error("IDENTIFIERC references unknown entity") - case Opcode.FUNREF(funref): + stack.push(Identifier(name)) + + case Opcode.FUNREF(): + id = stack.pop() + if not isinstance(id, Identifier): + _error("FUNREF consumes an IDENTIFIER") try: # FIXME: Verify this statically - stack.push(FunctionRef.parse(funref)) + stack.push(FunctionRef.parse(id.name)) except: _error("Invalid function ref") @@ -197,6 +187,20 @@ class Interpreter(object): stack = stack.call(sig, ip) continue + case Opcode.RETURN(n): + if (n > len(stack)): + _error("Stack size violation") + + if stack.depth == 0: + return stack[:n] + + sig = FunctionRef.parse(stack.name) + if (len(sig.ret) != n): + _error("Signature violation") + + stack = stack.ret(n) + continue + case Opcode.CLOSUREF(n): sig = stack.pop() if not isinstance(sig, FunctionRef): diff --git a/projects/shoggoth/src/python/ichor/isa.py b/projects/shoggoth/src/python/ichor/isa.py index a980740..3f9430e 100644 --- a/projects/shoggoth/src/python/ichor/isa.py +++ b/projects/shoggoth/src/python/ichor/isa.py @@ -10,6 +10,8 @@ class Opcode: #################################################################################################### # Logic #################################################################################################### + + # FIXME: This should become an instantiation of the BOOL enum class TRUE(t.NamedTuple): """() -> (bool) @@ -17,6 +19,7 @@ class Opcode: """ + # FIXME: This should become an instantiation of the BOOL enum class FALSE(t.NamedTuple): """() -> (bool) @@ -24,6 +27,7 @@ class Opcode: """ + # FIXME: This should become a `VTEST` macro ... or may be replaceable class IF(t.NamedTuple): """(bool) -> () @@ -35,12 +39,23 @@ class Opcode: # not, and, or, xor etc. can all be functions given if. + class GOTO(t.NamedTuple): + """() -> () + + Branch to another point within the same bytecode segment. The target MUST be within the same module range as the + current function. Branching does NOT update the name or module of the current function. + + """ + + target: int + #################################################################################################### # Stack manipulation #################################################################################################### # https://wiki.laptop.org/go/Forth_stack_operators # https://www.forth.com/starting-forth/2-stack-manipulation-operators-arithmetic/ # https://docs.oracle.com/javase/specs/jvms/se18/html/jvms-6.html#jvms-6.5.swap + class DUP(t.NamedTuple): """(A, B, ...) -> (A, B, ...) @@ -68,62 +83,39 @@ class Opcode: nargs: int = 1 - #################################################################################################### - # Functional abstraction - #################################################################################################### - class CALLS(t.NamedTuple): - """(... A) -> (... B) + class SLOT(t.NamedTuple): + """(..., A) -> (A, ..., A) - 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(t.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. - - Executing a `RETURN` pops (restores) the name and module path of the current function back to that of the caller. - - If the call stack is empty, `RETURN` will exit the interpreter. - - """ - - nargs: int - - class GOTO(t.NamedTuple): - """() -> () - - Branch to another point within the same bytecode segment. The target MUST be within the same module range as the - current function. Branching does NOT update the name or module of the current function. + Copy the Nth (counting up from 0 at the bottom of the stack) item to the top of the stack. + Intended to allow users to emulate (immutable) frame local slots for reused values. """ target: int - anywhere: bool = False + + #################################################################################################### + # Functional abstraction + #################################################################################################### + + class IDENTIFIERC(t.NamedTuple): + """() -> (IDENTIFIER) + + An inline constant which produces an identifier to the stack. + + Identifiers name functions, fields and types but are not strings. + They are a VM-internal naming structure with reference to the module. + + """ + + val: str class FUNREF(t.NamedTuple): - """() -> (`FUNREF<... A to ... B>`) + """(IDENTIFIER) -> (`FUNREF<... A to ... B>`) Construct a reference to a static codepoint. """ - funref: str - class CALLF(t.NamedTuple): """(`FUNREF<... A to ... B>`, ... A) -> (... B) @@ -139,6 +131,21 @@ class Opcode: nargs: int = 0 + class RETURN(t.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. + + Executing a `RETURN` pops (restores) the name and module path of the current function back to that of the caller. + + If the call stack is empty, `RETURN` will exit the interpreter. + + """ + + nargs: int + + class CLOSUREF(t.NamedTuple): """(`FUNREF`, A) -> (`CLOSURE<... B to ... C>`) @@ -176,19 +183,7 @@ class Opcode: # Structures #################################################################################################### - # FIXME: This lacks any sort of way to do dynamic field references - - class IDENTIFIERC(t.NamedTuple): - """() -> (CONST) - - An inline constant which produces an identifier to the stack. - - Identifiers name functions, fields and types but are not strings. - They are a VM-internal naming structure with reference to the module. - - """ - - val: str + # FIXME: This lacks any sort of way to do dynamic type/field references class TYPEREF(t.NamedTuple): """(IDENTIFIER) -> (TYPEREF) @@ -316,6 +311,10 @@ class Opcode: #################################################################################################### + class BREAK(t.NamedTuple): + pass + + class Module(t.NamedTuple): opcodes: list = [] functions: dict = {} @@ -339,7 +338,7 @@ class Module(t.NamedTuple): assert start <= d < end return Opcode.IF(d) - case Opcode.GOTO(t, anywhere=False): + case Opcode.GOTO(t): d = t + start assert start <= d < end return Opcode.GOTO(d) diff --git a/projects/shoggoth/src/python/ichor/typing.py b/projects/shoggoth/src/python/ichor/typing.py index fe8ac89..40516fa 100644 --- a/projects/shoggoth/src/python/ichor/typing.py +++ b/projects/shoggoth/src/python/ichor/typing.py @@ -91,3 +91,7 @@ class Struct(t.NamedTuple): name: str type_params: list children: t.Mapping[str, t.Any] + + +class Identifier(t.NamedTuple): + name: str diff --git a/projects/shoggoth/test/python/ichor/test_bootstrap.py b/projects/shoggoth/test/python/ichor/test_bootstrap.py index 9334b39..f6b6321 100644 --- a/projects/shoggoth/test/python/ichor/test_bootstrap.py +++ b/projects/shoggoth/test/python/ichor/test_bootstrap.py @@ -11,7 +11,12 @@ import pytest [[True], [False]], ]) def test_not(vm, stack, ret): - assert vm.run([Opcode.CALLS(NOT1), Opcode.RETURN(1)], stack = stack) == ret + assert vm.run([ + Opcode.IDENTIFIERC(NOT1), + Opcode.FUNREF(), + Opcode.CALLF(1), + Opcode.RETURN(1) + ], stack = stack) == ret @pytest.mark.parametrize("stack,ret", [ @@ -21,7 +26,12 @@ def test_not(vm, stack, ret): [[True, True], [True]], ]) def test_or(vm, stack, ret): - assert vm.run([Opcode.CALLS(OR2), Opcode.RETURN(1)], stack = stack) == ret + assert vm.run([ + Opcode.IDENTIFIERC(OR2), + Opcode.FUNREF(), + Opcode.CALLF(2), + Opcode.RETURN(1) + ], stack = stack) == ret @pytest.mark.parametrize("stack,ret", [ @@ -31,7 +41,12 @@ def test_or(vm, stack, ret): [[True, True], [True]], ]) def test_and(vm, stack, ret): - assert vm.run([Opcode.CALLS(AND2), Opcode.RETURN(1)], stack = stack) == ret + assert vm.run([ + Opcode.IDENTIFIERC(AND2), + Opcode.FUNREF(), + Opcode.CALLF(2), + Opcode.RETURN(1) + ], stack = stack) == ret @pytest.mark.parametrize("stack,ret", [ @@ -41,7 +56,12 @@ def test_and(vm, stack, ret): [[True, True], [False]], ]) def test_xor2(vm, stack, ret): - assert vm.run([Opcode.CALLS(XOR2), Opcode.RETURN(1)], stack = stack) == ret + assert vm.run([ + Opcode.IDENTIFIERC(XOR2), + Opcode.FUNREF(), + Opcode.CALLF(2), + Opcode.RETURN(1) + ], stack = stack) == ret @pytest.mark.parametrize("stack,ret", [ @@ -54,21 +74,36 @@ def test_xor2(vm, stack, ret): [[False, False, True], [True]], ]) def test_xor3(vm, stack, ret): - assert vm.run([Opcode.CALLS(XOR3), Opcode.RETURN(1)], stack = stack) == ret + assert vm.run([ + Opcode.IDENTIFIERC(XOR3), + Opcode.FUNREF(), + Opcode.CALLF(3), + Opcode.RETURN(1) + ], stack = stack) == ret @pytest.mark.parametrize("stack,ret", [ [[], [FunctionRef.parse(NOT1)]] ]) def test_funref(vm, stack, ret): - assert vm.run([Opcode.FUNREF(NOT1), Opcode.RETURN(1)], stack = stack) == ret + assert vm.run([ + Opcode.IDENTIFIERC(NOT1), + Opcode.FUNREF(), + 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(NOT1), Opcode.CALLF(1), Opcode.RETURN(1)], stack = stack) == ret + assert vm.run([ + Opcode.FALSE(), + Opcode.IDENTIFIERC(NOT1), + Opcode.FUNREF(), + Opcode.CALLF(1), + Opcode.RETURN(1) + ], stack = stack) == ret @pytest.mark.parametrize("stack,ret", [ @@ -79,7 +114,8 @@ def test_callf(vm, stack, ret): ]) def test_callc(vm, stack, ret): assert vm.run([ - Opcode.FUNREF(XOR2), + Opcode.IDENTIFIERC(XOR2), + Opcode.FUNREF(), Opcode.CLOSUREF(1), Opcode.CALLC(1), Opcode.RETURN(1), @@ -97,7 +133,8 @@ def test_callc(vm, stack, ret): ]) def test_closurec(vm, stack, ret): assert vm.run([ - Opcode.FUNREF(XOR3), + Opcode.IDENTIFIERC(XOR3), + Opcode.FUNREF(), Opcode.CLOSUREF(1), Opcode.CLOSUREC(1), Opcode.CALLC(1),