A high level object oriented programming language for the 20s
  • Rust 98.6%
  • C 0.6%
  • Python 0.5%
  • Nix 0.3%
Find a file
2025-09-14 20:14:39 -06:00
headers changed IOLock to be a little more thread safe. Fixed some errors in rowan_runtime.h 2025-09-13 10:12:44 -06:00
libunwind implemented collecting of live objects on the stack into a hashmap 2025-06-28 20:55:16 -07:00
pool-box added poolbox, a way to preallocate memory and reuse memory. Switched Stackframes to use this strategy. Need to use perf to figure out new bottleneck 2025-08-11 12:09:25 -07:00
rowan removed extra Cargo.lock files 2025-08-31 09:24:38 -06:00
rowan-register-debug moved rowan-register-debug into rowan-unwind due to shared need of windows handles. 2025-07-21 19:58:29 -07:00
rowan-runtime added external functions that can create contexts and destroy them 2025-09-01 17:25:48 -06:00
rowan-test-files fixed issues preventing most specializing of generics and inlining of imports 2025-09-14 20:13:21 -06:00
rowan-unwind removed dependency on rstack and moved that library into workspace 2025-09-01 12:55:08 -06:00
rowan_shared fixed errors in parsing binary into structured files. found error in compiler that generates strings to classes that don't exist 2025-08-23 20:16:02 -07:00
rowanc cleaned up some debug prints 2025-09-14 20:14:39 -06:00
std changed IOLock to be a little more thread safe. Fixed some errors in rowan_runtime.h 2025-09-13 10:12:44 -06:00
unwind-sys removed dependency on rstack and moved that library into workspace 2025-09-01 12:55:08 -06:00
.gitignore updated gitignore 2025-01-26 13:53:05 -07:00
Cargo.lock removed dependency on rstack and moved that library into workspace 2025-09-01 12:55:08 -06:00
Cargo.toml removed dependency on rstack and moved that library into workspace 2025-09-01 12:55:08 -06:00
generate_function_pointer_table.py altered generate_function_pointer_table to do specific arity 2025-01-26 19:57:00 -07:00
LICENSE Create LICENSE 2025-01-19 19:10:05 -07:00
README.md updated README 2025-01-19 21:04:49 -07:00
rowan.nix removed dependency on rstack and moved that library into workspace 2025-09-01 12:55:08 -06:00
shell.nix minorly improved performance of interpreter 2025-08-09 12:10:50 -07:00
todo.md updated todo.md with new tasks 2025-08-18 20:15:54 -07:00

Design

This language's design is borrowed from Godot. The main idea taken from Godot is that all objects are nodes on a graph. This means that you can change/add functionality by adding new nodes to other nodes. To take advantage of this design, objects may arbitrarily add or remove object from themselves. Objects attached this way will be updated every heartbeat.

The heartbeat is a message that all objects attached to the main object will respond to via the tick method. This method takes a f64 of the amount of seconds elapsed since the last heartbeat. This allows for time dependent code to be processed, without the need of sleep. Objects will also have a ready method that takes zero arguments that is called when an object is attached to to another object. The main's ready method is the first method called on VM startup.

There are 4 different ways of calling a method.

  1. Normal Method calls
    • These are pretty much methods from Java and they are synchronous, meaning that calling a method blocks the current method until it is done.
  2. Signals
    • These are like signals from Godot. They are asynchronous and call methods on objects that have been connected to to the object that emitted the signal.
  3. Static Signals
    • These are a special type of signals. They are connected at link time/compile time and all of the objects that have methods that are listening for the signal will have their methods called.
  4. Remote Proceedure calls
    • These are a work in progress and as such, don't have any documentation

Design Implementation

There will be a max heap priority queue that dictates which messages will be handled first. Tick (the heartbeat) always comes first, and as such, will not be in the queue. Messages are added to a lockless queue every heartbeat and then dumped into the priority queue during the processing of the tick message. The priority is dictated by a graph coloring algorithm to ensure thread safety. The way it should work is that is will order the messages based off of availablity. Thread safety is preserved by ensuring that an object is only in one thread at a time. This means that the order of messages shouldn't matter.

There will be 2 lockless queues. This ensures that we can add messages while processing tick and message order. The queues should be of a sufficient size to handle the adding an unknown number of messages

Garbage Collection

We can exploit the fact that there is a pause time in the running of the virtual machine. This means that all live memory only exists in either an object's children, one of an object's members, or in a message. A garbage collection could occur after a certain amount of ticks and before message processing. Since we have a threadpool, we can do a threaded divide and conquer algorithm to find all live memory.

Object Methods

These are the methods on the base object. All object types inherit from object.

Method Description Signature
tick Incremental processing method, called each heartbeat. (f64) -> void
ready Initialization method, called when an object is attached to a parent object. () -> void
upcast Returns the object as a different type if it can be, otherwise raise an error. [T]() -> T
try-upcast Returns the object as a different type if it can be as an Option type. [T]() -> Option[T]

Structures

These all use Rust-like syntax

type Symbol = usize; // An index into a symbol table

type Reference = usize;
type Index = usize; // Represents and index into a table
type VTableIndex = usize; // Represents an index into a table of VTables

enum TypeTag {
    U8,
    U16,
    U32,
    U64,
    I8,
    I16,
    I32,
    I64,
    F32,
    F64,
    Object, // u64
    Str,
}

Class

VM Structure
struct Class {
    name: Symbol,
    parents: Vec<Symbol>, // Is mirrored in Object.parent_objects
    vtables: Map<(Symbol, Symbol), VTableIndex>, // Key is (Starting Parent, Particular Child), value is the vtable for that object. This allows for calling parent methods via super while still overloading parent vtables
    members: Vec<MemberInfo>,
    signals: Vec<SignalInfo>,
}

struct VTable {
    symbol_mapper: Map<Symbol, Index>,
    table: [VirtFunc]
}

struct VirtFunc {
    name: Symbol,
    value: VirtFuncValue,
    responds_to: Option<Symbol>,
    arguments: Vec<TypeTag>,
    returnType: TypeTag
}

enum VirtFuncValue {
    Builtin,
    Bytecode,
    Compiled,
}

struct MemberInfo {
    type_tag: TypeTag,
    name: Symbol,
}

struct SignalInfo {
    name: Symbol,
    static: bool,
    arguments: Vec<TypeTag>,
}

File Structure
type StringIndex = u64;
type BytecodeIndex = u64;

enum TypeTag {
    Void,
    U8,
    U16,
    U32,
    U64,
    I8,
    I16,
    I32,
    I64,
    Str,
    Object,
}

struct ClassFile {
    magic: u8,
    major_ver: u8,
    minor_ver: u8,
    patch_ver: u8,
    name: StringIndex,
    parent_count: u8,
    parent_names: [StringIndex; parent_count],
    vtable_size: u64,
    vtables: [VTable; vtable_size],
    members_size: u64,
    members_names: [StringIndex; members_size],
    signals_count: u64,
    signals: [Signal; signals_count],
    bytecode_table_size: u64,
    bytecode_table: [BytecodeEntry],
    string_table_size: u64,
    string_table: [StringEntry], // one indexed
    signature_table_size: u64,
    signature_table: [SignatureEntry]
}

struct VTable {
    size: u64,
    /// Name, Responds to (zero means doesn't respond), signature of function, index into bytecode table
    functions: [(StringIndex, StringIndex, SignatureIndex, BytecodeIndex); size],
}

struct BytecodeEntry {
    size: u64,
    code: [u8],
}

struct StringEntry {
    size: u64,
    data: [u8],
}

struct SignatureEntry {
    size: u64,
    types: [TypeTag], // First value is always the return type
}

Object

struct Object {
    class: Symbol,
    parent_objects_size: usize,
    parent_objects: *mut [Reference], // Mirrors Class.parents
    children_size: usize,
    children: *mut[Reference],
    data: [u8], // Binary data
}

Bytecode


type TypeTag = u8;
type BlockId = usize;

enum Bytecode {
    Nop,
    Breakpoint,
    LoadU8(u8),
    LoadU16(u16),
    LoadU32(u32),
    LoadU64(u64),
    LoadI8(i8),
    LoadI16(i16),
    LoadI32(i32),
    LoadI64(i64),
    LoadF32(f32),
    LoadF64(f64),
    Pop,
    Dup,
    Swap,
    StoreLocal(u8)
    LoadLocal(u8)
    StoreArgument(u8)
    Add,
    Sub,
    Mul,
    Div,
    Mod,
    SatAdd,
    SatSub,
    SatMul,
    SatDiv,
    SatMod,
    And,
    Or,
    Xor,
    Not,
    AShl,
    LShl,
    AShr,
    LShr,
    Neg,
    Equal,
    NotEqual,
    Greater,
    Less,
    GreaterOrEqual,
    LessOrEqual,
    Convert(TypeTag),
    BinaryConvert(TypeTag),
    CreateArray(TypeTag),
    ArrayGet(TypeTag),
    ArraySet(TypeTag),
    NewObject(Symbol),
    GetField(Symbol, Symbol, usize), // Class name, Class name, Member. The second Class name is to allow for selecting the particular parent to access the field.
    SetField(Symbol, Symbol, usize), // Class name, Class name, Member. The second Class name is to allow for selecting the particular parent to access the field.
    IsA(Symbol),
    InvokeVirt(Symbol, Symbol, Symbol), // Class Name, Class Name, Function Name. The two class names allow for calling super methods as well as overridden super methods
    InvokeVirtTail(Symbol, Symbol, Symbol), // Class Name, Class Name, Function Name. The two class names allow for calling super methods as well as overridden super methods
    EmitSignal(Symbol, Symbol), // Class Name, Signal Name
    EmitStaticSignal(Symbol, Symbol), // Class Name, Signal Name
    ConnectSignal(Symbol, Symbol, Symbol, Symbol), // Signal Name, Class Name, Class Name, Method Name. The top two stack values are used for this. The top object is connected to the bottom object's signal via the 2nd and 3rd Class Names + the Method Name
    GetStrRef(Symbol),
    Return,
    ReturnVoid,
    StartBlock(usize),
    Goto(BlockId), // Offset to next block from current block
    If(BlockId, BlockId),
    Switch(Vec<BlockId>, BlockId),
    
}