Skip to content

markoelez/oxide

Repository files navigation

oxide

CI

A toy compiled programming language with Python/Rust hybrid syntax, targeting ARM64 macOS.

Installation

# Clone the repository
git clone https://github.com/markoelez/oxide.git
cd oxide

# Create virtual environment and install
uv venv
source .venv/bin/activate
uv pip install -e .

Usage

# Compile a source file to executable
oxide source.ox

# Specify output file
oxide source.ox -o myprogram

# Output assembly only
oxide source.ox --emit-asm

# Keep assembly file alongside binary
oxide source.ox --keep-asm

Requirements

  • Python 3.12+
  • macOS with ARM64 (Apple Silicon)
  • Xcode Command Line Tools (for as and ld)

Architecture

The compiler is structured as follows:

Source Code → Lexer → Parser → Type Checker → Code Generator → ARM64 Assembly

Modules

  • tokens.py - Token definitions
  • lexer.py - Tokenization with Python-style indentation handling
  • ast.py - AST node dataclasses
  • parser.py - Recursive descent parser with precedence climbing
  • checker.py - Type checking with scoped symbol tables
  • codegen.py - ARM64 assembly generation for macOS
  • compiler.py - Pipeline orchestration
  • cli.py - Command-line interface

Language Features

Oxide combines Python's indentation-based blocks with Rust's explicit type annotations:

# Ownership and borrowing (like Rust)
struct Point:
    x: i64
    y: i64

impl Point:
    fn sum(self) -> i64:
        return self.x + self.y

fn read_point(p: &Point) -> i64:    # Borrow immutably
    return p.x + p.y

fn modify_point(p: &mut Point) -> i64:  # Borrow mutably
    p.x = p.x + 10
    return p.x

fn main() -> i64:
    let p: Point = Point { x: 10, y: 20 }
    
    # Multiple shared borrows are OK
    let sum1: i64 = read_point(&p)
    let sum2: i64 = read_point(&p)
    
    # Mutable borrow is exclusive
    let result: i64 = modify_point(&mut p)
    
    # Can still use after borrow ends
    print(p.sum())
    
    return 0

Ownership System:

  • Values have a single owner; ownership transfers on assignment/function calls
  • Primitives (i64, bool) are Copy - they clone instead of move
  • &T - shared/immutable borrow (multiple allowed)
  • &mut T - exclusive/mutable borrow (only one at a time)
  • Cannot use values after they've been moved
  • Cannot move values inside loops

Rust-style Implicit Return:

fn factorial(n: i64) -> i64:
    if n <= 1:
        return 1
    n * factorial(n - 1)  # Last expression is implicitly returned

fn square(x: i64) -> i64:
    x * x  # No 'return' keyword needed

fn main() -> i64:
    print(factorial(5))  # 120
    0  # Implicit return

Variables and Constants:

fn main() -> i64:
    # Mutable variable (can be reassigned)
    let x: i64 = 10
    x = 20  # OK
    
    # Constant (cannot be reassigned)
    const PI_APPROX: i64 = 3
    # PI_APPROX = 4  # Error: Cannot assign to const variable
    
    # Const with structs
    const origin: Point = Point { x: 0, y: 0 }
    # origin.x = 1  # Error: Cannot assign to const variable
    
    x + PI_APPROX

Rust-style Type Aliases:

# Simple type aliases
type Integer = i64
type IntVec = vec[i64]
type IntPair = (i64, i64)

# Type alias for struct
struct Point:
    x: i64
    y: i64
type Vec2D = Point

fn add(a: Integer, b: Integer) -> Integer:
    a + b

fn main() -> i64:
    let x: Integer = 10
    let nums: IntVec = []
    nums.push(1)
    let p: Vec2D = Point { x: 5, y: 10 }
    add(x, p.x)  # 15

Result Type and Error Propagation (like Rust):

# Function that can fail returns Result[OkType, ErrType]
fn divide(a: i64, b: i64) -> Result[i64, i64]:
    if b == 0:
        return Err(1)  # Error code for division by zero
    Ok(a / b)

# The ? operator unwraps Ok or propagates Err
fn calculate(x: i64, y: i64, z: i64) -> Result[i64, i64]:
    let step1: i64 = divide(x, y)?  # Returns early with Err if y == 0
    let step2: i64 = divide(step1, z)?  # Returns early with Err if z == 0
    Ok(step1 + step2)

fn main() -> i64:
    let result: Result[i64, i64] = calculate(100, 5, 2)
    match result:
        Ok(value):
            print(value)  # 30
        Err(error):
            print(error)
    0

Option Type and ? Operator (like Rust):

# Generic Option enum for nullable values
enum Option<T>:
    Some(T)
    None

fn get_positive(x: i64) -> Option<i64>:
    if x > 0:
        Option<i64>::Some(x)
    else:
        Option<i64>::None

# The ? operator unwraps Some or propagates None
fn add_positives(a: i64, b: i64) -> Option<i64>:
    let x: i64 = get_positive(a)?  # Returns early with None if a <= 0
    let y: i64 = get_positive(b)?  # Returns early with None if b <= 0
    Option<i64>::Some(x + y)

fn main() -> i64:
    let result: Option<i64> = add_positives(10, 20)
    match result:
        Option<i64>::Some(val):
            print(val)  # 30
        Option<i64>::None:
            print(0)
    0

Rust-style Pattern Guards:

enum Option:
    Some(i64)
    None

fn classify(opt: Option) -> i64:
    match opt:
        # Pattern guards with 'if' clause
        Option::Some(x) if x < 0:
            print(-1)  # Negative
            -1
        Option::Some(x) if x == 0:
            print(0)   # Zero
            0
        Option::Some(x) if x < 10:
            print(1)   # Small positive
            1
        Option::Some(x):
            print(2)   # Large positive (fallback)
            2
        Option::None:
            print(99)
            99

fn main() -> i64:
    let a: Option = Option::Some(5)
    classify(a)  # Prints 1 (small positive)
    
    # Guards can use chained comparisons
    let b: Option = Option::Some(50)
    match b:
        Option::Some(x) if 0 < x < 100:
            print(1)  # In range
        Option::Some(x):
            print(0)  # Out of range
        Option::None:
            print(-1)
    
    0

Python/Rust-style Hashmaps (dict):

fn main() -> i64:
    # Create and use dict
    let scores: dict[i64, i64] = {}
    scores[1] = 100
    scores[2] = 85
    print(scores[1])  # 100
    print(scores.len())  # 2
    
    # Dict literals and methods
    let prices: dict[i64, i64] = {100: 50, 200: 75}
    if prices.contains(100):
        print(prices.get(100))  # 50
    prices.insert(300, 100)
    prices.remove(100)
    
    # Dict comprehension
    let squares: dict[i64, i64] = {x: x * x for x in range(0, 5)}
    let evens: dict[i64, i64] = {x: x * 2 for x in range(0, 10) if x % 2 == 0}
    0

Python-style List Comprehensions:

fn main() -> i64:
    # Basic comprehension
    let squares: vec[i64] = [x * x for x in range(0, 10)]
    print(squares.sum())  # 285
    
    # With filter condition
    let evens: vec[i64] = [x for x in range(0, 20) if x % 2 == 0]
    print(evens.len())  # 10
    
    # Complex expressions
    let transformed: vec[i64] = [x * 2 + 1 for x in range(0, 5)]
    0

Python-style Slice Syntax:

fn main() -> i64:
    let v: vec[i64] = [x * 10 for x in range(0, 10)]
    # v = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90]
    
    # Basic slice: [start:stop] - elements from index 2 to 5 (exclusive)
    let slice1: vec[i64] = v[2:5]  # [20, 30, 40]
    
    # From start: [:stop] - first 3 elements
    let first_three: vec[i64] = v[:3]  # [0, 10, 20]
    
    # To end: [start:] - elements from index 7 onwards  
    let last_three: vec[i64] = v[7:]  # [70, 80, 90]
    
    # Full copy: [:] - copy entire vector
    let copy: vec[i64] = v[:]
    
    # With step: [::step] - every 2nd element
    let evens: vec[i64] = v[::2]  # [0, 20, 40, 60, 80]
    
    # Full form: [start:stop:step]
    let middle_evens: vec[i64] = v[2:8:2]  # [20, 40, 60]
    
    # Negative indices (from end)
    let last_two: vec[i64] = v[-2:]  # [80, 90]
    let without_last_two: vec[i64] = v[:-2]  # first 8 elements
    
    # Array slicing (returns a vec)
    let arr: [i64; 5] = [1, 2, 3, 4, 5]
    let arr_slice: vec[i64] = arr[1:4]  # [2, 3, 4]
    
    # Chain with other operations
    let sum: i64 = v[0:5].sum()  # 0 + 10 + 20 + 30 + 40 = 100
    0

Python-style Chained Comparisons:

fn main() -> i64:
    let x: i64 = 5
    
    # Chained comparisons: a < b < c is equivalent to (a < b) and (b < c)
    if 0 < x < 10:
        print(1)  # x is in range (0, 10)
    
    # Works with any comparison operators
    if 0 <= x <= 100:
        print(2)  # x is in range [0, 100]
    
    # Multiple chains
    let a: i64 = 1
    let b: i64 = 2
    let c: i64 = 3
    if a < b < c:
        print(3)  # all comparisons true
    
    # Can mix different operators
    if 0 < x <= 10:
        print(4)
    
    # Works with expressions
    if 0 < x + 1 < 20:
        print(5)
    
    0

Operator Overloading (like Rust/C++):

struct Vec2:
    x: i64
    y: i64

impl Vec2:
    # Implement + operator
    fn __add__(self: Vec2, other: Vec2) -> Vec2:
        Vec2 { x: self.x + other.x, y: self.y + other.y }
    
    # Implement - operator
    fn __sub__(self: Vec2, other: Vec2) -> Vec2:
        Vec2 { x: self.x - other.x, y: self.y - other.y }
    
    # Implement * operator (scalar multiplication)
    fn __mul__(self: Vec2, scalar: i64) -> Vec2:
        Vec2 { x: self.x * scalar, y: self.y * scalar }
    
    # Implement == operator
    fn __eq__(self: Vec2, other: Vec2) -> bool:
        self.x == other.x and self.y == other.y
    
    # Implement < operator (comparison by magnitude)
    fn __lt__(self: Vec2, other: Vec2) -> bool:
        self.x * self.x + self.y * self.y < other.x * other.x + other.y * other.y

fn main() -> i64:
    let a: Vec2 = Vec2 { x: 1, y: 2 }
    let b: Vec2 = Vec2 { x: 3, y: 4 }
    
    let c: Vec2 = a + b  # Uses __add__
    print(c.x)  # 4
    print(c.y)  # 6
    
    let d: Vec2 = b - a  # Uses __sub__
    print(d.x)  # 2
    
    let e: Vec2 = a * 3  # Uses __mul__
    print(e.x)  # 3
    
    # Chained operators work
    let f: Vec2 = a + b + c
    print(f.x)  # 8
    
    # Comparison operators
    if a == a:
        print(1)  # true
    
    if a < b:  # Compare by magnitude
        print(2)  # true
    
    0

Supported operator methods:

  • __add__ for +
  • __sub__ for -
  • __mul__ for *
  • __div__ for /
  • __mod__ for %
  • __eq__ for ==
  • __ne__ for !=
  • __lt__ for <
  • __gt__ for >
  • __le__ for <=
  • __ge__ for >=

Functional Iterator Methods:

fn main() -> i64:
    let v: vec[i64] = []
    for i in range(0, 10):
        v.push(i)
    
    # Rust-style method chaining
    let result: vec[i64] = v.into_iter().skip(2).take(5).map(|x: i64| -> i64: x * 2).filter(|x: i64| -> bool: x > 5).collect()
    print(result.sum())  # 36
    
    # Fold for reductions
    let factorial: i64 = v.take(5).fold(1, |acc: i64, x: i64| -> i64: acc * x)
    0

Rust-style Generics with Monomorphization:

# Generic struct with type parameter
struct Box<T>:
    value: T

# Generic impl block - methods on generic structs
impl Box<T>:
    fn get(self: Box<T>) -> T:
        self.value

# Generic function with type inference
fn identity<T>(x: T) -> T:
    x

fn make_box<T>(val: T) -> Box<T>:
    Box<T> { value: val }

# Generic enum (like Rust's Option)
enum Option<T>:
    Some(T)
    None

fn main() -> i64:
    # Generic struct with methods
    let b: Box<i64> = Box<i64> { value: 42 }
    print(b.get())  # 42
    
    # Generic functions with type inference (types inferred from arguments)
    print(identity(100))  # T inferred as i64
    let boxed: Box<i64> = make_box(77)  # T inferred as i64
    print(boxed.get())  # 77
    
    # Explicit type args still work
    let x: i64 = identity<i64>(42)
    
    # Generic enum with pattern matching
    let opt: Option<i64> = Option<i64>::Some(50)
    match opt:
        Option<i64>::Some(val):
            print(val)  # 50
        Option<i64>::None:
            print(0)
    
    return 0

Supported: operator overloading (__add__, __eq__, etc.), generics (structs, enums, functions, impl blocks) with type inference, type aliases, const declarations, hashmaps with dict comprehensions (dict[K,V]), list comprehensions, slice syntax (v[1:3], v[::2], negative indices), chained comparisons (0 < x < 10), pattern guards (Some(x) if x > 0:), Result[T, E] type with ? operator, functional iterators (map, filter, fold, skip, take, sum), implicit return, ownership & borrowing, enums with match, keyword args, structs with impl, tuples, arrays, vectors, closures.

Todo

  • Traits (rust)
  • Default Parameter Values (Python)
  • String Interpolation / F-strings (Python)
  • Or-Patterns in Match (Rust)
  • if let / while let (Rust)
  • Tuple Unpacking / Destructuring (Python/Rust)
  • Struct Update Syntax (Rust)
  • Wildcard Patterns (Rust)
  • Async/Await (Rust/Python)

License

MIT

About

Toy programming language

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages