Skip to content

Latest commit

 

History

History
2668 lines (2012 loc) · 66.1 KB

File metadata and controls

2668 lines (2012 loc) · 66.1 KB

Luma Language Documentation

Luma is a statically typed, compiled programming language designed for systems programming. It combines the low-level control of C with a strong type system and modern safety features that eliminate many common runtime errors.

Luma is built on three core principles:

  • Simplicity: Minimal syntax with consistent patterns
  • Safety: Strong typing and memory safety features
  • Performance: Zero-cost abstractions and predictable performance

Quick Start

Here's a complete Luma program that demonstrates the core language features:

@module "main"

const Point -> struct {
    x: int,
    y: int,
    
    distance_to -> fn (other: Point) float {
        let dx: int = other.x - x;
        let dy: int = other.y - y;
        return sqrt(cast(dx * dx + dy * dy));
    }
};

const Status -> enum {
    Active,
    Inactive,
    Pending,
};

pub const main -> fn () int {
    let origin: Point = Point { x: 0, y: 0 };
    let destination: Point = Point { x: 3, y: 4 };
    let current_status: Status = Status::Active;
    
    outputln("Distance: ", origin.distance_to(destination));
    
    switch (current_status) {
        Status::Active -> outputln("System is running");
        Status::Inactive -> outputln("System is stopped");
        Status::Pending -> outputln("System is starting");
    }
    
    return 0;
}

This example shows:

  • Module declaration with @module
  • Struct definitions with methods
  • Enum definitions
  • Static access with :: for enum variants
  • Runtime access with . for struct members
  • Function definitions and calls
  • Switch statements with pattern matching

Type System

Luma provides a straightforward type system with both primitive and compound types.

Primitive Types

int      - Signed integer (64-bit)
float    - Floating point (32-bit)
double   - Floating point (64-bit)
bool     - Boolean (1 byte)
byte     - Single byte (1 byte)
*byte    - Character pointer / C-style string
void     - No value (used for function return types and generic pointers)

Note on String Types:

  • String literals like "hello" are of type *byte (null-terminated character arrays)
  • All string operations in the standard library use *byte
  • There is no separate str type in Luma

Type Modifiers & Operators

*T       - Pointer type (declares a pointer to type T)
[T; N]   - Array type (fixed-size array of N elements of type T)

Pointer Operators:

*expr    - Dereference operator (access value pointed to)
&expr    - Address-of operator (get pointer to value)

Example:

let x: int = 42;           // x is an int
let ptr: *int = &x;        // ptr is a pointer to int, holds address of x
let value: int = *ptr;     // value is 42 (dereferenced ptr)

Enumerations

Enums provide type-safe constants with underlying integer values:

const Direction -> enum {
    North,    // = 0
    South,    // = 1
    East,     // = 2
    West      // = 3
};

const current_direction: Direction = Direction::North;

// Can cast to int if needed
let dir_value: int = cast<int>(Direction::North);  // 0

Structures

Structures group related data with optional access control:

const Point -> struct {
    x: int,
    y: int
};

// With explicit access modifiers and methods
const Player -> struct {
pub:
    name: *byte,
    score: int,
priv:
    internal_id: int,
    
    // Methods can be defined inside structs
    get_info -> fn () void {
        outputln("Player: ", name, " Score: ", score);
    }
};

Using Types

const origin: Point = Point { x: 0, y: 0 };

const player: Player = Player { 
    name: "Alice", 
    score: 100,
    internal_id: 12345 
};

// Access fields
outputln(origin.x);           // 0
outputln(player.name);        // Alice

// Call methods
player.get_info();            // Player: Alice Score: 100

Type Compatibility

// Same types
let x: int = 42;
let y: int = x;  // OK

// Different types require explicit cast
let f: float = cast<float>(x);  // OK
let z: int = f;  // ERROR: must use cast<int>(f)

// Pointer type safety
let int_ptr: *int = &x;
let void_ptr: *void = cast<*void>(int_ptr);  // Explicit cast required

See Type Casting System for full details on conversions.


Generics (Not yet supported)

Luma supports generic programming through templates, enabling you to write code that works with multiple types while maintaining type safety and zero-cost abstractions.

Generic Functions

Generic functions are declared with type parameters in angle brackets <> after the fn keyword:

const add = fn<T>(a: T, b: T) T { 
    return a + b; 
}

const swap = fn<T>(a: *T, b: *T) void {
    let temp: T = *a;
    *a = *b;
    *b = temp;
}

const max = fn<T>(a: T, b: T) T {
    if (a > b) {
        return a;
    }
    return b;
}

Using Generic Functions

Generic functions require explicit type arguments at the call site:

const main = fn() int {
    // Integer arithmetic
    outputln("add(1, 2) = ", add<int>(1, 2));
    
    // Floating-point arithmetic
    outputln("add(1.5, 2.5) = ", add<float>(1.5, 2.5));

    // Swapping integers
    let x: int = 5; 
    let y: int = 10;
    swap<int>(&x, &y);
    outputln("After swap: x = ", x, ", y = ", y);
    
    // Finding maximum
    let largest: int = max<int>(42, 17);
    outputln("Max: ", largest);
    
    return 0;
}

Generic Structs

Structs can also be generic, allowing you to create container types and data structures that work with any type:

const Box -> struct<T> {
    value: T,
    
    get = fn() T {
        return value;
    },
    
    set = fn(new_value: T) void {
        value = new_value;
    }
};

const Pair -> struct<T, U> {
    first: T,
    second: U
};

// Usage
const main = fn() int {
    // Box holding an integer
    let int_box: Box<int> = Box { value: 42 };
    outputln("Box contains: ", int_box.get());
    
    // Box holding a float
    let float_box: Box<float> = Box { value: 3.14 };
    
    // Pair with different types
    let pair: Pair<int, *byte> = Pair { 
        first: 1, 
        second: "hello" 
    };
    outputln("Pair: (", pair.first, ", ", pair.second, ")");
    
    return 0;
}

Monomorphization

Luma uses monomorphization for generic code generation. This means:

  • The compiler generates separate machine code for each concrete type used
  • Generic code has zero runtime overhead compared to hand-written type-specific code
  • Each instantiation (e.g., add<int>, add<float>) produces its own optimized assembly
  • Similar to C++ templates and Rust generics, not Java's type erasure

Example:

const identity = fn<T>(x: T) T {
    return x;
}

// These calls generate separate functions in the compiled binary:
let a: int = identity<int>(42);        // Generates identity_int
let b: float = identity<float>(3.14);  // Generates identity_float
let c: *byte = identity<*byte>("hello"); // Generates identity_str

Top-Level Bindings with const

Luma uses the const keyword as a unified declaration mechanism for all top-level bindings. Whether you're declaring variables, functions, types, or enums, const provides a consistent syntax that enforces immutability at the binding level.

Basic Syntax

const NUM: int = 42;                                  // Immutable variable
const Direction -> enum { North, South, East, West };  // Enum definition
const Point -> struct { x: int, y: int };              // Struct definition
const Box -> struct<T> { value: T };                   // Generic struct
const add -> fn (a: int, b: int) int {                 // Function definition
    return a + b; 
};
const max = fn<T>(a: T, b: T) T {                     // Generic function
    if (a > b) { return a; }
    return b;
};

Why This Design?

Unified syntax: One parsing rule handles all top-level declarations, simplifying both the compiler and developer experience.

Semantic clarity: The binding itself is immutable—you cannot reassign or shadow a top-level const. This prevents accidental redefinition bugs.

Compiler optimization: Immutable bindings enable better optimization opportunities.

Future extensibility: This approach naturally supports compile-time metaprogramming and uniform import behavior.

Important Notes

const x: int = 5;
x = 10; // Error: `x` is immutable

const add -> fn (a: int, b: int) int { return a + b; };
add = something_else; // Error: cannot reassign function binding

Variables and Mutability

Inside functions, use let to declare local variables:

const main -> fn () int {
    let x: int = 10;        // Mutable local variable
    x = 20;                 // Can be reassigned
    
    let y: int = 5;
    y = y + 1;              // Can be modified
    
    let counter: int = 0;
    loop (counter < 10) {
        counter = counter + 1;  // Mutating in loop
    }
    
    return 0;
}

Key difference:

  • const at top-level = immutable binding (cannot reassign)
  • let in functions = mutable variable (can reassign and modify)

Functions

Functions are first-class values in Luma.

Function Declaration

// Basic function
const add -> fn (a: int, b: int) int {
    return a + b;
};

// Function with no parameters
const greet -> fn () void {
    outputln("Hello!");
};

// Function with no return value
const print_number -> fn (n: int) void {
    outputln("Number: ", n);
};

main() with Arguments

main can optionally accept command-line arguments:

// No arguments
pub const main -> fn () int {
    return 0;
}

// With argc/argv
pub const main -> fn (argc: int, argv: **byte) int {
    return 0;
}

Function Calls

const main -> fn () int {
    let result: int = add(5, 3);
    outputln("5 + 3 = ", result);
    
    greet();
    print_number(42);
    
    return 0;
}

Function Parameters

Parameters are passed by value by default:

const modify -> fn (x: int) void {
    x = 100;  // Modifies local copy only
};

const main -> fn () int {
    let num: int = 10;
    modify(num);
    outputln(num);  // Still 10
    return 0;
}

To modify the caller's variable, use pointers:

const modify_ptr -> fn (x: *int) void {
    *x = 100;  // Modifies original value
};

const main -> fn () int {
    let num: int = 10;
    modify_ptr(&num);  // Pass address
    outputln(num);     // Now 100
    return 0;
}

Return Values

// Single return value
const square -> fn (x: int) int {
    return x * x;
};

// Multiple return values via struct
const DivResult -> struct {
    quotient: int,
    remainder: int
};

const divide -> fn (a: int, b: int) DivResult {
    return DivResult {
        quotient: a / b,
        remainder: a % b
    };
};

const main -> fn () int {
    let result: DivResult = divide(17, 5);
    outputln("17 / 5 = ", result.quotient, " R ", result.remainder);
    return 0;
}

Early Returns

const find_positive -> fn (numbers: *int, size: int) int {
    loop [i: int = 0](i < size) : (++i) {
        if (numbers[i] > 0) {
            return numbers[i];  // Early return
        }
    }
    return -1;  // Not found
};

Name Resolution

Luma uses two distinct operators for name resolution to provide semantic clarity:

Static Access with ::

The :: operator is used for compile-time static access:

// Enum variants
const day: WeekDay = WeekDay::Monday;

// Module/namespace access
math::sqrt(16.0)

// Associated functions (if added later)
Point::new(10, 20)

Runtime Member Access with .

The . operator is used for runtime member access:

// Struct field access
let point: Point = Point { x: 10, y: 20 };
outputln(point.x);  // Access field at runtime

// Method calls on instances
let distance: float = origin.distance_to(destination);

// Generic struct field access
let box: Box<int> = Box { value: 42 };
outputln(box.value);

Benefits of This Distinction

  • Semantic clarity: :: means "resolved at compile time", . means "accessed at runtime"
  • Easier parsing: The compiler immediately knows the access type
  • Consistent with systems languages: Similar to C++ and Rust conventions
  • Future-proof: Supports advanced features like associated functions

Control Flow

Luma provides clean, flexible control flow constructs that handle most programming patterns without unnecessary complexity.

Conditional Statements

Use if, elif, and else for branching logic:

const x: int = 7;

if (x > 10) {
    outputln("Large number");
} elif (x > 5) {
    outputln("Medium number");  // This will execute
} else {
    outputln("Small number");
}

Loop Constructs

The loop keyword provides several iteration patterns:

For-Style Loops

// Basic for loop
loop [i: int = 0](i < 10) {
    outputln("Iteration: ", i);
    ++i;
}

// For loop with post-increment
loop [i: int = 0](i < 10) : (++i) {
    outputln("i = ", i);
}

// Multiple loop variables
loop [i: int = 0, j: int = 0](i < 10) : (++i) {
    outputln("i = ", i, ", j = ", j);
    ++j;
}

While-Style Loops

// Condition-only loop
let counter: int = 0;
loop (counter < 5) {
    outputln("Count: ", counter);
    counter = counter + 1;
}

// While loop with post-action
let j: int = 0;
loop (j < 10) : (++j) {
    outputln("Processing: ", j);
}

Infinite Loops

loop {
    // Runs forever until `break` is encountered
    if (should_exit()) {
        break;
    }
    do_work();
}

Loop Control

// Break: exit loop early
loop [i: int = 0](i < 100) : (++i) {
    if (i == 50) {
        break;  // Exit loop
    }
    process(i);
}

// Continue: skip to next iteration
loop [i: int = 0](i < 10) : (++i) {
    if (i % 2 == 0) {
        continue;  // Skip even numbers
    }
    outputln("Odd: ", i);
}

Switch Statements

Luma provides powerful pattern matching through switch statements that work with enums, integers, and other types. Switch statements must be exhaustive and all cases must be compile-time constants.

Basic Switch Syntax

@module "main"

const WeekDay -> enum {
    Sunday,
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
};

const classify_day -> fn (day: WeekDay) void {
    switch (day) {
        WeekDay::Monday, WeekDay::Tuesday, WeekDay::Wednesday, 
        WeekDay::Thursday, WeekDay::Friday ->
            outputln("Weekday => ", day);
        WeekDay::Saturday, WeekDay::Sunday ->
            outputln("Weekend => ", day);
    }
}

pub const main -> fn () int {
    classify_day(WeekDay::Monday);   // Output: Weekday => 1
    classify_day(WeekDay::Saturday); // Output: Weekend => 6
    return 0;
}

Switch with Default Case

When you need to handle unexpected values or want a catch-all case, use the default wildcard pattern _:

const handle_status_code -> fn (code: int) void {
    switch (code) {
        200 -> outputln("OK");
        404 -> outputln("Not Found");
        500 -> outputln("Internal Server Error");
        _   -> outputln("Unknown status code");
    }
};

Switch Features

  • Multiple values per case: Combine multiple values using commas
  • Exhaustiveness: All possible values must be covered (or use _ for default)
  • Compile-time constants: All case values must be compile-time constants
  • No fallthrough: Each case is automatically contained (no break needed)

Module System

Luma provides a simple module system for code organization and namespace management.

Module Declaration

Every Luma source file must declare its module name:

@module "main"

// Your code here...

Importing Modules

Use the @use directive to import other modules:

@module "main"

@use "std_math" as math
@use "std_string" as string

const main -> fn () int {
    // Access imported functions with namespace
    let result: double = math::sin(math::PI / 4.0);
    let len: int = string::strlen("hello");
    
    outputln("sin(π/4): ", result);
    outputln("Length: ", len);
    return 0;
}

Standard Library Module Names

All standard library modules use the std_ prefix:

std_math      - Mathematical functions and constants
std_memory    - Low-level memory operations
std_string    - String manipulation
std_io        - High-level I/O with formatted output
std_sys       - POSIX system calls (Linux/macOS)
std_win32     - Windows Win32 API
std_termfx    - Terminal colors and formatting
std_terminal  - Terminal input/raw mode control
std_time      - Time and timing operations
std_vector    - Dynamic array
std_arena     - Arena allocator
std_args      - Command-line argument parsing
std_libc      - C standard library bindings

Module Features

  • Explicit imports: All dependencies must be explicitly declared
  • Namespace isolation: Imported modules are accessed through their aliases
  • Static resolution: All module access is resolved at compile time using ::
  • Clean syntax: Simple @use "module" as alias pattern

Platform Directives

@os — Conditional Compilation

The @os block selects code based on the target operating system. This is how the standard library handles platform differences for things like syscall numbers and flags:

@os {
    "linux" -> {
        pub const SYS_WRITE: int = 1;
        pub const O_CREAT: int   = 64;
    }
    "macos" -> {
        pub const SYS_WRITE: int = 4;
        pub const O_CREAT: int   = 512;
    }
    "windows" -> {
        // Windows-specific declarations...
    }
}

You can also use @os inline inside function bodies:

const write_out -> fn (s: *byte) int {
    @os {
        "linux"   -> { return __syscall__(1, 1, cast<int>(s), len); }
        "macos"   -> { return __syscall__(4, 1, cast<int>(s), len); }
        "windows" -> { /* use WriteFile */ }
    }
}

Valid platform strings are "linux", "macos", and "windows".


Foreign Function Interface (FFI)

Luma can call into native shared libraries and DLLs through two complementary directives.

@link — Shared Library Linking (POSIX)

@link declares that the entire module links against a shared library. Place it at the top of your module, before any function declarations:

@module "std_libc"

@link("libc.so.6")

pub const puts    -> fn (s: *byte) int;
pub const printf  -> fn (fmt: *byte, val: *byte) int;
pub const malloc  -> fn (size: int) *void;
// ...

The function body is omitted — the linker resolves it from the named library at link time. Only one @link per module is needed; it applies to all subsequent pub const declarations that have no body.

#lib_import — Per-Function Library Override

When individual functions within a module come from a different library than the module-level @link, use #lib_import as a per-declaration attribute:

@module "std_libc"

@link("libc.so.6")

// Most functions come from libc.so.6 via @link above:
pub const malloc -> fn (size: int) *void;
pub const free   -> fn (ptr: *void) void;

// Math functions need libm — override per-function:
#lib_import("libm.so")
pub const sqrt -> fn (x: double) double;

#lib_import("libm.so")
pub const pow  -> fn (base: double, exp: double) double;

#lib_import("libm.so")
pub const sin  -> fn (x: double) double;

Place #lib_import(...) on the line immediately before the pub const it applies to.

#dll_import — Windows DLL Imports

On Windows, use #dll_import instead. It accepts the DLL name and an optional calling convention:

#dll_import("kernel32.dll", callconv: "stdcall")
pub const CreateFileA -> fn (
    lpFileName: *byte,
    dwDesiredAccess: int,
    dwShareMode: int,
    lpSecurityAttributes: *void,
    dwCreationDisposition: int,
    dwFlagsAndAttributes: int,
    hTemplateFile: int
) int;

#dll_import("kernel32.dll", callconv: "stdcall")
pub const WriteFile -> fn (
    hFile: int,
    lpBuffer: *void,
    nNumberOfBytesToWrite: int,
    lpNumberOfBytesWritten: *int,
    lpOverlapped: *void
) int;

#dll_import can also appear inside @os { "windows" -> { ... } } blocks so the same source file compiles cross-platform:

@os {
    "windows" -> {
        #dll_import("kernel32.dll", callconv: "stdcall")
        pub const GetStdHandle -> fn (nStdHandle: int) int;
    }
}

Using libc

The std_libc module wraps the C standard library (stdio, stdlib, string, and math) and is the easiest way to call into libc from Luma:

@module "main"

@use "std_libc" as c

pub const main -> fn () int {
    c::puts("hello from libc");

    let n: int = c::atoi("42");
    let r: double = c::sqrt(144.0);

    return 0;
}

Built-in Functions

Luma provides several built-in functions that are always available without imports.

Output Functions

output(...)      // Print values without newline
outputln(...)    // Print values with newline

Both functions are variadic - they accept any number of arguments of any type:

const main -> fn () int {
    output("Hello", " ", "World");           // Hello World
    outputln("The answer is:", 42);          // The answer is: 42\n
    
    let x: int = 10;
    let y: float = 3.14;
    outputln("x = ", x, ", y = ", y);       // x = 10, y = 3.14\n
    
    return 0;
}

Input Functions

input<T>(prompt: *byte) -> T    // Read typed input

The input function is generic and reads a value of the specified type:

const main -> fn () int {
    let name: *byte = input<*byte>("Enter your name: ");
    let age: int = input<int>("Enter your age: ");
    let height: double = input<double>("Enter height (meters): ");
    
    outputln("Name: ", name);
    outputln("Age: ", age);
    outputln("Height: ", height);
    
    return 0;
}

System Commands

system(command: *byte) -> int    // Execute system command

Execute shell commands from your program:

const main -> fn () int {
    system("clear");  // Clear terminal (Linux/Mac)
    system("stty -icanon -echo");  // Configure terminal
    return 0;
}

Type Information

sizeof<T> -> int    // Size of type in bytes

Get the size of any type at compile time:

const main -> fn () int {
    outputln("int: ", sizeof<int>);           // 8
    outputln("byte: ", sizeof<byte>);         // 1
    outputln("double: ", sizeof<double>);     // 8
    
    // Use in allocations
    let buffer: *int = cast<*int>(alloc(100 * sizeof<int>));
    defer free(buffer);
    
    return 0;
}

Type Casting System

Luma uses explicit casting with the cast<T>() function for all type conversions.

Basic Syntax

cast<TargetType>(expression)

Numeric Conversions

const main -> fn () int {
    // Integer to float
    let i: int = 42;
    let f: float = cast<float>(i);        // 42.0
    
    // Float to integer (truncates)
    let pi: double = 3.14159;
    let rounded: int = cast<int>(pi);     // 3
    
    // Between integer types
    let small: byte = cast<byte>(65);     // 'A'
    let large: int = cast<int>(small);    // 65
    
    return 0;
}

Pointer Casting

const main -> fn () int {
    // void* to typed pointer
    let raw: *void = alloc(sizeof<int>);
    let typed: *int = cast<*int>(raw);
    *typed = 42;
    free(raw);
    
    // Between pointer types
    let int_ptr: *int = cast<*int>(alloc(sizeof<int>));
    let void_ptr: *void = cast<*void>(int_ptr);
    free(int_ptr);
    
    return 0;
}

Pointer to Integer (and back)

const main -> fn () int {
    let ptr: *byte = cast<*byte>(alloc(10));
    defer free(ptr);
    
    // Pointer to integer
    let addr: int = cast<int>(ptr);
    
    // Add offset (pointer arithmetic)
    let offset_addr: int = addr + 5;
    
    // Back to pointer
    let offset_ptr: *byte = cast<*byte>(offset_addr);
    
    return 0;
}

Array Types

Luma supports fixed-size arrays with compile-time known sizes.

Array Declaration

// Syntax: [Type; Size]
let numbers: [int; 10];           // Array of 10 integers
let bytes: [byte; 256];           // Array of 256 bytes
let buffer: [double; 100];        // Array of 100 doubles

// Constants can be arrays too
const PRIMES: [int; 5] = [2, 3, 5, 7, 11];

Array Initialization

const main -> fn () int {
    // Uninitialized (contains garbage)
    let data: [int; 5];
    
    // Initialize with literal
    let primes: [int; 5] = [2, 3, 5, 7, 11];
    
    // Initialize element by element
    let scores: [int; 3];
    scores[0] = 95;
    scores[1] = 87;
    scores[2] = 92;
    
    return 0;
}

Array Access

const main -> fn () int {
    let numbers: [int; 5] = [10, 20, 30, 40, 50];
    
    // Read elements
    let first: int = numbers[0];    // 10
    let last: int = numbers[4];     // 50
    
    // Write elements
    numbers[2] = 99;
    
    // Loop through array
    loop [i: int = 0](i < 5) : (++i) {
        outputln("numbers[", i, "] = ", numbers[i]);
    }
    
    return 0;
}

String Literals and String Types

String Literals

String literals are null-terminated byte arrays:

const main -> fn () int {
    // String literal - type is *byte
    let message: *byte = "Hello, World!";
    outputln(message);
    
    return 0;
}

Character Literals

Single characters use single quotes:

const main -> fn () int {
    let letter: byte = 'A';           // Character literal
    let newline: byte = '\n';         // Escape sequence
    let tab: byte = '\t';             // Tab character
    
    return 0;
}

Escape Sequences

'\n'   // Newline
'\r'   // Carriage return
'\t'   // Horizontal tab
'\\'   // Backslash
'\''   // Single quote
'\"'   // Double quote
'\0'   // Null character
'\xHH' // Hexadecimal byte (e.g., '\x1b' for ESC)

Pointer Arithmetic

Luma supports pointer arithmetic for low-level memory manipulation.

Basic Pattern

const main -> fn () int {
    let arr: *int = cast<*int>(alloc(5 * sizeof<int>));
    defer free(arr);
    
    // Initialize
    loop [i: int = 0](i < 5) : (++i) {
        arr[i] = i * 10;
    }
    
    // Pointer arithmetic: convert to int, add offset, convert back
    let addr: int = cast<int>(arr);
    let new_addr: int = addr + (2 * sizeof<int>);
    let new_ptr: *int = cast<*int>(new_addr);
    
    outputln(*new_ptr);  // arr[2] = 20
    
    return 0;
}

Visibility and Access Control

Public Module Members

Use pub to export items from a module:

@module "math"

// Public - accessible from other modules
pub const PI: double = 3.14159265359;

pub const sqrt -> fn (x: double) double {
    return x;
}

// Private - only within this module
const INTERNAL_CONSTANT: int = 42;

Struct Access Control

const Person -> struct {
pub:
    name: *byte,
    age: int,
    
priv:
    ssn: *byte,
    internal_id: int
};

Memory Management

Luma provides explicit memory management with safety-oriented features.

Basic Memory Operations

alloc(size: int) -> *void    // Allocate memory
free(ptr: *void)             // Deallocate memory
sizeof<T> -> int             // Size of type

Example Usage

const main -> fn () int {
    // Allocate memory
    let ptr: *int = cast<*int>(alloc(sizeof<int>));
    
    // Use the memory
    *ptr = 42;
    outputln("Value: ", *ptr);
    
    // Clean up
    free(ptr);
    return 0;
}

The defer Statement

Ensure cleanup with defer statements that execute when leaving scope:

const process_data -> fn () void {
    let buffer: *int = cast<*int>(alloc(100 * sizeof<int>));
    defer free(buffer);  // Guaranteed to run when function exits
    
    let file: *File = open_file("data.txt");
    defer close_file(file);  // Will run even if early return
    
    // Complex processing...
    if (error_condition) {
        return; // defer statements still execute
    }
    
    // More processing...
    // defer statements execute here automatically
}

Multiple statements can be deferred:

defer {
    close_file(file);
    cleanup_resources();
    log("Operation completed");
}

Key Benefits:

  • Ensures cleanup code runs regardless of how the function exits
  • Keeps allocation and deallocation code close together
  • Prevents resource leaks from early returns
  • Executes in reverse order (LIFO - Last In, First Out)

Ownership Transfer Attributes

Luma provides function attributes that document and enforce ownership semantics.

#returns_ownership

Marks functions that allocate and return pointers:

#returns_ownership
const create_buffer -> fn (size: int) *int {
    let buffer: *int = cast<*int>(alloc(size * sizeof<int>));
    return buffer;  // Caller now owns this memory
}

const main -> fn () int {
    let data: *int = create_buffer(100);
    defer free(data);  // Caller must free
    return 0;
}

#takes_ownership

Marks functions that take ownership of pointer arguments:

#takes_ownership
const consume_buffer -> fn (buffer: *int) void {
    outputln("Processing: ", *buffer);
    free(buffer);  // Function owns and frees the buffer
}

const main -> fn () int {
    let data: *int = cast<*int>(alloc(sizeof<int>));
    *data = 42;
    
    consume_buffer(data);  // Ownership transferred
    // Note: do not use `data` after this point
    
    return 0;
}

Static Memory Analysis

Luma's compiler includes a static analyzer that tracks memory at compile time to prevent common memory management errors.

What the Analyzer Tracks

Verified at Compile Time:

  • Memory Leaks: Detects alloc() calls without corresponding free()
  • Double-Free: Prevents freeing the same pointer twice
  • Use-After-Free: Catches access to freed memory within the same function
  • Ownership Transfer: Validates #returns_ownership and #takes_ownership annotations
  • Defer Statement Cleanup: Ensures deferred frees execute properly

How It Works:

const good_memory_usage -> fn () void {
    let ptr: *int = cast<*int>(alloc(sizeof<int>));
    defer free(ptr);  // Analyzer confirms cleanup
    *ptr = 42;
}  // No leak reported

const bad_memory_usage -> fn () void {
    let ptr: *int = cast<*int>(alloc(sizeof<int>));
    *ptr = 42;
    // Compiler error: memory leak - ptr never freed
}

#returns_ownership
const create_buffer -> fn (size: int) *int {
    let buffer: *int = cast<*int>(alloc(size));
    return buffer;  // Ownership transferred to caller
}  // No leak reported - caller is responsible

const main -> fn () int {
    let data: *int = create_buffer(100);
    defer free(data);  // Caller properly handles ownership
    return 0;
}

Ownership Tracking:

The analyzer understands three ownership patterns:

  1. #returns_ownership functions: Allocations inside are NOT tracked as leaks because ownership transfers to the caller
  2. #takes_ownership functions: Parameters marked with this receive ownership and are responsible for cleanup
  3. defer statements: Deferred cleanup is tracked and validated at function exit

Transitive Ownership:

#returns_ownership
const create_arena_sized -> fn (size: int) Arena {
    let a: Arena;
    a.buf = alloc(size);  // Not tracked - inside #returns_ownership
    return a;
}

const create_arena -> fn () Arena {
    return create_arena_sized(1024);
    // Warning: Should add #returns_ownership annotation
    // for API clarity (ownership is being passed through)
}

Current Limitations

Known Edge Cases:

The analyzer currently has limitations in these areas:

  1. Struct Field Granularity: When tracking a.buf = alloc(...), the analyzer tracks the entire struct a, not the specific field a.buf. This works for single-pointer structs but may cause issues with:

    const Container -> struct {
        data1: *int,
        data2: *int
    };
    
    let c: Container;
    c.data1 = alloc(10);  // Tracked as "c"
    c.data2 = alloc(20);  // Also tracked as "c" - potential confusion
    free(c.data1);        // Marks "c" as freed, but c.data2 still allocated
    
  2. Conditional Allocations: The analyzer may report false positives for conditional paths:

    let ptr: *int;
    if (condition) {
        ptr = alloc(sizeof<int>);
    }
    // May warn even if you don't need to free in else branch
    
  3. Allocations in Loops: Each loop iteration's allocations should be independent, but edge cases may exist:

    loop [i: int = 0](i < 10) : (++i) {
        let temp: *int = alloc(4);
        // Use temp...
        free(temp);  // Should work correctly
    }
    
  4. Early Returns with Defer: While generally working, complex control flow with multiple early returns may need testing:

    const process -> fn () int {
        let a: *int = alloc(sizeof<int>);
        defer free(a);
        
        if (error) { return -1; }  // Defer should fire
        if (warning) { return 0; } // Defer should fire
        return 1;                  // Defer should fire
    }
    
  5. Stack vs Heap: The analyzer doesn't currently detect returning pointers to stack variables:

    const dangerous -> fn () *int {
        let local: int = 42;
        return &local;  // NOT DETECTED - returns dangling pointer
    }
    
  6. Arrays of Pointers: Complex allocation patterns may not be fully tracked:

    let arr: [*int; 5];
    loop [i: int = 0](i < 5) : (++i) {
        arr[i] = alloc(sizeof<int>);  // Each needs individual free
    }
    

Best Practices

To work effectively with the analyzer:

  1. Use ownership annotations consistently: Mark all functions that allocate and return resources with #returns_ownership
  2. Use defer for cleanup: Always pair allocations with defer free() for automatic cleanup
  3. One allocation per variable: Avoid reassigning pointer variables after allocation without freeing
  4. Clear ownership semantics: Document which functions own their pointer parameters vs. borrowing them
  5. Test early returns: Ensure defer statements properly handle all exit paths

The analyzer is conservative - it may report false positives to prevent missed leaks. When in doubt, it will warn about potential issues rather than silently allowing them.

Performance

Understanding performance is crucial for systems programming.

Zero-Cost Abstractions

Luma follows the "zero-cost abstraction" principle: abstractions should have no runtime overhead.

Generics are zero-cost:

const add = fn<T>(a: T, b: T) T {
    return a + b;
}

// These calls compile to separate, optimized functions:
let x: int = add<int>(1, 2);        // Same as: x = 1 + 2
let y: float = add<float>(1.0, 2.0); // Same as: y = 1.0 + 2.0

No runtime dispatch - all generic instantiations are resolved at compile time through monomorphization.

Monomorphization

Luma generates specialized code for each type:

const max = fn<T>(a: T, b: T) T {
    if (a > b) { return a; }
    return b;
}

// Compiler generates:
// max_int(a: int, b: int) -> int { ... }
// max_float(a: float, b: float) -> float { ... }

Benefits:

  • No runtime overhead
  • Full optimization per type
  • No vtables or dynamic dispatch

Trade-offs:

  • Larger binary size (one copy per type)
  • Longer compilation time

Memory Layout

Struct layout is predictable:

const Point -> struct {
    x: int,    // Offset 0, 8 bytes
    y: int     // Offset 8, 8 bytes
};  // Total: 16 bytes

Array layout is contiguous:

let arr: [int; 10];  // 80 contiguous bytes
// arr[0] at offset 0, arr[1] at offset 8, arr[2] at offset 16...

Memory Allocation Performance

Stack allocation is fast:

const fast_function -> fn () void {
    let buffer: [int; 1024];  // Stack allocated - instant
    // Use buffer...
}  // Automatically cleaned up

Heap allocation has overhead:

const slower_function -> fn () void {
    let buffer: *int = cast<*int>(alloc(1024 * sizeof<int>));
    defer free(buffer);
    // Use buffer...
}

Optimization Guidelines

1. Prefer stack allocation when possible:

let temp: [int; 100];  // Good for small, fixed-size data

2. Minimize pointer indirection:

// Better: direct access
let ptr: *int;
let value: int = *ptr;   // One memory load

// Best: value directly
let value2: int = 42;     // No memory load

3. Batch operations:

// Good: one large allocation
let buffer: *int = cast<*int>(alloc(1000 * sizeof<int>));
loop [i: int = 0](i < 1000) : (++i) {
    // Use buffer[i]...
}
free(buffer);

4. Avoid unnecessary copying:

// Bad: pass large struct by value
const process -> fn (data: LargeStruct) void { }

// Good: pass by pointer
const process_fast -> fn (data: *LargeStruct) void { }

Performance Summary

Operation Cost Notes
Stack variable ~0 Instant
Heap allocation High System call
Pointer dereference Low One memory access
Array index Low Bounds check + access
Function call Low-Medium Depends on size
Generic instantiation 0 Compile-time only
Struct field access Low Offset calculation
Enum comparison ~0 Integer comparison

Standard Library

Luma's standard library provides essential functionality for systems programming. All modules use the std_ prefix in their @module declaration.


Module: std_math

Mathematical operations and constants.

@use "std_math" as math

// Constants
math::PI           // 3.14159...
math::TWO_PI       // 6.28318...
math::HALF_PI      // 1.57079...

// Basic arithmetic functions
math::add(x: int, y: int) -> int
math::subtract(x: int, y: int) -> int
math::multiply(x: int, y: int) -> int
math::divide(x: int, y: int) -> int       // Returns 0 on division by zero
math::mod(x: int, y: int) -> int          // Returns 0 on division by zero

// Min/Max
math::max_size(a: int, b: int) -> int
math::min_size(a: int, b: int) -> int

// Power
math::power(base: double, exponent: int) -> double

// Trigonometry (uses lookup table for performance)
math::sin(angle: double) -> double        // Angle in radians
math::cos(x: double) -> double
math::tan(x: double) -> double
math::sec(x: double) -> double
math::csc(x: double) -> double
math::cot(x: double) -> double

// Other
math::fib(n: int, a: int, b: int) -> int  // Fibonacci
math::rand(seed: *int) -> int             // Simple PRNG

Example:

@use "std_math" as math

const main -> fn () int {
    let angle: double = math::PI / 4.0;
    let sine: double = math::sin(angle);
    outputln("sin(π/4) = ", sine);
    
    let result: double = math::power(2.0, 10);
    outputln("2^10 = ", result);
    
    return 0;
}

Module: std_memory

Low-level memory operations.

@use "std_memory" as mem

// Basic operations
mem::memcpy(dest: *void, src: *void, n: int) -> *void       // Copy memory
mem::memcmp(a: *void, b: *void, n: int) -> int              // Compare memory (returns 0 if equal)
mem::memset(dest: *void, value: int, n: int) -> *void       // Fill memory
mem::memmove(dest: *void, src: *void, n: int) -> *void      // Move (handles overlap)
mem::memzero(dest: *void, n: int) -> *void                  // Zero memory

// Search
mem::memchr(ptr: *void, value: int, n: int) -> *void        // Find byte
mem::memmem(haystack: *void, haystack_len: int, 
            needle: *void, needle_len: int) -> *void        // Find substring

// Allocation helpers
mem::calloc(count: int, size: int) -> *void                 // Allocate + zero (#returns_ownership)
mem::realloc(ptr: *void, old_size: int, new_size: int) -> *void  // Reallocate (#returns_ownership)

// Utilities
mem::memswap(a: *void, b: *void, n: int) -> void            // Swap regions
mem::memswapn(a: *void, b: *void, count: int, size: int) -> void  // Swap n elements
mem::memfill(dest: *void, value: int, size: int, count: int) -> *void  // Fill with pattern
mem::memrev(ptr: *void, n: int) -> *void                    // Reverse bytes
mem::memcount(ptr: *void, value: int, n: int) -> int        // Count occurrences
mem::memdup(src: *void, n: int) -> *void                    // Duplicate region (#returns_ownership)
mem::memeq(a: *void, b: *void, n: int) -> bool              // Check equality
mem::align(member_alignments: *int, count: int) -> int      // Calculate alignment

Note: mem::realloc takes three arguments — ptr, old_size, and new_size — unlike C's realloc. The old size is required because Luma does not have an allocator header that tracks it implicitly.

Example:

@use "std_memory" as mem

const main -> fn () int {
    let buffer: *void = mem::calloc(10, sizeof<byte>);
    defer free(buffer);
    
    mem::memset(buffer, 65, 10);  // Fill with 'A'
    outputln("Buffer filled");
    
    return 0;
}

Module: std_string

String manipulation functions.

@use "std_string" as string

// Creation (#returns_ownership)
string::from_byte(c: byte) -> *byte                         // Create string from byte
string::from_int(n: int) -> *byte                           // Convert int to string
string::from_float(f: float, precision: int) -> *byte       // Convert float to string

// Measurement
string::strlen(s: *byte) -> int                             // Get length

// Comparison
string::strcmp(s1: *byte, s2: *byte) -> int                 // Compare strings (returns 0 if equal)

// Search
string::s_byte(s: *byte, c: int) -> byte                    // Find character (returns '\0' if not found)

// Manipulation
string::copy(dest: *byte, src: *byte) -> *byte              // Copy string
string::n_copy(dest: *byte, src: *byte, n: int) -> *byte    // Copy n characters
string::cat(dest: *byte, s1: *byte, s2: *byte) -> *byte     // Concatenate

// Conversion
string::atio(value: *byte) -> int                           // ASCII to integer
string::int_to_str(num: int, buf: *byte, buf_size: int) -> void  // Int to string (in-place)

// Character classification
string::is_digit(ch: byte) -> bool                          // Check if digit
string::is_alpha(ch: byte) -> bool                          // Check if alphabetic
string::is_alnum(ch: byte) -> bool                          // Check if alphanumeric

// Output
string::putbyte(c: byte) -> int                             // Output single character

// Large number arithmetic (#returns_ownership)
string::string_add(num1: *byte, num2: *byte) -> *byte       // Add two numbers as strings

Example:

@use "std_string" as string

const main -> fn () int {
    let name: *byte = "Alice";
    let len: int = string::strlen(name);
    outputln("Length: ", len);
    
    let num_str: *byte = string::from_int(42);
    defer free(num_str);
    outputln("Number: ", num_str);
    
    return 0;
}

Module: std_sys

POSIX system call interface (Linux and macOS).

Platform Warning: This module works on Linux x86_64 and macOS x86_64/ARM64. It does NOT work on Windows. Use @os blocks or std_win32 for Windows support. Note that direct syscalls on macOS are officially unsupported by Apple and may break across OS updates; prefer std_libc for production macOS code.

All syscall numbers are defined inside @os { "linux" -> { ... } "macos" -> { ... } } blocks so the same source compiles on both platforms.

@use "std_sys" as sys

// File descriptors
sys::STDIN, sys::STDOUT, sys::STDERR

// O_ flags (values differ between Linux and macOS — use the constants)
sys::O_RDONLY, sys::O_WRONLY, sys::O_RDWR
sys::O_CREAT, sys::O_TRUNC, sys::O_APPEND, sys::O_NONBLOCK
sys::O_EXCL, sys::O_CLOEXEC, sys::O_DIRECTORY

// File operations
sys::read(fd: int, buf: *void, count: int) -> int
sys::write(fd: int, buf: *void, count: int) -> int
sys::open(path: *byte, flags: int, mode: int) -> int
sys::close(fd: int) -> int
sys::lseek(fd: int, offset: int, whence: int) -> int
sys::pread(fd: int, buf: *void, count: int, offset: int) -> int
sys::pwrite(fd: int, buf: *void, count: int, offset: int) -> int
sys::unlink(path: *byte) -> int
sys::dup(oldfd: int) -> int
sys::dup2(oldfd: int, newfd: int) -> int
sys::pipe(pipefd: *int) -> int

// Directory operations
sys::mkdir(path: *byte, mode: int) -> int
sys::rmdir(path: *byte) -> int
sys::chdir(path: *byte) -> int
sys::getcwd(buf: *byte, size: int) -> *byte

// Process management
sys::exit(code: int) -> void
sys::fork() -> int
sys::getpid() -> int
sys::getuid() -> int
sys::getgid() -> int
sys::kill(pid: int, sig: int) -> int
sys::wait4(pid: int, status: *int, options: int, rusage: *void) -> int
sys::execve(path: *byte, argv: **byte, envp: **byte) -> int

// Memory management
sys::brk(addr: *void) -> int
sys::mmap(addr: *void, length: int, prot: int, flags: int, fd: int, offset: int) -> *void
sys::munmap(addr: *void, length: int) -> int

// Helper functions
sys::is_error(result: int) -> bool
sys::get_errno(result: int) -> int
sys::write_str(fd: int, s: *byte) -> int
sys::eprint(s: *byte) -> int

// File permissions
sys::S_IRUSR, sys::S_IWUSR, sys::S_IXUSR
sys::S_IRGRP, sys::S_IWGRP, sys::S_IXGRP
sys::S_IROTH, sys::S_IWOTH, sys::S_IXOTH
sys::MODE_0644, sys::MODE_0755, sys::MODE_0777

// lseek whence
sys::SEEK_SET, sys::SEEK_CUR, sys::SEEK_END

// mmap
sys::PROT_READ, sys::PROT_WRITE, sys::PROT_EXEC, sys::PROT_NONE
sys::MAP_SHARED, sys::MAP_PRIVATE, sys::MAP_ANONYMOUS, sys::MAP_ANON

// Signals
sys::SIGHUP, sys::SIGINT, sys::SIGTERM, sys::SIGKILL, sys::SIGSEGV
// (SIGBUS and SIGCHLD have different numbers on Linux vs macOS — use the constants)

// errno constants
sys::EPERM, sys::ENOENT, sys::EACCES, sys::EINVAL, sys::ENOMEM // etc.

// Wait flags
sys::WNOHANG, sys::WUNTRACED

Example:

@use "std_sys" as sys

const main -> fn () int {
    let fd: int = sys::open("test.txt", sys::O_WRONLY | sys::O_CREAT, sys::MODE_0644);
    if (sys::is_error(fd)) {
        sys::eprint("Failed to open file\n");
        return 1;
    }
    defer sys::close(fd);
    
    sys::write_str(fd, "Hello, World!\n");
    return 0;
}

Module: std_win32

Windows Win32 API interface. This is the Windows equivalent of std_sys — use @os blocks to select between them in cross-platform code.

All functions are imported from kernel32.dll (and similar DLLs) via #dll_import with the "stdcall" calling convention.

@use "std_win32" as win32

// Handle constants
win32::INVALID_HANDLE_VALUE
win32::STD_INPUT_HANDLE, win32::STD_OUTPUT_HANDLE, win32::STD_ERROR_HANDLE

// File access flags
win32::GENERIC_READ, win32::GENERIC_WRITE, win32::GENERIC_EXECUTE

// Share flags
win32::FILE_SHARE_NONE, win32::FILE_SHARE_READ, win32::FILE_SHARE_WRITE

// Creation disposition
win32::CREATE_NEW, win32::CREATE_ALWAYS, win32::OPEN_EXISTING, win32::OPEN_ALWAYS

// File attributes
win32::FILE_ATTRIBUTE_NORMAL, win32::FILE_ATTRIBUTE_READONLY, win32::FILE_ATTRIBUTE_DIRECTORY

// Seek
win32::FILE_BEGIN, win32::FILE_CURRENT, win32::FILE_END

// Memory protection
win32::PAGE_READWRITE, win32::PAGE_READONLY, win32::PAGE_EXECUTE_READWRITE
win32::MEM_COMMIT, win32::MEM_RESERVE, win32::MEM_RELEASE

// Console
win32::CTRL_C_EVENT, win32::CTRL_BREAK_EVENT

// --- Kernel32 functions ---

// File I/O
win32::CreateFileA(lpFileName, dwDesiredAccess, dwShareMode,
                   lpSecurityAttributes, dwCreationDisposition,
                   dwFlagsAndAttributes, hTemplateFile) -> int
win32::ReadFile(hFile, lpBuffer, nNumberOfBytesToRead,
                lpNumberOfBytesRead: *int, lpOverlapped) -> int
win32::WriteFile(hFile, lpBuffer, nNumberOfBytesToWrite,
                 lpNumberOfBytesWritten: *int, lpOverlapped) -> int
win32::CloseHandle(hObject: int) -> int
win32::GetFileSizeEx(hFile: int, lpFileSize: *int) -> int
win32::SetFilePointerEx(hFile, liDistanceToMove, lpNewFilePointer: *int, dwMoveMethod) -> int
win32::DeleteFileA(lpFileName: *byte) -> int
win32::MoveFileA(lpExistingFileName: *byte, lpNewFileName: *byte) -> int
win32::CopyFileA(lpExistingFileName, lpNewFileName, bFailIfExists: int) -> int
win32::CreateDirectoryA(lpPathName: *byte, lpSecurityAttributes: *void) -> int
win32::RemoveDirectoryA(lpPathName: *byte) -> int
win32::GetCurrentDirectoryA(nBufferLength: int, lpBuffer: *byte) -> int
win32::SetCurrentDirectoryA(lpPathName: *byte) -> int

// Console / Stdio
win32::GetStdHandle(nStdHandle: int) -> int
win32::SetConsoleOutputCP(wCodePageID: int) -> int

// Process
win32::ExitProcess(uExitCode: int) -> void
win32::GetCurrentProcessId() -> int
win32::OpenProcess(dwDesiredAccess, bInheritHandle, dwProcessId: int) -> int
win32::TerminateProcess(hProcess: int, uExitCode: int) -> int
win32::GetExitCodeProcess(hProcess: int, lpExitCode: *int) -> int
win32::CreateProcessA(...) -> int
win32::WaitForSingleObject(hHandle: int, dwMilliseconds: int) -> int

// Memory
win32::VirtualAlloc(lpAddress: *void, dwSize, flAllocationType, flProtect: int) -> *void
win32::VirtualFree(lpAddress: *void, dwSize, dwFreeType: int) -> int
win32::GetProcessHeap() -> int
win32::HeapAlloc(hHeap, dwFlags, dwBytes: int) -> *void
win32::HeapFree(hHeap, dwFlags: int, lpMem: *void) -> int

// Error handling
win32::GetLastError() -> int
win32::SetLastError(dwErrCode: int) -> void

// Time / Sleep
win32::GetTickCount64() -> int
win32::Sleep(dwMilliseconds: int) -> void

// Helper functions (implemented in Luma)
win32::is_invalid_handle(handle: int) -> bool
win32::win32_failed(result: int) -> bool
win32::write_str(handle: int, s: *byte) -> int
win32::eprint(s: *byte) -> int
win32::print_str(s: *byte) -> int
win32::open_read(path: *byte) -> int
win32::open_write(path: *byte) -> int
win32::open_rdwr(path: *byte) -> int
win32::file_size(handle: int) -> int

Example:

@use "std_win32" as win32

const main -> fn () int {
    let handle: int = win32::open_write("output.txt");
    if (win32::is_invalid_handle(handle)) {
        win32::eprint("Failed to open file\n");
        return 1;
    }
    defer win32::CloseHandle(handle);
    
    win32::write_str(handle, "Hello from Windows!\n");
    return 0;
}

Module: std_io

High-level, cross-platform I/O with formatted output. Works on Linux, macOS, and Windows — internally uses @os blocks to dispatch to the correct syscalls or Win32 calls.

@use "std_io" as io

// Formatted types
io::FormatArg -> struct { tag, str_ptr, int_val, byte_val }
io::NULL_FORMAT_ARG

// Helpers to build FormatArg values
io::str_arg(arg: *byte) -> FormatArg
io::int_arg(arg: int) -> FormatArg
io::byte_arg(arg: byte) -> FormatArg

// Formatted output (supports %d, %s, %c)
io::print(s: *byte, args: [FormatArg; 256]) -> int    // Print to stdout
io::print_err(s: *byte, args: [int; 256]) -> int      // Print integers to stderr with %d

// File operations (#returns_ownership)
io::read_file(path: *byte) -> *byte                   // Read entire file into string
io::write_buffer_to_file(path: *byte, buffer: *byte) -> int  // Write string to file

Example:

@use "std_io" as io

const main -> fn () int {
    io::print("The answer is %d\n", [io::int_arg(42), io::NULL_FORMAT_ARG]);
    io::print("Hello, %s!\n", [io::str_arg("World"), io::NULL_FORMAT_ARG]);
    
    let content: *byte = io::read_file("data.txt");
    if (content != cast<*byte>(0)) {
        defer free(content);
        outputln(content);
    }
    
    return 0;
}

Module: std_termfx

Terminal formatting and colors (ANSI escape codes).

@use "std_termfx" as fx

// Basic colors
fx::RED, fx::GREEN, fx::BLUE, fx::YELLOW
fx::MAGENTA, fx::CYAN, fx::WHITE, fx::BLACK

// Bright colors
fx::BRIGHT_RED, fx::BRIGHT_GREEN, fx::BRIGHT_BLUE, fx::BRIGHT_YELLOW
fx::BRIGHT_MAGENTA, fx::BRIGHT_CYAN, fx::BRIGHT_WHITE, fx::BRIGHT_BLACK

// Background colors
fx::BG_RED, fx::BG_GREEN, fx::BG_BLUE, fx::BG_YELLOW
fx::BG_MAGENTA, fx::BG_CYAN, fx::BG_WHITE, fx::BG_BLACK
fx::BG_BRIGHT_RED, fx::BG_BRIGHT_GREEN, fx::BG_BRIGHT_BLUE
fx::BG_BRIGHT_YELLOW, fx::BG_BRIGHT_MAGENTA, fx::BG_BRIGHT_CYAN
fx::BG_BRIGHT_WHITE, fx::BG_BRIGHT_BLACK

// Text styles
fx::BOLD, fx::DIM, fx::ITALIC, fx::UNDERLINE
fx::BLINK, fx::INVERT, fx::HIDDEN, fx::STRIKETHROUGH

// Screen control
fx::CLEAR_SCREEN, fx::CLEAR_LINE, fx::CLEAR_TO_EOL, fx::CLEAR_TO_EOS
fx::CURSOR_HOME, fx::CURSOR_HIDE, fx::CURSOR_SHOW
fx::SAVE_CURSOR, fx::RESTORE_CURSOR

// Functions (#returns_ownership)
fx::fg_rgb(r: int, g: int, b: int) -> *byte          // Custom foreground color
fx::bg_rgb(r: int, g: int, b: int) -> *byte          // Custom background color
fx::move_cursor(row: int, col: int) -> *byte         // Move cursor (1-based)

// Reset
fx::RESET

Example:

@use "std_termfx" as fx

const main -> fn () int {
    output(fx::CLEAR_SCREEN, fx::CURSOR_HOME);
    output(fx::BOLD, fx::RED, "ERROR: ", fx::RESET);
    outputln("Something went wrong!");
    output(fx::GREEN, "✓ Success", fx::RESET, "\n");
    
    let custom_color: *byte = fx::fg_rgb(255, 128, 0);
    defer free(custom_color);
    output(custom_color, "Orange text!", fx::RESET, "\n");
    
    return 0;
}

Module: std_terminal

Interactive terminal input and raw mode control. Uses shell commands (stty) internally so it works wherever stty is available (Linux and macOS).

@use "std_terminal" as term

// Raw mode control
term::enable_raw_mode() -> void                      // Enable raw mode
term::disable_raw_mode() -> void                     // Restore normal mode

// Character input
term::getch() -> byte                                // Get single character (no enter required)
term::getch_raw() -> byte                            // Get character in raw mode (enables raw if needed)
term::getch_silent() -> byte                         // Get character silently (no echo)
term::getche() -> byte                               // Get character with echo

// Input utilities
term::kbhit() -> int                                 // Check if key pressed (non-blocking)
term::wait_for_key() -> void                         // Wait for any key
term::clear_input_buffer() -> void                   // Clear input buffer
term::get_line(prompt: *byte, buffer: *byte, size: int) -> void  // Read line of input

// Password input (#returns_ownership)
term::getpass(prompt: *byte) -> *byte                // Get password (shows * per character)

// Timing
term::sleep_ms(ms: int) -> void                      // Sleep for milliseconds

Example:

@use "std_terminal" as term
@use "std_string" as string

const main -> fn () int {
    outputln("Press any key...");
    let key: byte = term::getch();
    
    let key_str: *byte = string::from_byte(key);
    defer free(key_str);
    outputln("You pressed: ", key_str);
    
    let password: *byte = term::getpass("Enter password: ");
    defer free(password);
    outputln("Password entered");
    
    return 0;
}

Module: std_time

Time and timing operations using clock_gettime/nanosleep syscalls.

Platform note: std_time currently uses hardcoded Linux syscall numbers (NANOSLEEP = 35, CLOCK_GETTIME = 228). These differ on macOS — for cross-platform time, prefer std_libc (c::usleep, etc.) or check the platform with @os.

@use "std_time" as time

// Types (declared const, not pub — access via time:: prefix)
time::TimeSpec -> struct { sec: int, nsec: int }
time::Timer -> struct { start_time: TimeSpec }

// Constants
time::CLOCK_REALTIME    // = 0

// Time operations
time::now() -> TimeSpec                              // Get current time
time::clock_gettime(clk_id: int, ts: *TimeSpec) -> int
time::usleep(usec: int) -> int                       // Sleep microseconds

// Conversions
time::to_millis(t: TimeSpec) -> int
time::to_micros(t: TimeSpec) -> int
time::to_nanos(t: TimeSpec) -> int

// Timer operations
time::timer_start() -> Timer                         // Start timer
time::timer_elapsed_ms(t: Timer) -> int              // Get elapsed milliseconds
time::elapsed_ms(start: TimeSpec, end: TimeSpec) -> int
time::timespec_sub(a: TimeSpec, b: TimeSpec) -> TimeSpec

Example:

@use "std_time" as time

const main -> fn () int {
    let timer: time::Timer = time::timer_start();
    
    // Do some work...
    time::usleep(1000000);  // Sleep 1 second
    
    let elapsed: int = time::timer_elapsed_ms(timer);
    outputln("Elapsed: ", elapsed, " ms");
    
    return 0;
}

Module: std_vector

Dynamic array implementation. Automatically grows when capacity is exceeded.

@use "std_vector" as vector

// Types
vector::Vector -> struct {
    data: *void,
    capacity: int,
    size: int,
    element_size: int,
    
    // Methods
    push_back(elem: *void) -> void
    pop_back(out: *void) -> int
    insert(elem: *void, index: int) -> int
    remove_at(index: int) -> int
    get(index: int) -> *void
    iter() -> VectorIter
}

vector::VectorIter -> struct {
    // Methods
    next() -> *void    // Returns NULL when exhausted
}

// Creation (#returns_ownership)
vector::create_vector(element_size: int) -> Vector
vector::create_vector_capacity(init_capacity: int, element_size: int) -> Vector

// Cleanup (#takes_ownership)
vector::free_vector(v: *Vector) -> void

Example:

@use "std_vector" as vector

const main -> fn () int {
    let v: vector::Vector = vector::create_vector(sizeof<int>);
    defer vector::free_vector(&v);
    
    loop [i: int = 0](i < 10) : (++i) {
        v.push_back(cast<*void>(&i));
    }
    
    let val: int;
    v.pop_back(cast<*void>(&val));
    outputln("Popped: ", val);
    
    return 0;
}

Module: std_arena

Arena allocator for fast bulk allocations with a single free.

@use "std_arena" as arena

// Type
arena::Arena -> struct {
    buf: *byte,
    buf_len: int,
    prev_offset: int,
    curr_offset: int,
}

// Constants
arena::ARENA_DEFAULT_SIZE  // 1 MB (1024 * 1024)

// Creation (#returns_ownership)
arena::create_arena() -> Arena
arena::create_arena_sized(size: int) -> Arena

// Operations
arena::alloc_arena(a: *Arena, size: int) -> *void   // Returns NULL if out of space
arena::reset_arena(a: *Arena) -> void

// Cleanup (#takes_ownership)
arena::free_arena(a: *Arena) -> void

Example:

@use "std_arena" as arena

const main -> fn () int {
    let a: arena::Arena = arena::create_arena();
    defer arena::free_arena(&a);
    
    // Allocate many small objects quickly
    loop [i: int = 0](i < 1000) : (++i) {
        let ptr: *int = cast<*int>(arena::alloc_arena(&a, sizeof<int>));
        *ptr = i;
    }
    
    // Reset for reuse (doesn't free the underlying buffer)
    arena::reset_arena(&a);
    
    return 0;
}

Module: std_args

Command-line argument parsing and iteration.

@use "std_args" as args

// Types
args::Args -> struct {
    data: **byte,
    count: int,
    
    // Methods
    len() -> int
    is_empty() -> int           // Returns -1 (true) or 0 (false)
    get(index: int) -> *byte    // "" if out of bounds
    contains(val: *byte) -> int // 1 if found, 0 otherwise
    tail() -> Args              // Args excluding first element
    iter() -> ArgsIter
}

args::ArgsIter -> struct {
    data: **byte,
    remaining: int,
    
    // Methods
    peek() -> *byte    // Look without consuming; NULL if exhausted
    next() -> *byte    // Consume and return; NULL if exhausted
    skip(n: int) -> void
}

// Initialization
args::init_args(argc: int, argv: **byte) -> Args

Example:

@use "std_args" as args

pub const main -> fn (argc: int, argv: **byte) int {
    let a: args::Args = args::init_args(argc, argv);
    
    if (a.contains("--help")) {
        outputln("Usage: program [options]");
        return 0;
    }
    
    let iter: args::ArgsIter = a.iter();
    iter.skip(1);  // Skip program name
    loop {
        let arg: *byte = iter.next();
        if (arg == cast<*byte>(0)) { break; }
        outputln("Arg: ", arg);
    }
    
    return 0;
}

Module: std_libc

Direct bindings to the C standard library. Useful when you need libc functions directly, or when writing code that must work across Linux, macOS, and Windows (since libc is universally available).

Internally uses @link("libc.so.6") (POSIX) and #lib_import("libm.so") for math functions.

@use "std_libc" as c

// stdio.h
c::printf(fmt: *byte, val: *byte) -> int
c::puts(s: *byte) -> int
c::putchar(c: int) -> int
c::getchar() -> int
c::fflush(stream: *void) -> int
c::fopen(path: *byte, mode: *byte) -> *void
c::fclose(stream: *void) -> int
c::fread(ptr: *void, size: int, count: int, stream: *void) -> int
c::fwrite(ptr: *void, size: int, count: int, stream: *void) -> int
c::fgets(buf: *byte, n: int, stream: *void) -> *byte
c::fputs(s: *byte, stream: *void) -> int
c::feof(stream: *void) -> int
c::ferror(stream: *void) -> int
c::rewind(stream: *void) -> void
c::fseek(stream: *void, offset: int, whence: int) -> int
c::ftell(stream: *void) -> int
c::remove(path: *byte) -> int
c::rename(old: *byte, new: *byte) -> int

// stdlib.h
c::malloc(size: int) -> *void
c::calloc(count: int, size: int) -> *void
c::realloc(ptr: *void, size: int) -> *void
c::free(ptr: *void) -> void
c::exit(code: int) -> void
c::abort() -> void
c::abs(n: int) -> int
c::atoi(s: *byte) -> int
c::atof(s: *byte) -> double
c::atol(s: *byte) -> int
c::rand() -> int
c::srand(seed: int) -> void
c::getenv(name: *byte) -> *byte
c::system(cmd: *byte) -> int
c::qsort(base: *void, count: int, size: int, cmp: *void) -> void
c::bsearch(key: *void, base: *void, count: int, size: int, cmp: *void) -> *void

// string.h
c::strlen(s: *byte) -> int
c::strcpy(dst: *byte, src: *byte) -> *byte
c::strncpy(dst: *byte, src: *byte, n: int) -> *byte
c::strcat(dst: *byte, src: *byte) -> *byte
c::strcmp(a: *byte, b: *byte) -> int
c::strncmp(a: *byte, b: *byte, n: int) -> int
c::strchr(s: *byte, c: int) -> *byte
c::strstr(haystack: *byte, needle: *byte) -> *byte
c::strtok(s: *byte, delim: *byte) -> *byte
c::memset(ptr: *void, val: int, n: int) -> *void
c::memcpy(dst: *void, src: *void, n: int) -> *void
c::memmove(dst: *void, src: *void, n: int) -> *void
c::memcmp(a: *void, b: *void, n: int) -> int

// math.h (via #lib_import("libm.so"))
c::sqrt(x: double) -> double
c::pow(base: double, exp: double) -> double
c::floor(x: double) -> double
c::ceil(x: double) -> double
c::fabs(x: double) -> double
c::fmod(x: double, y: double) -> double
c::log(x: double) -> double
c::log2(x: double) -> double
c::log10(x: double) -> double
c::exp(x: double) -> double
c::sin(x: double) -> double
c::cos(x: double) -> double
c::tan(x: double) -> double
c::atan2(y: double, x: double) -> double

Example:

@use "std_libc" as c

pub const main -> fn () int {
    c::puts("hello from libc");
    
    let n: int = c::atoi("42");
    let r: double = c::sqrt(144.0);
    outputln("sqrt(144) = ", r);
    
    return 0;
}

Creating Your Own Modules

Module structure:

// File: mymodule.lx
@module "mymodule"

// Private helper
const helper -> fn (x: int) int {
    return x * 2;
}

// Public function
pub const process -> fn (data: *int, size: int) void {
    loop [i: int = 0](i < size) : (++i) {
        data[i] = helper(data[i]);
    }
}

// Public constant
pub const VERSION: int = 1;

Using your module:

// File: main.lx
@module "main"
@use "mymodule" as mm

const main -> fn () int {
    let data: [int; 5] = [1, 2, 3, 4, 5];
    mm::process(cast<*int>(&data), 5);
    outputln("Version: ", mm::VERSION);
    return 0;
}

Safety Features

Luma provides several safety features to prevent common bugs:

1. Static Memory Analysis

  • Tracks allocations and deallocations at compile time
  • Detects memory leaks before runtime
  • Prevents double-free errors
  • Identifies use-after-free bugs

2. Ownership Attributes

  • #returns_ownership documents memory transfers
  • #takes_ownership clarifies responsibility
  • Compiler enforces ownership rules

3. Defer Statements

  • Guarantees cleanup code execution
  • Prevents resource leaks
  • Works with early returns and errors

4. Strong Type System

  • No implicit conversions (except safe ones)
  • Explicit casting required
  • Type-safe generics
  • Enum exhaustiveness checking

5. Explicit Error Handling

  • No hidden exceptions
  • Clear error propagation
  • Predictable control flow

Quick Reference

Keywords

const      let        if         elif       else
loop       break      continue   return     defer
struct     enum       pub        priv       cast
sizeof     alloc      free       switch     fn

Directives

@module "name"              // Declare module name
@use "name" as alias        // Import module
@os { "linux" -> { } }      // Platform-conditional code
@link("lib.so")             // Link against shared library (module-level)

Attributes

#returns_ownership          // Function returns allocated memory (caller must free)
#takes_ownership            // Function takes ownership of a pointer argument
#lib_import("lib.so")       // Per-function library override (POSIX)
#dll_import("dll", callconv: "stdcall")  // Per-function DLL import (Windows)

Operators

Arithmetic:  +  -  *  /  %  ++  --
Comparison:  ==  !=  <  >  <=  >=
Logical:     &&  ||  !
Bitwise:     &  |  ^  ~  <<  >>
Assignment:  =
Access:      .   ::  []  *  &

Primitive Types

int     double    bool    *T      [T; N]
uint    float     byte    void

Common Patterns

// Allocation with cleanup
let ptr: *T = cast<*T>(alloc(sizeof<T>));
defer free(ptr);

// Array iteration
loop [i: int = 0](i < size) : (++i) {
    array[i] = value;
}

// Error checking
if (ptr == cast<*T>(0)) {
    return ERROR_CODE;
}

// Module usage
@use "std_module" as m
let x: int = m::function();

// FFI — link a shared library (POSIX)
@link("libsomething.so")
pub const some_fn -> fn (x: int) int;

// FFI — per-function library override
#lib_import("libm.so")
pub const sqrt -> fn (x: double) double;

// FFI — Windows DLL
#dll_import("user32.dll", callconv: "stdcall")
pub const MessageBoxA -> fn (hwnd: int, text: *byte, caption: *byte, utype: int) int;

// Platform-conditional code
@os {
    "linux"   -> { outputln("Linux"); }
    "macos"   -> { outputln("macOS"); }
    "windows" -> { outputln("Windows"); }
}

Conclusion

Luma is a modern systems programming language that provides:

  • Simplicity: Clean, consistent syntax
  • Safety: Static analysis and ownership tracking
  • Performance: Zero-cost abstractions and predictable behavior
  • Control: Manual memory management with safety nets
  • Interoperability: First-class FFI via @link, #lib_import, and #dll_import
  • Portability: @os blocks for clean cross-platform code

The language is designed for programmers who want the performance and control of C with modern safety features and ergonomics.

For more examples, see the standard library modules and test files included with the language distribution.