VM basics (#5)

Co-authored-by: Andre Wagner <WagnerAndre@JohnDeere.com>
Reviewed-on: https://192.168.5.48/andre/tyche/pulls/5
This commit was merged in pull request #5.
This commit is contained in:
2026-04-30 13:34:49 -05:00
parent 71390b0f84
commit b835dbb36e
24 changed files with 366 additions and 47 deletions

View File

@@ -59,15 +59,15 @@ FetchContent_MakeAvailable(googletest)
# #
add_library(lib${PROJECT_NAME} SHARED add_library(lib${PROJECT_NAME} SHARED
src/bytecode/bytearray.hh src/common/overloaded.hh
src/bytecode/bytearray.cc src/common/bytearray.hh
src/common/bytearray.cc
src/bytecode/bytecode.cc src/bytecode/bytecode.cc
src/bytecode/bytecode.hh src/bytecode/bytecode.hh
src/bytecode/bytecodeprototype.hh src/bytecode/bytecodeprototype.hh
src/common/overloaded.hh src/bytecode/constant.hh
src/vm/code.cc src/vm/code.cc
src/vm/code.hh src/vm/code.hh
src/bytecode/constant.hh
src/vm/instruction.hh src/vm/instruction.hh
src/vm/instruction.cc src/vm/instruction.cc
src/vm/value.cc src/vm/value.cc
@@ -75,6 +75,11 @@ add_library(lib${PROJECT_NAME} SHARED
src/vm/stack.cc src/vm/stack.cc
src/vm/stack.hh src/vm/stack.hh
src/vm/vm_exceptions.hh src/vm/vm_exceptions.hh
src/vm/vm.cc
src/vm/vm.hh
src/vm/expr.cc
src/vm/expr.hh
src/vm/location.hh
) )
target_compile_options(lib${PROJECT_NAME} PRIVATE ${warnings}) target_compile_options(lib${PROJECT_NAME} PRIVATE ${warnings})

View File

@@ -25,9 +25,12 @@ After some additional development:
- [x] Output bytecode format - [x] Output bytecode format
- [x] Value object - [x] Value object
- [x] Stack object - [x] Stack object
- [ ] External interface - [x] External interface
- [ ] Code execution (except functions) - [x] Code execution (except functions)
- [ ] Functions - [x] Functions
- [x] Print stack
- [ ] Assembler
After some additional development: After some additional development:
- [ ] Bytecode loader - [ ] Bytecode loader

View File

@@ -1,7 +1,7 @@
#include "bytecode.hh" #include "bytecode.hh"
#include "../common/overloaded.hh" #include "../common/overloaded.hh"
namespace tyche { namespace tyche::bc {
Bytecode::Bytecode(ByteArray ba) Bytecode::Bytecode(ByteArray ba)
: byte_array_(std::move(ba)) : byte_array_(std::move(ba))

View File

@@ -1,10 +1,10 @@
#ifndef TYCHE_BYTECODE_HH #ifndef TYCHE_BYTECODE_HH
#define TYCHE_BYTECODE_HH #define TYCHE_BYTECODE_HH
#include "bytearray.hh" #include "../common/bytearray.hh"
#include "bytecodeprototype.hh" #include "bytecodeprototype.hh"
namespace tyche { namespace tyche::bc {
class Bytecode { class Bytecode {
public: public:

View File

@@ -6,9 +6,9 @@
#include <variant> #include <variant>
#include <vector> #include <vector>
#include "constant.hh" #include "constant.hh"
#include "bytearray.hh" #include "../common/bytearray.hh"
namespace tyche { namespace tyche::bc {
struct BytecodePrototype { struct BytecodePrototype {
struct Function { struct Function {

View File

@@ -4,7 +4,7 @@
#include <string> #include <string>
#include <variant> #include <variant>
namespace tyche { namespace tyche::bc {
using ConstantValue = std::variant<float, std::string>; using ConstantValue = std::variant<float, std::string>;

View File

@@ -3,11 +3,12 @@
#include <cstring> #include <cstring>
#include <functional> #include <functional>
#include "bytearray.hh" #include "../common/bytearray.hh"
#include "bytecodeprototype.hh" #include "bytecodeprototype.hh"
#include "bytecode.hh" #include "bytecode.hh"
using namespace tyche; using namespace tyche;
using namespace tyche::bc;
TEST(ByteArray, ByteArray) TEST(ByteArray, ByteArray)
{ {

View File

@@ -2,14 +2,51 @@
#include "../common/overloaded.hh" #include "../common/overloaded.hh"
#include "instruction.hh" #include "instruction.hh"
namespace tyche { namespace tyche::vm {
void Code::import_bytecode(ByteArray incoming) FunctionId Code::import_bytecode(ByteArray incoming)
{ {
Bytecode bc(std::move(incoming)); bc::Bytecode bc(std::move(incoming));
// TODO - adjust function calls, constants // TODO - adjust function calls, constants
bytecode_ = std::move(bc); bytecode_ = std::move(bc);
return 0; // TODO
}
Operation Code::operation(Location const& location) const
{
Instruction inst = (Instruction) bytecode_.get_code_byte(location.function_id, location.pc);
OperandType opet = instruction_operand_type(inst);
switch (opet) {
case OperandType::NoOperand:
return {
.instruction = inst,
.operator_ = 0,
.next_location = { .function_id = location.function_id, .pc = location.pc + 1 },
};
case OperandType::Int8:
return {
.instruction = inst,
.operator_ = bytecode_.get_code_int8(location.function_id, location.pc + 1),
.next_location = { .function_id = location.function_id, .pc = location.pc + 2 },
};
case OperandType::Int16:
return {
.instruction = inst,
.operator_ = bytecode_.get_code_int16(location.function_id, location.pc + 1),
.next_location = { .function_id = location.function_id, .pc = location.pc + 3 },
};
case OperandType::Int32:
return {
.instruction = inst,
.operator_ = bytecode_.get_code_int32(location.function_id, location.pc + 1),
.next_location = { .function_id = location.function_id, .pc = location.pc + 5 },
};
}
throw std::logic_error("Should not get here");
} }
std::string Code::disassemble() const std::string Code::disassemble() const

View File

@@ -1,18 +1,30 @@
#ifndef TYCHE_CODE_HH #ifndef TYCHE_CODE_HH
#define TYCHE_CODE_HH #define TYCHE_CODE_HH
#include "instruction.hh"
#include "location.hh"
#include "value.hh"
#include "../bytecode/bytecode.hh" #include "../bytecode/bytecode.hh"
namespace tyche { namespace tyche::vm {
struct Operation
{
Instruction instruction;
int32_t operator_;
Location next_location;
};
class Code { class Code {
public: public:
void import_bytecode(ByteArray incoming); FunctionId import_bytecode(ByteArray incoming);
[[nodiscard]] std::string disassemble() const; [[nodiscard]] std::string disassemble() const;
[[nodiscard]] Operation operation(Location const& location) const;
private: private:
Bytecode bytecode_; bc::Bytecode bytecode_;
}; };
} // tyche } // tyche

18
src/vm/expr.cc Normal file
View File

@@ -0,0 +1,18 @@
#include "expr.hh"
#include "vm_exceptions.hh"
namespace tyche::vm {
Value binary_operation(Value const& a, Value const& b, BinaryOperationType op)
{
// TODO - this is temporary code
if (a.type() == Type::Integer && b.type() == Type::Integer && op == BinaryOperationType::Sum) {
return Value::CreateInteger(a.as_integer() + b.as_integer());
} else {
throw VMInvalidOperation(op, a.type(), b.type());
}
}
}

12
src/vm/expr.hh Normal file
View File

@@ -0,0 +1,12 @@
#ifndef TYCHE_EXPR_HH
#define TYCHE_EXPR_HH
#include "value.hh"
namespace tyche::vm {
enum class BinaryOperationType { Sum };
Value binary_operation(Value const& a, Value const& b, BinaryOperationType op);
}
#endif //TYCHE_EXPR_HH

View File

@@ -1,6 +1,6 @@
#include "instruction.hh" #include "instruction.hh"
namespace tyche { namespace tyche::vm {
std::pair<std::string, size_t> debug_instruction(Instruction inst, int oper) std::pair<std::string, size_t> debug_instruction(Instruction inst, int oper)
{ {
@@ -98,30 +98,48 @@ std::pair<std::string, size_t> debug_instruction(Instruction inst, int oper)
out = "???"; out = "???";
} }
if ((uint8_t) inst < 0xa0) OperandType operands = instruction_operand_type(inst);
if (operands == OperandType::NoOperand)
return { out, 1 }; return { out, 1 };
out += " " + std::to_string(oper); out += " " + std::to_string(oper);
if ((uint8_t) inst >= 0xe0) if (operands == OperandType::Int32)
return { out, 5 }; return { out, 5 };
else if ((uint8_t) inst >= 0xc0) if (operands == OperandType::Int16)
return { out, 3 }; return { out, 3 };
else
return { out, 2 }; return { out, 2 };
} }
std::pair<std::string, size_t> debug_instruction(Bytecode const& bt, uint32_t function_id, uint32_t addr) std::pair<std::string, size_t> debug_instruction(bc::Bytecode const& bt, uint32_t function_id, uint32_t addr)
{ {
auto inst = (Instruction) bt.get_code_byte(function_id, addr); auto inst = (Instruction) bt.get_code_byte(function_id, addr);
switch (instruction_operand_type(inst)) {
case OperandType::NoOperand:
return debug_instruction(inst);
case OperandType::Int8:
return debug_instruction(inst, bt.get_code_int8(function_id, addr + 1));
case OperandType::Int16:
return debug_instruction(inst, bt.get_code_int16(function_id, addr + 1));
case OperandType::Int32:
return debug_instruction(inst, bt.get_code_int32(function_id, addr + 1));
default:
}
return { "???", 1 };
}
OperandType instruction_operand_type(Instruction inst)
{
if ((uint8_t) inst >= 0xe0) if ((uint8_t) inst >= 0xe0)
return debug_instruction(inst, bt.get_code_int32(function_id, addr + 1)); return OperandType::Int32;
else if ((uint8_t) inst >= 0xc0) if ((uint8_t) inst >= 0xc0)
return debug_instruction(inst, bt.get_code_int16(function_id, addr + 1)); return OperandType::Int16;
else if ((uint8_t) inst >= 0xa0) if ((uint8_t) inst >= 0xa0)
return debug_instruction(inst, bt.get_code_int8(function_id, addr + 1)); return OperandType::Int8;
return OperandType::NoOperand;
return debug_instruction(inst);
} }
} }

View File

@@ -7,7 +7,7 @@
#include "../bytecode/bytecode.hh" #include "../bytecode/bytecode.hh"
namespace tyche { namespace tyche::vm {
enum class Instruction : uint8_t { enum class Instruction : uint8_t {
@@ -96,7 +96,10 @@ enum class Instruction : uint8_t {
}; };
std::pair<std::string, size_t> debug_instruction(Instruction inst, int oper=0); std::pair<std::string, size_t> debug_instruction(Instruction inst, int oper=0);
std::pair<std::string, size_t> debug_instruction(Bytecode const& bt, uint32_t function_id, uint32_t addr); std::pair<std::string, size_t> debug_instruction(bc::Bytecode const& bt, uint32_t function_id, uint32_t addr);
enum class OperandType { NoOperand, Int8, Int16, Int32 };
OperandType instruction_operand_type(Instruction instruction);
} }

16
src/vm/location.hh Normal file
View File

@@ -0,0 +1,16 @@
#ifndef TYCHE_LOCATION_HH
#define TYCHE_LOCATION_HH
#include <cstdint>
namespace tyche::vm {
struct Location
{
uint32_t function_id;
uint32_t pc;
};
}
#endif //TYCHE_LOCATION_HH

View File

@@ -2,7 +2,7 @@
#include "vm_exceptions.hh" #include "vm_exceptions.hh"
namespace tyche { namespace tyche::vm {
Stack::Stack() Stack::Stack()
{ {
@@ -16,7 +16,7 @@ void Stack::push(Value const& value)
Value Stack::pop() Value Stack::pop()
{ {
if (stack_.size() <= fps_.size()) if (stack_.size() <= fps_.top())
throw VMStackUnderflow(); throw VMStackUnderflow();
Value v = stack_.back(); Value v = stack_.back();
@@ -58,4 +58,15 @@ void Stack::pop_fp()
fps_.pop(); fps_.pop();
} }
std::string Stack::debug() const
{
if (stack_.empty())
return "empty";
std::string out;
for (size_t i = 0; i < stack_.size(); ++i)
out += "[" + stack_.at(i).to_string() + "] ";
return out;
}
} // tyche } // tyche

View File

@@ -6,7 +6,7 @@
#include "value.hh" #include "value.hh"
namespace tyche { namespace tyche::vm {
class Stack { class Stack {
public: public:
@@ -21,6 +21,10 @@ public:
void push_fp(); void push_fp();
void pop_fp(); void pop_fp();
[[nodiscard]] size_t fp_level() const { return fps_.size(); }
[[nodiscard]] std::string debug() const;
private: private:
std::vector<Value> stack_; std::vector<Value> stack_;
std::stack<size_t> fps_; std::stack<size_t> fps_;

View File

@@ -1,12 +1,15 @@
#include "gtest/gtest.h" #include "gtest/gtest.h"
#include "../bytecode/bytecodeprototype.hh" #include "../bytecode/bytecodeprototype.hh"
#include "../bytecode/bytearray.hh" #include "../common/bytearray.hh"
#include "../bytecode/bytecode.hh" #include "../bytecode/bytecode.hh"
#include "code.hh" #include "code.hh"
#include "stack.hh" #include "stack.hh"
#include "vm.hh"
using namespace tyche; using namespace tyche;
using namespace tyche::bc;
using namespace tyche::vm;
TEST(Code, ImportSingleAndDebug) TEST(Code, ImportSingleAndDebug)
{ {
@@ -68,6 +71,27 @@ TEST(Stack, FramePointer)
ASSERT_EQ(stack.at(-2).as_integer(), 10); ASSERT_EQ(stack.at(-2).as_integer(), 10);
} }
TEST(VM, BasicCode)
{
// code (2+3)
BytecodePrototype bp;
bp.functions.emplace_back(0, 0);
bp.functions.at(0).code.append_byte((uint8_t) Instruction::PushInt8);
bp.functions.at(0).code.append_int8(2);
bp.functions.at(0).code.append_byte((uint8_t) Instruction::PushInt8);
bp.functions.at(0).code.append_int8(3);
bp.functions.at(0).code.append_byte((uint8_t) Instruction::Sum);
bp.functions.at(0).code.append_byte((uint8_t) Instruction::Return);
ByteArray ba = Bytecode::generate(bp);
VM vm;
vm.load_bytecode(std::move(ba));
vm.call(0);
int32_t result = vm.to_integer(-1);
ASSERT_EQ(result, 5);
}
int main(int argc, char** argv) int main(int argc, char** argv)
{ {
testing::InitGoogleTest(&argc, argv); testing::InitGoogleTest(&argc, argv);

View File

@@ -2,7 +2,7 @@
#include "../common/overloaded.hh" #include "../common/overloaded.hh"
namespace tyche { namespace tyche::vm {
Type Value::type() const Type Value::type() const
{ {
@@ -11,6 +11,18 @@ Type Value::type() const
[](int32_t) { return Type::Integer; }, [](int32_t) { return Type::Integer; },
[](float) { return Type::Float; }, [](float) { return Type::Float; },
[](std::string const&) { return Type::String; }, [](std::string const&) { return Type::String; },
[](Function const&) { return Type::Function; },
}, value_);
}
std::string Value::to_string() const
{
return std::visit(overloaded {
[](std::monostate) { return std::string("nil"); },
[](int32_t i) { return std::to_string(i); },
[](float f) { return std::to_string(f); },
[](std::string const& s) { return s; },
[](Function const& f) { return "@" + std::to_string(f.f_id); }
}, value_); }, value_);
} }

View File

@@ -4,14 +4,19 @@
#include <string> #include <string>
#include <variant> #include <variant>
namespace tyche { namespace tyche::vm {
using FunctionId = uint32_t;
enum class Type : uint8_t enum class Type : uint8_t
{ {
Nil = 0, Integer, Float, String, Array, Table, Function, NativePointer, Nil = 0, Integer, Float, String, Array, Table, Function, NativePointer,
}; };
class Value { class Value {
struct Function { FunctionId f_id; };
public: public:
Value() : value_(std::monostate()) {} Value() : value_(std::monostate()) {}
@@ -19,15 +24,19 @@ public:
static Value CreateInteger(int32_t v) { return Value(v); } static Value CreateInteger(int32_t v) { return Value(v); }
static Value CreateFloat(float f) { return Value(f); } static Value CreateFloat(float f) { return Value(f); }
static Value CreateString(std::string const& str) { return Value(str); } static Value CreateString(std::string const& str) { return Value(str); }
static Value CreateFunctionId(FunctionId f_id) { return Value(Function { f_id }); }
[[nodiscard]] Type type() const; [[nodiscard]] Type type() const;
[[nodiscard]] int32_t as_integer() const { return std::get<int32_t>(value_); } [[nodiscard]] int32_t as_integer() const { return std::get<int32_t>(value_); }
[[nodiscard]] float as_float() const { return std::get<float>(value_); } [[nodiscard]] float as_float() const { return std::get<float>(value_); }
[[nodiscard]] std::string as_string() const { return std::get<std::string>(value_); } [[nodiscard]] std::string as_string() const { return std::get<std::string>(value_); }
[[nodiscard]] FunctionId as_function_id() const { return std::get<Function>(value_).f_id; }
[[nodiscard]] std::string to_string() const;
private: private:
using Internal = std::variant<std::monostate, int32_t, float, std::string>; using Internal = std::variant<std::monostate, int32_t, float, std::string, Function>;
Internal value_; Internal value_;
explicit Value(Internal const& internal) : value_(internal) {} explicit Value(Internal const& internal) : value_(internal) {}

79
src/vm/vm.cc Normal file
View File

@@ -0,0 +1,79 @@
#include "vm.hh"
#include "vm_exceptions.hh"
#include "expr.hh"
namespace tyche::vm {
void VM::load_bytecode(ByteArray const& ba)
{
FunctionId f_id = code_.import_bytecode(ba);
stack_.push(Value::CreateFunctionId(f_id));
}
void VM::call(size_t n_params)
{
// TODO - parameters
Value f = stack_.pop();
if (f.type() != Type::Function)
throw VMTypeError(Type::Function, f.type());
loc_.emplace(f.as_function_id(), 0);
stack_.push_fp();
run_until_return();
// stack_.pop_fp();
loc_.pop();
}
int32_t VM::to_integer(int index) const
{
Value i = stack_.at(index);
assert_type(i, Type::Integer);
return i.as_integer();
}
void VM::push_integer(int32_t value)
{
stack_.push(Value::CreateInteger(value));
}
void VM::run_until_return()
{
size_t level = stack_.fp_level();
while (stack_.fp_level() >= level)
step();
}
void VM::step()
{
Operation op = code_.operation(loc_.top());
switch (op.instruction) {
case Instruction::PushInt8:
case Instruction::PushInt16:
case Instruction::PushInt32:
push_integer(op.operator_);
break;
case Instruction::Sum:
stack_.push(binary_operation(stack_.pop(), stack_.pop(), BinaryOperationType::Sum));
break;
case Instruction::Return: {
Value v = stack_.pop();
stack_.pop_fp();
stack_.push(v);
return;
}
default:
throw VMInvalidOpcode((uint8_t) op.instruction);
}
loc_.top() = op.next_location;
}
void VM::assert_type(Value const& val, Type type)
{
if (val.type() != type)
throw VMTypeError(type, val.type());
}
} // tyche

35
src/vm/vm.hh Normal file
View File

@@ -0,0 +1,35 @@
#ifndef TYCHE_VM_HH
#define TYCHE_VM_HH
#include "code.hh"
#include "location.hh"
#include "stack.hh"
namespace tyche::vm {
class VM {
public:
void load_bytecode(ByteArray const& ba);
void call(size_t n_params);
[[nodiscard]] int32_t to_integer(int index) const;
void push_integer(int32_t value);
[[nodiscard]] std::string debug_stack() const { return stack_.debug(); }
private:
void run_until_return();
void step();
static void assert_type(Value const& val, Type type);
Stack stack_;
Code code_;
std::stack<Location> loc_;
};
} // tyche
#endif //TYCHE_VM_HH

View File

@@ -4,7 +4,9 @@
#include <stdexcept> #include <stdexcept>
#include <string> #include <string>
namespace tyche { #include "expr.hh"
namespace tyche::vm {
class VMRuntimeError : public std::runtime_error class VMRuntimeError : public std::runtime_error
{ {
@@ -24,6 +26,24 @@ public:
explicit VMStackOutOfRange() : VMRuntimeError("Item does not exist in stack") {} explicit VMStackOutOfRange() : VMRuntimeError("Item does not exist in stack") {}
}; };
class VMTypeError : public VMRuntimeError
{
public:
explicit VMTypeError(Type expected, Type found) : VMRuntimeError("Type error") {} // TODO - print types
};
class VMInvalidOpcode : public VMRuntimeError
{
public:
explicit VMInvalidOpcode(uint8_t opcode) : VMRuntimeError("Invalid opcode " + std::to_string(opcode)) {}
};
class VMInvalidOperation : public VMRuntimeError
{
public:
explicit VMInvalidOperation(BinaryOperationType op, Type type1, Type type2) : VMRuntimeError("Invalid binary operation") {} // TODO - print types
};
} }
#endif //TYCHE_VM_EXCEPTIONS_HH #endif //TYCHE_VM_EXCEPTIONS_HH