diff --git a/runtime/.clang-format b/runtime/.clang-format
new file mode 100644
index 0000000..8929d9b
--- /dev/null
+++ b/runtime/.clang-format
@@ -0,0 +1,6 @@
+Language: Cpp
+BasedOnStyle: WebKit
+IndentWidth: 4
+ColumnLimit: 80
+IndentCaseLabels: true
+
diff --git a/runtime/Makefile b/runtime/Makefile
new file mode 100644
index 0000000..c591d85
--- /dev/null
+++ b/runtime/Makefile
@@ -0,0 +1,30 @@
+
+CXX_FLAGS = \
+	-std=c++20 \
+	-Og \
+	-fsanitize=address,undefined \
+	-pedantic -pedantic-errors \
+	-Wall -Wextra -Wpedantic -Wconversion \
+
+OUT=build/sliger
+
+CXX_HEADERS = $(shell find . -name *.hpp)
+
+CXX_SOURCES = $(shell find . -name *.cpp)
+
+CXX_OBJECTS = $(patsubst %.cpp,build/%.o,$(CXX_SOURCES))
+
+all: build_dir $(OUT)
+
+$(OUT): $(CXX_OBJECTS)
+	g++ -o $@ $(CXX_FLAGS) $^
+
+build_dir:
+	mkdir -p build/
+
+build/%.o: %.cpp $(CXX_HEADERS)
+	g++ -c -o $@ $(CXX_FLAGS) $<
+
+clean:
+	rm -rf build/
+
diff --git a/runtime/arch.hpp b/runtime/arch.hpp
new file mode 100644
index 0000000..411758e
--- /dev/null
+++ b/runtime/arch.hpp
@@ -0,0 +1,37 @@
+#pragma once
+
+#include <cstdint>
+
+namespace sliger {
+
+// NOTICE: keep up to date with src/arch.ts
+
+enum class Op : uint32_t {
+    Nop = 0,
+    PushNull = 1,
+    PushInt = 2,
+    PushString = 3,
+    PushArray = 4,
+    PushStruct = 5,
+    PushPtr = 6,
+    Pop = 7,
+    LoadLocal = 8,
+    StoreLocal = 9,
+    Call = 10,
+    Return = 11,
+    Jump = 12,
+    JumpIfNotZero = 13,
+    Add = 14,
+    Subtract = 15,
+    Multiply = 16,
+    Divide = 17,
+    Remainder = 18,
+    Equal = 19,
+    LessThan = 20,
+    And = 21,
+    Or = 22,
+    Xor = 23,
+    Not = 24,
+};
+
+}
diff --git a/runtime/build/main.o b/runtime/build/main.o
new file mode 100644
index 0000000..7547f02
Binary files /dev/null and b/runtime/build/main.o differ
diff --git a/runtime/build/sliger b/runtime/build/sliger
new file mode 100755
index 0000000..4124329
Binary files /dev/null and b/runtime/build/sliger differ
diff --git a/runtime/compile_flags.txt b/runtime/compile_flags.txt
new file mode 100644
index 0000000..c79d0c3
--- /dev/null
+++ b/runtime/compile_flags.txt
@@ -0,0 +1,9 @@
+-xc++
+-std=c++20
+-pedantic
+-pedantic-errors
+-Wall
+-Wextra
+-Wpedantic
+-Wconversion
+
diff --git a/runtime/main.cpp b/runtime/main.cpp
new file mode 100644
index 0000000..1eec9af
--- /dev/null
+++ b/runtime/main.cpp
@@ -0,0 +1,8 @@
+#include <format>
+#include <iostream>
+
+int main()
+{
+    //
+    std::cout << std::format("hello world\n");
+}
diff --git a/runtime/value.hpp b/runtime/value.hpp
new file mode 100644
index 0000000..1f841f3
--- /dev/null
+++ b/runtime/value.hpp
@@ -0,0 +1,86 @@
+#pragma once
+
+#include <cstdint>
+#include <string>
+#include <variant>
+
+namespace sliger {
+
+enum class ValueType {
+    Null,
+    Int,
+    Bool,
+    String,
+    Ptr,
+};
+
+class Values;
+
+struct Null { };
+struct Int {
+    uint32_t value;
+};
+struct Bool {
+    bool value;
+};
+struct String {
+    std::string value;
+};
+struct Ptr {
+    uint32_t value;
+};
+
+class Value {
+public:
+    Value(Null&& value)
+        : m_type(ValueType::Null)
+        , value(value)
+    {
+    }
+    Value(Int&& value)
+        : m_type(ValueType::Int)
+        , value(value)
+    {
+    }
+    Value(Bool&& value)
+        : m_type(ValueType::Bool)
+        , value(value)
+    {
+    }
+    Value(String&& value)
+        : m_type(ValueType::String)
+        , value(value)
+    {
+    }
+    Value(Ptr&& value)
+        : m_type(ValueType::Ptr)
+        , value(value)
+    {
+    }
+
+    inline auto type() const -> ValueType { return m_type; };
+
+    inline auto as_null() -> Null& { return std::get<Null>(value); }
+    inline auto as_null() const -> const Null& { return std::get<Null>(value); }
+
+    inline auto as_int() -> Int& { return std::get<Int>(value); }
+    inline auto as_int() const -> const Int& { return std::get<Int>(value); }
+
+    inline auto as_bool() -> Bool& { return std::get<Bool>(value); }
+    inline auto as_bool() const -> const Bool& { return std::get<Bool>(value); }
+
+    inline auto as_string() -> String& { return std::get<String>(value); }
+    inline auto as_string() const -> const String&
+    {
+        return std::get<String>(value);
+    }
+
+    inline auto as_ptr() -> Ptr& { return std::get<Ptr>(value); }
+    inline auto as_ptr() const -> const Ptr& { return std::get<Ptr>(value); }
+
+private:
+    ValueType m_type;
+    std::variant<Null, Int, Bool, String, Ptr> value;
+};
+
+}
diff --git a/runtime/vm.cpp b/runtime/vm.cpp
new file mode 100644
index 0000000..3cba947
--- /dev/null
+++ b/runtime/vm.cpp
@@ -0,0 +1,51 @@
+#include "vm.hpp"
+#include "arch.hpp"
+#include <format>
+#include <iostream>
+
+using namespace sliger;
+
+void VM::run()
+{
+    while (!done()) {
+        auto op = eat_as_op();
+        switch (op) {
+            case Op::Nop:
+                // nothing
+                break;
+            case Op::PushNull:
+                this->stack.push_back(Null {});
+                break;
+            case Op::PushInt:
+                if (done()) {
+                    std::cerr
+                        << std::format("program malformed: missing int value");
+                }
+                this->stack.push_back(Null {});
+                break;
+            case Op::PushString:
+            case Op::PushArray:
+            case Op::PushStruct:
+            case Op::PushPtr:
+            case Op::Pop:
+            case Op::LoadLocal:
+            case Op::StoreLocal:
+            case Op::Call:
+            case Op::Return:
+            case Op::Jump:
+            case Op::JumpIfNotZero:
+            case Op::Add:
+            case Op::Subtract:
+            case Op::Multiply:
+            case Op::Divide:
+            case Op::Remainder:
+            case Op::Equal:
+            case Op::LessThan:
+            case Op::And:
+            case Op::Or:
+            case Op::Xor:
+            case Op::Not:
+                break;
+        }
+    }
+}
diff --git a/runtime/vm.hpp b/runtime/vm.hpp
new file mode 100644
index 0000000..eac7227
--- /dev/null
+++ b/runtime/vm.hpp
@@ -0,0 +1,43 @@
+#pragma once
+
+#include "arch.hpp"
+#include "value.hpp"
+#include <cstddef>
+#include <cstdint>
+#include <vector>
+
+namespace sliger {
+
+class VM {
+public:
+    VM(const std::vector<Op>& program)
+        : program(program.data())
+        , program_size(program.size())
+    {
+    }
+    void run();
+
+    inline void step() { this->pc += 1; }
+
+    inline auto eat_as_op() -> Op
+    {
+        auto value = curr_as_op();
+        step();
+        return value;
+    }
+
+    inline auto curr_as_op() const -> Op
+    {
+        return static_cast<Op>(this->program[this->pc]);
+    }
+
+    inline auto done() const -> bool { return this->pc >= this->program_size; }
+
+private:
+    uint32_t pc = 0;
+    const Op* program;
+    size_t program_size;
+    std::vector<Value> stack;
+    std::vector<Value> pool_heap;
+};
+}
diff --git a/src/arch.ts b/src/arch.ts
index db8287a..eaa838c 100644
--- a/src/arch.ts
+++ b/src/arch.ts
@@ -1,6 +1,8 @@
 export type Ins = Ops | number;
 export type Program = Ins[];
 
+// NOTICE: keep up to date with runtime/arch.hpp
+
 export type Ops = typeof Ops;
 export const Ops = {
     Nop: 0,