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
src/bytecode/bytearray.hh
src/bytecode/bytearray.cc
src/common/overloaded.hh
src/common/bytearray.hh
src/common/bytearray.cc
src/bytecode/bytecode.cc
src/bytecode/bytecode.hh
src/bytecode/bytecodeprototype.hh
src/common/overloaded.hh
src/bytecode/constant.hh
src/vm/code.cc
src/vm/code.hh
src/bytecode/constant.hh
src/vm/instruction.hh
src/vm/instruction.cc
src/vm/value.cc
@@ -75,6 +75,11 @@ add_library(lib${PROJECT_NAME} SHARED
src/vm/stack.cc
src/vm/stack.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})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,14 +2,51 @@
#include "../common/overloaded.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
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

View File

@@ -1,18 +1,30 @@
#ifndef TYCHE_CODE_HH
#define TYCHE_CODE_HH
#include "instruction.hh"
#include "location.hh"
#include "value.hh"
#include "../bytecode/bytecode.hh"
namespace tyche {
namespace tyche::vm {
struct Operation
{
Instruction instruction;
int32_t operator_;
Location next_location;
};
class Code {
public:
void import_bytecode(ByteArray incoming);
FunctionId import_bytecode(ByteArray incoming);
[[nodiscard]] std::string disassemble() const;
[[nodiscard]] Operation operation(Location const& location) const;
private:
Bytecode bytecode_;
bc::Bytecode bytecode_;
};
} // 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"
namespace tyche {
namespace tyche::vm {
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 = "???";
}
if ((uint8_t) inst < 0xa0)
OperandType operands = instruction_operand_type(inst);
if (operands == OperandType::NoOperand)
return { out, 1 };
out += " " + std::to_string(oper);
if ((uint8_t) inst >= 0xe0)
if (operands == OperandType::Int32)
return { out, 5 };
else if ((uint8_t) inst >= 0xc0)
if (operands == OperandType::Int16)
return { out, 3 };
else
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);
if ((uint8_t) inst >= 0xe0)
return debug_instruction(inst, bt.get_code_int32(function_id, addr + 1));
else if ((uint8_t) inst >= 0xc0)
return debug_instruction(inst, bt.get_code_int16(function_id, addr + 1));
else if ((uint8_t) inst >= 0xa0)
return debug_instruction(inst, bt.get_code_int8(function_id, addr + 1));
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)
return OperandType::Int32;
if ((uint8_t) inst >= 0xc0)
return OperandType::Int16;
if ((uint8_t) inst >= 0xa0)
return OperandType::Int8;
return OperandType::NoOperand;
}
}

View File

@@ -7,7 +7,7 @@
#include "../bytecode/bytecode.hh"
namespace tyche {
namespace tyche::vm {
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(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"
namespace tyche {
namespace tyche::vm {
Stack::Stack()
{
@@ -16,7 +16,7 @@ void Stack::push(Value const& value)
Value Stack::pop()
{
if (stack_.size() <= fps_.size())
if (stack_.size() <= fps_.top())
throw VMStackUnderflow();
Value v = stack_.back();
@@ -58,4 +58,15 @@ void Stack::pop_fp()
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

View File

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

View File

@@ -1,12 +1,15 @@
#include "gtest/gtest.h"
#include "../bytecode/bytecodeprototype.hh"
#include "../bytecode/bytearray.hh"
#include "../common/bytearray.hh"
#include "../bytecode/bytecode.hh"
#include "code.hh"
#include "stack.hh"
#include "vm.hh"
using namespace tyche;
using namespace tyche::bc;
using namespace tyche::vm;
TEST(Code, ImportSingleAndDebug)
{
@@ -68,6 +71,27 @@ TEST(Stack, FramePointer)
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)
{
testing::InitGoogleTest(&argc, argv);

View File

@@ -2,7 +2,7 @@
#include "../common/overloaded.hh"
namespace tyche {
namespace tyche::vm {
Type Value::type() const
{
@@ -11,6 +11,18 @@ Type Value::type() const
[](int32_t) { return Type::Integer; },
[](float) { return Type::Float; },
[](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_);
}

View File

@@ -4,14 +4,19 @@
#include <string>
#include <variant>
namespace tyche {
namespace tyche::vm {
using FunctionId = uint32_t;
enum class Type : uint8_t
{
Nil = 0, Integer, Float, String, Array, Table, Function, NativePointer,
};
class Value {
struct Function { FunctionId f_id; };
public:
Value() : value_(std::monostate()) {}
@@ -19,15 +24,19 @@ public:
static Value CreateInteger(int32_t v) { return Value(v); }
static Value CreateFloat(float f) { return Value(f); }
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]] int32_t as_integer() const { return std::get<int32_t>(value_); }
[[nodiscard]] float as_float() const { return std::get<float>(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:
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_;
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 <string>
namespace tyche {
#include "expr.hh"
namespace tyche::vm {
class VMRuntimeError : public std::runtime_error
{
@@ -24,6 +26,24 @@ public:
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