diff --git a/projects/shogoth/BUILD b/projects/shogoth/BUILD
index 5159119..7f56c63 100644
--- a/projects/shogoth/BUILD
+++ b/projects/shogoth/BUILD
@@ -1,35 +1,7 @@
-py_library(
-    name = "lib",
-    srcs = glob(["src/python/**/*"]),
-    imports = [
-        "src/python"
-    ],
-    deps = [
+py_project(
+    name = "shogoth",
+    main = "src/python/shogoth/repl/__main__.py",
+    lib_deps = [
         py_requirement("lark"),
     ],
 )
-
-zapp_binary(
-    name = "shogothd",
-    main = "src/python/shogoth/server/__main__.py",
-    deps = [
-        ":lib",
-    ],
-)
-
-zapp_binary(
-    name = "shogoth",
-    main = "src/python/shogoth/client/__main__.py",
-    deps = [
-        ":lib",
-    ],
-)
-
-zapp_binary(
-    name = "repl",
-    main = "src/python/shogoth/repl/__main__.py",
-    shebang = "/usr/bin/env python3.10",
-    deps = [
-        ":lib",
-    ]
-)
diff --git a/projects/shogoth/src/python/shogoth/vm/NOTES.md b/projects/shogoth/src/python/shogoth/vm/NOTES.md
new file mode 100644
index 0000000..bf97a1e
--- /dev/null
+++ b/projects/shogoth/src/python/shogoth/vm/NOTES.md
@@ -0,0 +1,11 @@
+# VM notes
+
+## Papers
+
+10.1145/3486606.3488073
+10.1145/3486606.3486783
+10.1145/3427765.3432355
+10.1145/3281287.3281295
+10.1145/2542142.2542144
+10.1145/1941054.1941059
+10.1145/1941054.1941058
diff --git a/projects/shogoth/src/python/shogoth/vm/__init__.py b/projects/shogoth/src/python/shogoth/vm/__init__.py
new file mode 100644
index 0000000..b69cc11
--- /dev/null
+++ b/projects/shogoth/src/python/shogoth/vm/__init__.py
@@ -0,0 +1,3 @@
+#!/usr/bin/env python3
+
+from .impl import Interpreter, Opcode, BOOTSTRAP, AND, OR, NOT, XOR
diff --git a/projects/shogoth/src/python/shogoth/vm/impl.py b/projects/shogoth/src/python/shogoth/vm/impl.py
new file mode 100644
index 0000000..b5a786e
--- /dev/null
+++ b/projects/shogoth/src/python/shogoth/vm/impl.py
@@ -0,0 +1,325 @@
+#!/usr/bin/env python3.10
+
+"""The Shogoth VM implementation.
+
+The whole point of shogoth is that program executions are checkpointable and restartable. This requires that rather than
+using a traditional recursive interpreter which is difficult to snapshot, interpretation in shogoth occur within a
+context (a virtual machine) which DOES have an easily introspected and serialized representation.
+
+## The Shogoth VM Architecture
+
+
+- NOT    [bool] -> [bool]
+- IF     [then: addr, else: addr, cond: bool] -> []
+- CALL   [procedure, n, ...] -> [...]
+- RETURN [n, ...]
+
+"""
+
+from random import Random
+from typing import NamedTuple
+
+class Module(NamedTuple):
+    opcodes: list = []
+    functions: dict = {}
+    types: dict = {}
+    constants: dict = {}
+
+    rand: Random = Random()
+
+    def copy(self):
+        return Module(
+            self.opcodes.copy(),
+            self.functions.copy(),
+            self.types.copy(),
+            self.constants.copy(),
+        )
+
+    @staticmethod
+    def translate(offset: int, i: "Opcode"):
+        match i:
+            case Opcode.IF(t):
+                return Opcode.IF(t + offset)
+            case Opcode.GOTO(t, anywhere=False):
+                return Opcode.GOTO(t + offset)
+            case _:
+                return i
+
+    def define_function(self, name, opcodes):
+        start = len(self.opcodes)
+        self.functions[name] = start
+        for op in opcodes:
+            self.opcodes.append(self.translate(start, op))
+        return name
+
+
+class Opcode:
+    class TRUE(NamedTuple):
+        """() -> (bool)
+        Push the constant TRUE onto the stack.
+        """
+
+    class FALSE(NamedTuple):
+        """() -> (bool)
+        Push the constant FALSE onto the stack.
+        """
+
+    class IF(NamedTuple):
+        """(bool) -> ()
+        Branch to another point if the top item of the stack is TRUE.
+        Otherwise fall through.
+        """
+
+        target: int
+
+    # not, and, or, xor etc. can all be functions given if.
+
+    class DUP(NamedTuple):
+        """(A, B, ...) -> (A, B, ...)
+        Duplicate the top N items of the stack.
+        """
+
+        nargs: int = 1
+
+    class ROT(NamedTuple):
+        """(A, B, ... Z) -> (Z, A, B, ...)
+        Rotate the top N elements of the stack.
+        """
+
+        nargs: int = 2
+
+    class DROP(NamedTuple):
+        """(*) -> ()
+        Drop the top N items of the stack.
+        """
+
+        nargs: int = 1
+
+    class CALL(NamedTuple):
+        """(*) -> ()
+        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.
+        """
+
+        funref: str
+
+    class RETURN(NamedTuple):
+        """(*) -> ()
+        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.
+        If the call stack is empty, `RETURN` will exit the interpreter.
+        """
+
+        nargs: int
+
+    class GOTO(NamedTuple):
+        """() -> ()
+        Branch to another point within the same bytecode segment.
+        """
+
+        target: int
+        anywhere: bool = False
+
+    class STRUCT(NamedTuple):
+        """(*) -> (T)
+        Consume the top N items of the stack, producing a struct.
+        """
+
+        nargs: int
+        structref: str
+
+    class FIELD(NamedTuple):
+        """(A) -> (B)
+        Consume the struct reference at the top of the stack, producing the value of the referenced field.
+        """
+
+        fieldref: str
+
+
+def rotate(l):
+    return [l[-1]] + l[:-1]
+
+
+class FunctionSignature(NamedTuple):
+    type_params: list
+    name: str
+    args: list
+    sig: list
+
+    @staticmethod
+    def parse_list(l):
+        return [e for e in l.split(",") if e]
+
+    @classmethod
+    def parse(cls, name: str):
+        vars, name, args, sig = name.split(";")
+        return cls(
+            cls.parse_list(vars),
+            name,
+            cls.parse_list(args),
+            cls.parse_list(sig)
+        )
+
+
+class Stackframe(object):
+    def __init__(self, stack=None, name=None, ip=None, parent=None):
+        self.stack = stack or []
+        self.name = name or ";unknown;;"
+        self.ip = ip or 0
+        self.parent = parent
+
+    def push(self, obj):
+        self.stack.insert(0, obj)
+
+    def pop(self):
+        return self.stack.pop(0)
+
+    def call(self, signature, ip):
+        nargs = len(signature.args)
+        args, self.stack = self.stack[:nargs], self.stack[nargs:]
+        return Stackframe(
+                stack=args,
+                name=signature.name,
+                ip=ip,
+                parent=self
+            )
+
+    def ret(self, nargs):
+        self.parent.stack = self.stack[:nargs] + self.parent.stack
+        return self.parent
+
+    def dup(self, nargs):
+        self.stack = self.stack[:nargs] + self.stack
+
+    def drop(self, nargs):
+        self.stack = self.stack[nargs:]
+
+    def rot(self, nargs):
+        self.stack = rotate(self.stack[:nargs]) + self.stack[nargs:]
+
+
+class Interpreter(object):
+    def __init__(self, bootstrap_module):
+        self.bootstrap = bootstrap_module
+
+    def run(self, opcodes):
+        """Directly interpret some opcodes in the configured environment."""
+
+        stack = Stackframe()
+        mod = self.bootstrap.copy()
+        mod.define_function(";<entry>;;", opcodes)
+        stack.ip = mod.functions[";<entry>;;"]
+
+        while True:
+            op = mod.opcodes[stack.ip]
+            print(stack.ip, op, stack.stack)
+            match op:
+                case Opcode.TRUE():
+                    stack.push(True)
+
+                case Opcode.FALSE():
+                    stack.push(False)
+
+                case Opcode.IF(target):
+                    if not stack.pop():
+                        stack.ip = target
+                        continue
+
+                case Opcode.DUP(n):
+                    stack.dup(n)
+
+                case Opcode.ROT(n):
+                    stack.rot(n)
+
+                case Opcode.DROP(n):
+                    stack.drop(n)
+
+                case Opcode.CALL(dest):
+                    sig = FunctionSignature.parse(dest)
+                    ip = mod.functions[dest]
+                    stack = stack.call(sig, ip)
+                    continue
+
+                case Opcode.RETURN(n):
+                    if stack.parent:
+                        stack = stack.ret(n)
+                    else:
+                        return stack.stack[:n]
+
+                case Opcode.GOTO(n, _):
+                    stack.ip = n
+                    continue
+
+                case _:
+                    raise Exception(f"Unhandled interpreter state {op}")
+
+            stack.ip += 1
+
+
+BOOTSTRAP = Module()
+
+NOT = ";/lang/shogoth/v0/bootstrap/not;bool;bool"
+BOOTSTRAP.define_function(
+    NOT,
+    [
+        Opcode.IF(target=3),
+        Opcode.FALSE(),
+        Opcode.RETURN(1),
+        Opcode.TRUE(),
+        Opcode.RETURN(1),
+    ],
+)
+
+OR = ";/lang/shogoth/v0/bootstrap/or;bool,bool;bool"
+BOOTSTRAP.define_function(
+    OR,
+    [
+        Opcode.IF(target=3),
+        Opcode.TRUE(),
+        Opcode.RETURN(1),
+        Opcode.IF(target=6),
+        Opcode.TRUE(),
+        Opcode.RETURN(1),
+        Opcode.FALSE(),
+        Opcode.RETURN(1)
+    ],
+)
+
+AND = ";/lang/shogoth/v0/bootstrap/and;bool,bool;bool"
+BOOTSTRAP.define_function(
+    AND,
+    [
+        Opcode.IF(target=3),
+        Opcode.IF(target=3),
+        Opcode.GOTO(target=5),
+        Opcode.FALSE(),
+        Opcode.RETURN(1),
+        Opcode.TRUE(),
+        Opcode.RETURN(1),
+    ],
+)
+
+XOR = ";/lang/shogoth/v0/bootstrap/xor;bool,bool;bool"
+BOOTSTRAP.define_function(
+    XOR,
+    [
+        Opcode.DUP(nargs=2),
+        # !A && B
+        Opcode.CALL(";/lang/shogoth/v0/bootstrap/not;bool;bool"),
+        Opcode.CALL(";/lang/shogoth/v0/bootstrap/and;bool,bool;bool"),
+        Opcode.IF(target=6),
+        Opcode.TRUE(),
+        Opcode.RETURN(1),
+        # !B && A
+        Opcode.ROT(2),
+        Opcode.CALL(";/lang/shogoth/v0/bootstrap/not;bool;bool"),
+        Opcode.CALL(";/lang/shogoth/v0/bootstrap/and;bool,bool;bool"),
+        Opcode.IF(target=12),
+        Opcode.TRUE(),
+        Opcode.RETURN(1),
+        Opcode.FALSE(),
+        Opcode.RETURN(1),
+    ],
+)
diff --git a/projects/shogoth/test/python/shogoth/vm/test_interpreter.py b/projects/shogoth/test/python/shogoth/vm/test_interpreter.py
new file mode 100644
index 0000000..c1aff81
--- /dev/null
+++ b/projects/shogoth/test/python/shogoth/vm/test_interpreter.py
@@ -0,0 +1,167 @@
+"""
+Tests coverign the VM interpreter
+"""
+
+from shogoth.vm import *
+
+vm = Interpreter(BOOTSTRAP)
+
+
+def test_true():
+    assert vm.run([Opcode.TRUE(), Opcode.RETURN(1)]) == [True]
+
+
+def test_false():
+    assert vm.run([Opcode.FALSE(), Opcode.RETURN(1)]) == [False]
+
+
+def test_return():
+    assert vm.run([Opcode.FALSE(), Opcode.RETURN(0)]) == []
+    assert vm.run([Opcode.TRUE(), Opcode.FALSE(), Opcode.RETURN(1)]) == [False]
+    assert vm.run([Opcode.TRUE(), Opcode.FALSE(), Opcode.RETURN(2)]) == [False, True]
+
+
+def test_dup():
+    assert vm.run([
+        Opcode.TRUE(),
+        Opcode.FALSE(),
+        Opcode.DUP(1),
+        Opcode.RETURN(3)
+    ]) == [False, False, True]
+
+    assert vm.run([
+        Opcode.TRUE(),
+        Opcode.FALSE(),
+        Opcode.DUP(2),
+        Opcode.RETURN(4)
+    ]) == [False, True, False, True]
+
+
+def test_rot():
+    assert vm.run([
+        Opcode.TRUE(),
+        Opcode.FALSE(),
+        Opcode.ROT(2),
+        Opcode.RETURN(2)
+    ]) == [True, False]
+
+    assert vm.run([
+        Opcode.FALSE(),
+        Opcode.TRUE(),
+        Opcode.FALSE(),
+        Opcode.ROT(3),
+        Opcode.RETURN(3)
+    ]) == [False, False, True]
+
+
+def test_drop():
+    assert vm.run([
+        Opcode.TRUE(),
+        Opcode.FALSE(),
+        Opcode.DROP(1),
+        Opcode.RETURN(1)
+    ]) == [True]
+
+
+def test_not():
+    assert vm.run([
+        Opcode.TRUE(),
+        Opcode.CALL(NOT),
+        Opcode.RETURN(1)
+    ]) == [False]
+
+    assert vm.run([
+        Opcode.FALSE(),
+        Opcode.CALL(NOT),
+        Opcode.RETURN(1)
+    ]) == [True]
+
+
+def test_or():
+    assert vm.run([
+        Opcode.FALSE(),
+        Opcode.FALSE(),
+        Opcode.CALL(OR),
+        Opcode.RETURN(1)
+    ]) == [False]
+
+    assert vm.run([
+        Opcode.TRUE(),
+        Opcode.FALSE(),
+        Opcode.CALL(OR),
+        Opcode.RETURN(1)
+    ]) == [True]
+
+    assert vm.run([
+        Opcode.FALSE(),
+        Opcode.TRUE(),
+        Opcode.CALL(OR),
+        Opcode.RETURN(1)
+    ]) == [True]
+
+    assert vm.run([
+        Opcode.TRUE(),
+        Opcode.TRUE(),
+        Opcode.CALL(OR),
+        Opcode.RETURN(1)
+    ]) == [True]
+
+
+def test_and():
+    assert vm.run([
+        Opcode.FALSE(),
+        Opcode.FALSE(),
+        Opcode.CALL(AND),
+        Opcode.RETURN(1)
+    ]) == [False]
+
+    assert vm.run([
+        Opcode.TRUE(),
+        Opcode.FALSE(),
+        Opcode.CALL(AND),
+        Opcode.RETURN(1)
+    ]) == [False]
+
+    assert vm.run([
+        Opcode.FALSE(),
+        Opcode.TRUE(),
+        Opcode.CALL(AND),
+        Opcode.RETURN(1)
+    ]) == [False]
+
+    assert vm.run([
+        Opcode.TRUE(),
+        Opcode.TRUE(),
+        Opcode.CALL(AND),
+        Opcode.RETURN(1)
+    ]) == [True]
+
+
+def test_xor():
+    assert vm.run([
+        Opcode.FALSE(),
+        Opcode.FALSE(),
+        Opcode.CALL(XOR),
+        Opcode.RETURN(1)
+    ]) == [False]
+
+    assert vm.run([
+        Opcode.TRUE(),
+        Opcode.FALSE(),
+        Opcode.CALL(XOR),
+        Opcode.RETURN(1)
+    ]) == [True]
+
+    assert vm.run([
+        Opcode.FALSE(),
+        Opcode.TRUE(),
+        Opcode.CALL(XOR),
+        Opcode.RETURN(1)
+    ]) == [True]
+
+    assert vm.run([
+        Opcode.TRUE(),
+        Opcode.TRUE(),
+        Opcode.CALL(XOR),
+        Opcode.RETURN(1)
+    ]) == [False]