NateTheGrappler/OliNat-Programming-Language

A statically typed, bytecode-VM programming language written from scratch in C. It includes a static type checker, a mark-and-sweep garbage collector, a standard library (I/O, math, file handling, type conversions, and more), and support for classes, functions, and closures. Built and tested cross-platform with GitHub Actions (Ubuntu and Windows)

C

14

82 commits

updated Oct 4, 2026

See the code

See what people are saying

SourceMessageScoreDate

I built a browser playground and implementation-focused docs for my statically typed bytecode VM language (r/coolgithubprojects)

Hi everyone. Over the last few months I've been building Oli-Nat, a statically typed language with a hand-written bytecode VM in C. It began with Crafting Interpreters, and then I added a separate AST stage, a static type checker, typed arrays, classes with inheritance, and a small standard…

1

Oct 5, 2026

README

Oli-Nat-Banner

A statically-typed bytecode VM language built in C from scratch.

Overview

Oli-Nat compiles source code through a full pipeline into bytecode executed by a custom stack-based virtual machine. Every stage is hand-written with no external dependencies.

flowchart LR
    Source[Source Code] --> Scanner[Scanner]
    Scanner --> Parser[Pratt Parser]
    Parser --> AST[AST]
    AST --> TypeChecker[Type Checker]
    TypeChecker --> Compiler[Bytecode Compiler]
    Compiler --> VM[VM]
    
    VM --> |owns| GC[Garbage Collector]
    GC --> |marks/sweeps| Heap[Heap Objects]

Table of Contents

Language Features

  • Static typing with explicit type annotations
  • Integer, float, double, boolean, and string primitives
  • Static arrays with type inference
  • Global and local variable declarations with compound assignment operators (+=, -=, *=, /=, ++, --)
  • Lexical scoping with block statements
  • If/else, while, and for control flow
  • First-class functions with closures and upvalue capture
  • Classes with typed fields, default values, and methods
  • Class instance creation and field access via dot notation
  • Classes with inheritance and custom constructors
  • String concatenation and O(1) string equality via interning
  • Arithmetic and comparison operators with numeric type promotion
  • Standard library via #pullf imports

Syntax Example

#pullf io

class Player
{
    make int health = 100;
    make string name = "hero";

    make empty takeDamage(int amount)
    {
        health = health - amount;
    }
}

make Player p = Player();
p.health = p.health - 10;
println(p.health);

make int x = 10;
make string greeting = "hello";

if (x > 5)
{
    make int y = x + 1;
    println(y);
}

for (make int i = 0; i < 5; i++)
{
    println(i);
}

Standard Library

Imported via #pullf <library>. Available libraries:

LibraryContents
ioprint, println, intake
mathsin, cos, tan, sqrt, pow, floor, ceil, abs, ln, log10, log2, expo
randomrandom, seed, randint
chronosclock, time, sleep, dateString, timeString
fileIOreadFile, writeFile, appendFile, fileExists, deleteFile
typesintToStr, intToDouble, intToFloat, doubleToStr, doubleToInt, doubleToFloat, floatToStr, floatToInt, floatToDouble, strToInt, strToDouble, strToFloat, strToBool, boolToStr
StringsstrLength, strContains, strSlice, strToUpper, strToLower, strReplace, strReverse
utilslength, assert
stdlibAll of the above

Architecture

Pipeline Stages

Scanner (scanner.h/c) — lazy token-at-a-time scanning, O(1) memory. Keyword recognition via a trie-style switch on the first character.

Pratt Parser (ASTcompiler.h/c) — two-pass compiler. The first pass pre-registers all function and class declarations so forward references and mutual recursion work correctly. The second pass produces a full AST and emits bytecode. Each token type has an associated prefix and infix parse function with a precedence level.

AST (Expr.h/c) — discriminated union with anonymous union fields. Node types cover literals, binary/unary expressions, variables, assignments, function calls, array operations, logical operators, and field get/set expressions.

Type Checker (typeChecker.h/c) — runs during the second pass, before bytecode emission. Maintains a flat symbol table mapping variable names to declared types, scope depths, and function signatures. For class symbols, field metadata is stored as a heap-allocated array of CheckerFieldInfo structs populated during the first pass. Rejects type mismatches, undeclared variables, invalid field accesses, and bad function call signatures at compile time.

Bytecode Compiler (Bytecompiler.h/c) — walks the AST recursively and emits bytecode into a Chunk. Resolves variable references to stack slots (locals), upvalues (captured locals), or global name constants at compile time.

VM (vm.h/c) — stack-based interpreter with a call frame stack supporting nested function calls and closures. Dispatch loop over opcodes with type-promoted arithmetic and direct stack slot access for locals.

Garbage Collector (memory.h/c) — tri-color mark-and-sweep GC. Objects are linked in a VM-owned intrusive list. The GC traces roots from the value stack, call frames, open upvalues, and the globals table. Class objects mark their methods hashmap and field default values; instances mark their class and field value array.

Key Data Structures

Value — tagged union:

typedef struct {
    ValueType type;
    union { bool boolean_val; int integer_val; float float_val;
            double double_val; Obj* object_val; } as;
} Value;

ObjString — heap string with cached FNV-1a hash for O(1) interned equality.

ObjFunction / ObjClosure — functions carry their bytecode chunk, arity, parameter type info, and return type. Closures wrap a function with a captured upvalue array.

ObjClass — holds the class name, a fixed-size FieldInfo array with per-field type, name, and default value, a field count, and a hashmap of methods.

ObjInstance — holds a pointer to its class and a heap-allocated Value array for field storage, initialized from the class's default values at instantiation.

Hashmap — open addressing with linear probing and tombstone deletion. Used for the string intern table, global variables, and class method tables.

Chunk — bytecode buffer with a parallel constants array and line info.

Memory Model

All heap objects are allocated through a central reallocate function that tracks total bytes and triggers garbage collection when a growth threshold is crossed. The GC threshold grows by a configurable factor after each collection. The gray stack used during marking is allocated separately with raw realloc to avoid re-entrancy issues.

Design Decisions

String interning — all strings are deduplicated on creation via FNV-1a hashing. Identical strings share the same pointer, so equality is a single pointer comparison. This also makes field name lookup in OP_GET_FIELD and OP_SET_FIELD an O(1) pointer comparison rather than memcmp.

Static typing eliminates runtime type guards — the type checker rejects any program that would reach an invalid type combination at runtime. The IS_ macros in the VM serve as dispatch tools, not safety checks.

Locals are stack slots, not named variables — declaring a local variable pushes its initializer value onto the stack. The compiler tracks which stack slot each name maps to. No OP_DEFINE_LOCAL opcode is needed.

Two-pass compilation — the first pass scans for function and class declarations and registers their signatures in the type checker's symbol table. The second pass can then type-check calls and field accesses against those signatures without requiring forward declarations in source code.

Class fields use slot indices — at compile time each field is assigned a slot index. At runtime ObjInstance.fields is a plain Value array indexed by slot. Field name lookup only happens at class definition time via OP_CLASS_FIELD; at runtime OP_GET_FIELD and OP_SET_FIELD use interned string pointer comparison to find the right slot.

Flat symbol table — the type checker uses a simple array of Symbol structs scanned backwards so inner scopes shadow outer ones. Symbols are popped in sync with the compiler's locals array when a scope ends.

Patchable jumps — emitJump writes a placeholder two-byte operand and returns its offset. patchJump backfills the real offset once the jump target is known.

Opcodes

OpcodeDescription
OP_CONSTANT / _LONGPush constant onto stack
OP_ADD / SUB / MUL / DIVArithmetic with numeric type promotion
OP_NEGATE / INVERSEUnary minus and boolean not
OP_EQUAL / NOT_EQUALEquality (pointer comparison for strings)
OP_GREATER / LESS / _EQUALNumeric comparisons
OP_DEFINE_GLOBALPop value, store in globals hashmap
OP_GET_GLOBAL / SET_GLOBALHashmap lookup and update by interned name
OP_GET_LOCAL / SET_LOCALDirect stack slot access by index
OP_GET_UPVALUE / SET_UPVALUEAccess captured variables through closure
OP_CLOSE_UPVALUEMove upvalue from stack to heap on scope exit
OP_JUMPUnconditional forward jump
OP_JUMP_IF_FALSEConditional jump, leaves condition on stack
OP_LOOPJump backwards to repeat a loop body
OP_CALLCall a closure or native function
OP_CLOSUREWrap a function in a closure with upvalue bindings
OP_RETURNReturn from function, restore call frame
OP_MISSING_RETURNRuntime error for non-void functions without return
OP_CREATE_ARRAYCollect N stack values into a static array object
OP_GET_ARRAY_INDEXIndex into an array
OP_SET_ARRAY_INDEXAssign to an array element
OP_CLASSCreate a class object and push onto stack
OP_CLASS_FIELDAttach a field with default value to a class
OP_CLASS_METHODAttach a method closure to a class
OP_FIELD_DEFAULTPush a zero default value for a given type
OP_GET_FIELDGet a field or method from an instance
OP_SET_FIELDSet a field on an instance with type checking
OP_POPDiscard top of stack

Building From Source:

Linux / MacOS

git clone https://github.com/NateTheGrappler/OliNat-Programming-Language.git
cd OliNat-Programming-Language
mkdir cmake-build
cd cmake-build
cmake ..
make
./Oli_Nat </path/to/your/source.oli>

Windows (MSYS2 / MinGW-w64)

  1. Install MSYS2
  2. Open the UCRT64 shell and install the toolchain:
pacman -S mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-make
  1. Build inside said shell
mkdir cmake-build
cd cmake-build
cmake .. -G "MinGW Makefiles"
mingw32-make
  1. Run Program In any terminal
./Oli_Nat.exe </path/to/your/source.oli>

Example Programs

The testCases/ folder has runnable .oli example programs covering classes, control flow, and the standard library. Once built, try one out:

Run inside of cmake-build directory containing Oli_Nat executable

./Oli_Nat ../testCases/Classes/InheritanceTestBaby.oli

Developer Tips:

Enable debug tracing by defining DEBUG_TRACE_EXECUTION to print the stack state and disassembled instruction before each opcode. Define DEBUG_LOG_GC to trace garbage collection events. You can also uncomment the santitization libraries in the cmake.txt file in order to utilize that library if you have it installed Feel free to look through the code's guts and have fun with it as I did!

Roadmap

  • Adding in the ability to link code through multiple files
  • Native Random Functions
  • Native Dynamic Arrays and Hashmaps/Dictionaries
  • Possibly a graphics library

Significant stargazers

Dmitry Atamanov

197 followers · starred Jun 2026

NateTheGrappler/OliNat-Programming-Language

A statically typed, bytecode-VM programming language written from scratch in C. It includes a static type checker, a mark-and-sweep garbage collector, a standard library (I/O, math, file handling, type conversions, and more), and support for classes, functions, and closures. Built and tested cross-platform with GitHub Actions (Ubuntu and Windows)

C

14

82 commits

updated Oct 4, 2026

See the code

See what people are saying

SourceMessageScoreDate

I built a browser playground and implementation-focused docs for my statically typed bytecode VM language (r/coolgithubprojects)

Hi everyone. Over the last few months I've been building Oli-Nat, a statically typed language with a hand-written bytecode VM in C. It began with Crafting Interpreters, and then I added a separate AST stage, a static type checker, typed arrays, classes with inheritance, and a small standard…

1

Oct 5, 2026

README

Oli-Nat-Banner

A statically-typed bytecode VM language built in C from scratch.

Overview

Oli-Nat compiles source code through a full pipeline into bytecode executed by a custom stack-based virtual machine. Every stage is hand-written with no external dependencies.

flowchart LR
    Source[Source Code] --> Scanner[Scanner]
    Scanner --> Parser[Pratt Parser]
    Parser --> AST[AST]
    AST --> TypeChecker[Type Checker]
    TypeChecker --> Compiler[Bytecode Compiler]
    Compiler --> VM[VM]
    
    VM --> |owns| GC[Garbage Collector]
    GC --> |marks/sweeps| Heap[Heap Objects]

Table of Contents

Language Features

  • Static typing with explicit type annotations
  • Integer, float, double, boolean, and string primitives
  • Static arrays with type inference
  • Global and local variable declarations with compound assignment operators (+=, -=, *=, /=, ++, --)
  • Lexical scoping with block statements
  • If/else, while, and for control flow
  • First-class functions with closures and upvalue capture
  • Classes with typed fields, default values, and methods
  • Class instance creation and field access via dot notation
  • Classes with inheritance and custom constructors
  • String concatenation and O(1) string equality via interning
  • Arithmetic and comparison operators with numeric type promotion
  • Standard library via #pullf imports

Syntax Example

#pullf io

class Player
{
    make int health = 100;
    make string name = "hero";

    make empty takeDamage(int amount)
    {
        health = health - amount;
    }
}

make Player p = Player();
p.health = p.health - 10;
println(p.health);

make int x = 10;
make string greeting = "hello";

if (x > 5)
{
    make int y = x + 1;
    println(y);
}

for (make int i = 0; i < 5; i++)
{
    println(i);
}

Standard Library

Imported via #pullf <library>. Available libraries:

LibraryContents
ioprint, println, intake
mathsin, cos, tan, sqrt, pow, floor, ceil, abs, ln, log10, log2, expo
randomrandom, seed, randint
chronosclock, time, sleep, dateString, timeString
fileIOreadFile, writeFile, appendFile, fileExists, deleteFile
typesintToStr, intToDouble, intToFloat, doubleToStr, doubleToInt, doubleToFloat, floatToStr, floatToInt, floatToDouble, strToInt, strToDouble, strToFloat, strToBool, boolToStr
StringsstrLength, strContains, strSlice, strToUpper, strToLower, strReplace, strReverse
utilslength, assert
stdlibAll of the above

Architecture

Pipeline Stages

Scanner (scanner.h/c) — lazy token-at-a-time scanning, O(1) memory. Keyword recognition via a trie-style switch on the first character.

Pratt Parser (ASTcompiler.h/c) — two-pass compiler. The first pass pre-registers all function and class declarations so forward references and mutual recursion work correctly. The second pass produces a full AST and emits bytecode. Each token type has an associated prefix and infix parse function with a precedence level.

AST (Expr.h/c) — discriminated union with anonymous union fields. Node types cover literals, binary/unary expressions, variables, assignments, function calls, array operations, logical operators, and field get/set expressions.

Type Checker (typeChecker.h/c) — runs during the second pass, before bytecode emission. Maintains a flat symbol table mapping variable names to declared types, scope depths, and function signatures. For class symbols, field metadata is stored as a heap-allocated array of CheckerFieldInfo structs populated during the first pass. Rejects type mismatches, undeclared variables, invalid field accesses, and bad function call signatures at compile time.

Bytecode Compiler (Bytecompiler.h/c) — walks the AST recursively and emits bytecode into a Chunk. Resolves variable references to stack slots (locals), upvalues (captured locals), or global name constants at compile time.

VM (vm.h/c) — stack-based interpreter with a call frame stack supporting nested function calls and closures. Dispatch loop over opcodes with type-promoted arithmetic and direct stack slot access for locals.

Garbage Collector (memory.h/c) — tri-color mark-and-sweep GC. Objects are linked in a VM-owned intrusive list. The GC traces roots from the value stack, call frames, open upvalues, and the globals table. Class objects mark their methods hashmap and field default values; instances mark their class and field value array.

Key Data Structures

Value — tagged union:

typedef struct {
    ValueType type;
    union { bool boolean_val; int integer_val; float float_val;
            double double_val; Obj* object_val; } as;
} Value;

ObjString — heap string with cached FNV-1a hash for O(1) interned equality.

ObjFunction / ObjClosure — functions carry their bytecode chunk, arity, parameter type info, and return type. Closures wrap a function with a captured upvalue array.

ObjClass — holds the class name, a fixed-size FieldInfo array with per-field type, name, and default value, a field count, and a hashmap of methods.

ObjInstance — holds a pointer to its class and a heap-allocated Value array for field storage, initialized from the class's default values at instantiation.

Hashmap — open addressing with linear probing and tombstone deletion. Used for the string intern table, global variables, and class method tables.

Chunk — bytecode buffer with a parallel constants array and line info.

Memory Model

All heap objects are allocated through a central reallocate function that tracks total bytes and triggers garbage collection when a growth threshold is crossed. The GC threshold grows by a configurable factor after each collection. The gray stack used during marking is allocated separately with raw realloc to avoid re-entrancy issues.

Design Decisions

String interning — all strings are deduplicated on creation via FNV-1a hashing. Identical strings share the same pointer, so equality is a single pointer comparison. This also makes field name lookup in OP_GET_FIELD and OP_SET_FIELD an O(1) pointer comparison rather than memcmp.

Static typing eliminates runtime type guards — the type checker rejects any program that would reach an invalid type combination at runtime. The IS_ macros in the VM serve as dispatch tools, not safety checks.

Locals are stack slots, not named variables — declaring a local variable pushes its initializer value onto the stack. The compiler tracks which stack slot each name maps to. No OP_DEFINE_LOCAL opcode is needed.

Two-pass compilation — the first pass scans for function and class declarations and registers their signatures in the type checker's symbol table. The second pass can then type-check calls and field accesses against those signatures without requiring forward declarations in source code.

Class fields use slot indices — at compile time each field is assigned a slot index. At runtime ObjInstance.fields is a plain Value array indexed by slot. Field name lookup only happens at class definition time via OP_CLASS_FIELD; at runtime OP_GET_FIELD and OP_SET_FIELD use interned string pointer comparison to find the right slot.

Flat symbol table — the type checker uses a simple array of Symbol structs scanned backwards so inner scopes shadow outer ones. Symbols are popped in sync with the compiler's locals array when a scope ends.

Patchable jumps — emitJump writes a placeholder two-byte operand and returns its offset. patchJump backfills the real offset once the jump target is known.

Opcodes

OpcodeDescription
OP_CONSTANT / _LONGPush constant onto stack
OP_ADD / SUB / MUL / DIVArithmetic with numeric type promotion
OP_NEGATE / INVERSEUnary minus and boolean not
OP_EQUAL / NOT_EQUALEquality (pointer comparison for strings)
OP_GREATER / LESS / _EQUALNumeric comparisons
OP_DEFINE_GLOBALPop value, store in globals hashmap
OP_GET_GLOBAL / SET_GLOBALHashmap lookup and update by interned name
OP_GET_LOCAL / SET_LOCALDirect stack slot access by index
OP_GET_UPVALUE / SET_UPVALUEAccess captured variables through closure
OP_CLOSE_UPVALUEMove upvalue from stack to heap on scope exit
OP_JUMPUnconditional forward jump
OP_JUMP_IF_FALSEConditional jump, leaves condition on stack
OP_LOOPJump backwards to repeat a loop body
OP_CALLCall a closure or native function
OP_CLOSUREWrap a function in a closure with upvalue bindings
OP_RETURNReturn from function, restore call frame
OP_MISSING_RETURNRuntime error for non-void functions without return
OP_CREATE_ARRAYCollect N stack values into a static array object
OP_GET_ARRAY_INDEXIndex into an array
OP_SET_ARRAY_INDEXAssign to an array element
OP_CLASSCreate a class object and push onto stack
OP_CLASS_FIELDAttach a field with default value to a class
OP_CLASS_METHODAttach a method closure to a class
OP_FIELD_DEFAULTPush a zero default value for a given type
OP_GET_FIELDGet a field or method from an instance
OP_SET_FIELDSet a field on an instance with type checking
OP_POPDiscard top of stack

Building From Source:

Linux / MacOS

git clone https://github.com/NateTheGrappler/OliNat-Programming-Language.git
cd OliNat-Programming-Language
mkdir cmake-build
cd cmake-build
cmake ..
make
./Oli_Nat </path/to/your/source.oli>

Windows (MSYS2 / MinGW-w64)

  1. Install MSYS2
  2. Open the UCRT64 shell and install the toolchain:
pacman -S mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-make
  1. Build inside said shell
mkdir cmake-build
cd cmake-build
cmake .. -G "MinGW Makefiles"
mingw32-make
  1. Run Program In any terminal
./Oli_Nat.exe </path/to/your/source.oli>

Example Programs

The testCases/ folder has runnable .oli example programs covering classes, control flow, and the standard library. Once built, try one out:

Run inside of cmake-build directory containing Oli_Nat executable

./Oli_Nat ../testCases/Classes/InheritanceTestBaby.oli

Developer Tips:

Enable debug tracing by defining DEBUG_TRACE_EXECUTION to print the stack state and disassembled instruction before each opcode. Define DEBUG_LOG_GC to trace garbage collection events. You can also uncomment the santitization libraries in the cmake.txt file in order to utilize that library if you have it installed Feel free to look through the code's guts and have fun with it as I did!

Roadmap

  • Adding in the ability to link code through multiple files
  • Native Random Functions
  • Native Dynamic Arrays and Hashmaps/Dictionaries
  • Possibly a graphics library

Significant stargazers

Dmitry Atamanov

197 followers · starred Jun 2026