Skip to content

Repository files navigation

Ratchet logo

Ratchet is a small, imperative, strongly typed language for tooling and scripting, with C-like syntax and explicit memory choices.

Experimental and under active development. Coroutines are planned.

See the language documentation, grammar, and test programs for more detail and examples.

A small program

module main; // Every source file starts with its module declaration.

// Example function
fn int clamp(int value, int low, int high) {
    if (value < low) {
        return low; 
    }
    if (value > high) {
        return high;
    }
    return value;
}

// null means the function returns no value
fn null printScore(int score) {
    if (score < 0) {
        echo("Score cannot be negative.");
        return; 
    }

    echo("Score:");
    echo(clamp(score, 0, 100)); 
}

fn bool program() {
    echo("Hello, Ratchet!");    // Print a value followed by a newline.

    int score = 120;
    int bounded = clamp(score, 0, 100); 
    echo(bounded);              // 100

    printScore(score); 
    printScore(-5);             // "Score cannot be negative."
    return false;
}

Structs and methods

module main;

struct Counter {
    int value;
    // Structs have methods with implicit "this"
    method null add(int amount) { 
        this.value = this.value + amount; 
    }
}

fn bool program() {
    Counter counter;        // A struct value, stack allocated
    counter.value = 10; 
    counter.value = counter.value + 5;
    counter.add(5); 
    echo(counter.value);    // 20
    return false;
}

Borrow parameters

Use in for a read-only borrow and inout for a mutable borrow, at both the declaration and the call.

module main;

struct Point {
    int x;
    int y;
}

// Borrow the caller's Point for mutation instead of copying it.
fn null move(inout Point point, int dx, int dy) {
    point.x = point.x + dx;
    point.y = point.y + dy;
}

// Read the caller's Point without copying it; writes through point are forbidden.
fn int sum(in Point point) {
    return point.x + point.y;
}

fn bool program() {
    Point point;
    point.x = 1;
    point.y = 2;
    move(inout point, 3, 4);    // Repeat inout at the call: point is now (4, 6).
    echo(sum(in point));        // Repeat in for a read-only borrow. Prints 10.
    return false;
}

Values and managed references

module main;

fn bool program() {
    int value = 5;                  // Values behave as you expect
    int copy = value;
    copy = 9; 
    echo(value);                    // 5

    int& shared = new int& { 5 };   // Allocate an integer managed by the GC.
    int& alias = shared;            // Copy the reference: both names refer to the same object.
    alias.value = 9;                // .value accesses the integer inside the managed object.
    echo(shared.value);             // 9
    return false;
}

Arrays

module main;

fn bool program() {
    // Inline arrays have a fixed size and copy their elements.
    int[3] local;                   // Three integers stored inline; indices are 0..2.
    local[0] = 7;
    int[3] copy = local;            // Independent storage.
    copy[0] = 42;
    echo(local[0]);                 // 7
    echo(copy[0]);                  // 42

    // Managed arrays live on the heap and can grow.
    int count = 3;
    int[]& heap = new int[count]&;  // Initial length is chosen at runtime.
    heap[0] = 7;
    int[]& alias = heap;            // Share the array instead of copying its elements.
    alias[0] = 99;
    echo(heap[0]);                  // 99

    heap.add(10);                   // Append; grow the backing storage when needed.
    echo(alias.length);             // 4: both references see the appended element.
    echo(heap[3]);                  // 10: the new element is at the old length.
    
    // Accessing heap[4] would cause a runtime bounds error.
    return false;
}

Pointers

Every value type T has both an & and a * variant, including primitives, structs, and arrays. T& is managed by the runtime; T* is manually managed by your code.

Pointer support is implemented; free() is still in development. These examples show the type forms and intended cleanup:

Type Runtime-managed Manually managed
Integer int& int*
Struct Point& Point*
Array int[]& int[]*
module main;

fn bool program() {
    int& managed = new int& { 5 };      // The runtime manages this allocation's lifetime.
    int* manual = new int* { 5 };       // Your code must release this allocation.
    int* alias = manual;                // Copy the pointer, not the allocated integer.

    int[]& managedArray = new int[3]&;  // Three integers managed by the runtime.
    int[]* manualArray = new int[3]*;   // Three integers with a manual lifetime.
    managedArray[0] = 7;                // Both forms use the same indexing syntax.
    manualArray[0] = 7;

    free(manual);                       // Still in development: release each allocation once.
    free(manualArray);                  // Manual arrays also require explicit cleanup.

    // Both manual and alias now point to freed memory and must not be used.
    return false;
}

Modules

Each file declares its module. use makes names from another module available in that file; qualified names work too.

example/numbers.ratc:

module numbers; // Declarations in this file belong to "numbers".

fn int doubleValue(int value) {
    return value * 2;
}

example/main.ratc:

module main;
use numbers; // Allow unqualified names from numbers in this file.

fn bool program() {
    echo(doubleValue(21));         // 42: found through the use statement.
    echo(numbers::doubleValue(5)); // 10: qualification also works without use.
    return false;
}

Build and run

Coming soon....