Files
bux-lang/docs/LanguageRef.md
T
dimgigov ec5984762b
ci / build (ubuntu) (push) Has been cancelled
ci / unit + fmt (push) Has been cancelled
ci / examples (push) Has been cancelled
ci / goldens + tools (push) Has been cancelled
ci / apps (push) Has been cancelled
ci / selfhost smoke (push) Has been cancelled
ci / macos smoke (push) Has been cancelled
ci / windows smoke (push) Has been cancelled
ci / CI gate (push) Has been cancelled
selfhost-loop / bootstrap determinism (push) Has been cancelled
feat: try/unwrap payload types, LSP format, macro paste, freestanding runtime
- Type `?`/`!` as Result/Option Ok payload (not always int); fix unwrap C types
- LSP 0.18 document formatting (bux fmt) + VS Code format-on-save
- Macro `:type` generics (Array_New<$t>) and operators-only tt paste
- Ship runtime_freestanding.c + BUX_RUNTIME=freestanding + smokes/examples
2026-07-28 16:56:35 +03:00

39 KiB
Raw Blame History

Bux Language Reference

Status: Normative for v1.0.0 (language freeze).
Describes Bux as implemented by the bootstrap (buxc, Nim) and self-hosted (buxc2) compilers.
Behaviour here is the contract for semver after 1.0 — see SEMVER.md and RELEASE_v1.0.0.md.

This document is the primary language specification. Compiler bugs that contradict it are fixed without a MAJOR version bump; intentional breaking changes require MAJOR.


Table of Contents

  1. Lexical Structure
  2. Types
  3. Variables
  4. Functions
  5. Control Flow
  6. Structs
  7. Enums
  8. Pattern Matching
  9. Methods and Interfaces
  10. Generics
  11. Gradual Ownership — Checked / Release / Drop & RAII
  12. Error Handling
  13. Modules and Imports
  14. Async/Await
  15. Operator Overloading
  16. Operators
  17. Macros

Lexical Structure

Comments

// Single-line comment

/*
   Multi-line comment
   /* Nested comments are supported */
*/

Identifiers

Identifiers start with a letter or underscore, followed by letters, digits, or underscores.

Keywords

func, let, var, const, type, struct, enum, union, interface, extend
module, import, pub, extern, if, else, while, do, loop, for, in
break, continue, return, match, as, is, null, self, super, sizeof
async, await, spawn, defer, switch, case, default, checked

String Literals

"Hello"           // String (UTF-8) — escape sequences: \n \t \r \\ \"
c8"Hello"         // *char8 (C string)
c16"Hello"        // *char16
c32"Hello"        // *char32
`raw literal`     // Raw multi-line string — no escape processing
`line 1
line 2
line 3`           // Newlines preserved as-is
f"Hello, {name}"  // Interpolated string — expressions inside {}

Backtick raw strings (`...`) treat all characters literally:

  • \n is two characters, not a newline
  • Actual newlines in source are preserved in the string
  • No way to escape the backtick character itself (use regular strings if needed)

Interpolated strings (f"..."):

  • Expressions inside {} are evaluated and converted to String
  • Supported types: int, uint, float, bool, String
  • Escaped braces: \{ and \}

Number Literals

42        // int
3.14      // float64
0x2A      // hex
0o52      // octal
0b101010  // binary
32i8      // int8 literal
1000u64   // uint64 literal

Types

Primitive Types

Type Description
int8, int16, int32, int64, int Signed integers
uint8, uint16, uint32, uint64, uint Unsigned integers
float32, float64 Floating-point
bool, bool8, bool16, bool32 Booleans
char8, char16, char32 Characters
String C-compatible string (const char*)

Composite Types

*T              // Pointer to T
&T              // Shared reference (read-only in checked functions)
&mut T          // Mutable reference (exclusive borrow)
own T           // Owned value (move semantics)
T[]             // Slice (unsized)
T[N]            // Fixed-size array
(T1, T2, T3)    // Tuple — access fields with .0, .1, .2
func(T1) -> T2  // Function pointer type

Tuples

func Pair(a: int, b: int) -> (int, int) {
    return (a, b);
}

func Main() -> int {
    let t: (int, int) = Pair(10, 20);
    PrintInt(t.0);  // 10
    PrintInt(t.1);  // 20
    return 0;
}

Function pointers and closures

func Apply(f: func(int) -> int, x: int) -> int {
    return f(x);
}

func Double(n: int) -> int { return n * 2; }

func MakeAdder(base: int) -> func(int) -> int {
    // Each call allocates its own capture environment
    return |a: int| -> int { return a + base; };
}

func Main() -> int {
    let g: func(int) -> int = Double;   // named func → fat pointer
    let a10 = MakeAdder(10);
    let a20 = MakeAdder(20);
    // a10 and a20 are independent instances
    return Apply(g, 21) + a10(1) + a20(1);  // 42 + 11 + 21
}

func(T) -> R values are fat pointers { code, env }:

  • capturing closures store captures in a heap env
  • capture-less closures and named functions use env = null

Structs

struct Point {
    x: int;
    y: int;
}

Enums

enum Color {
    Red,
    Green,
    Blue
}

// Algebraic enum (tagged union)
enum Result {
    Ok(int),
    Err(String)
}

Unions

union Bits {
    asByte: uint8;
    asInt: int32;
}

Variables

let x: int = 42;       // Immutable
var y: int = 10;       // Mutable
y = 20;                // OK

const MAX: int = 100;  // Compile-time constant

Functions

func Add(a: int, b: int) -> int {
    return a + b;
}

// Extern C function
extern func printf(fmt: *char8, ...);

// Generic function
func Min<T>(a: T, b: T) -> T {
    if a < b {
        return a;
    }
    return b;
}

// Named and default parameters
func HttpResponse(code: int = 200, body: String = "") -> Response { ... }
let r: Response = HttpResponse(body: "hello");      // code defaults to 200
let s: Response = HttpResponse(404, body: "err");   // positional + named mixed

// Operator overloading (bootstrap only)
func Vec2_operator_add(self: *Vec2, other: Vec2) -> Vec2 { ... }
func Vec2_operator_sub(self: *Vec2, other: Vec2) -> Vec2 { ... }
func Vec2_operator_eq(self: *Vec2, other: Vec2) -> bool { ... }
func Vec2_operator_lt(self: *Vec2, other: Vec2) -> bool { ... }
func MyArray_operator_index_get(self: *MyArray, idx: int) -> int { ... }
func MyArray_operator_index_set(self: *MyArray, idx: int, value: int) { ... }

// Closures (capture-less for now)
let add: func(int, int) -> int = |a: int, b: int| -> int { return a + b; };
let sum: int = add(3, 4);  // 7

// Closure passed to higher-order function
func Apply(x: int, op: func(int) -> int) -> int { return op(x); }
let doubled: int = Apply(5, |x: int| -> int { return x * 2; });  // 10

Control Flow

If / Else

if x > 0 {
    PrintLine("positive");
} else if x < 0 {
    PrintLine("negative");
} else {
    PrintLine("zero");
}

Loops

while i < 10 {
    i = i + 1;
}

do {
    i = i + 1;
} while i < 10;

loop {
    // Infinite loop
    break;
}

for i in 0..10 {
    // Range 0 to 9 (exclusive)
}

for i in 0..=10 {
    // Range 0 to 10 (inclusive)
}

Break / Continue with Labels

outer: loop {
    loop {
        break outer;
    }
}

defer

Runs an expression when the current scope exits (LIFO order).

func ReadFile(path: String) -> String {
    let fd: int = Open(path);
    defer Close(fd);
    defer PrintLine("done");
    let data: String = ReadAll(fd);
    return data;   // both defers run before return
}

switch / case

Desugars to an if-else chain. Supports a default case.

switch statusCode {
    case 200: PrintLine("OK");
    case 404: PrintLine("Not Found");
    case 500: PrintLine("Server Error");
    default:  PrintLine("Unknown");
}

Structs

struct Rectangle {
    width: int;
    height: int;
}

func Main() -> int {
    let rect: Rectangle = Rectangle { width: 10, height: 5 };
    PrintInt(rect.width);
    return 0;
}

Enums

Simple Enums

enum Color { Red, Green, Blue }

let c: Color = Color::Red;
if c == Color::Red {
    PrintLine("red");
}

Algebraic Enums

enum Result {
    Ok(int),
    Err(String)
}

enum Pair {
    Two(int, int),   // multi-field → nested payload
    One(int),        // single-field → flat data.One_0
    None
}

func Main() -> int {
    let r: Result = Result { tag: Result_Ok };
    r.data.Ok_0 = 42;

    // Multi-field construction: data.Variant.Variant_i
    var p: Pair = Pair { tag: Pair_Two };
    p.data.Two.Two_0 = 3;
    p.data.Two.Two_1 = 4;

    if r.tag == Result_Ok {
        PrintInt(r.data.Ok_0);
    }
    return 0;
}

Layout notes:

  • Single positional field: flat union member data.Variant_0
  • Multi-field: nested payload data.Variant.Variant_0 / data.Variant.Variant_1 (C type Enum_Variant_Payload)

Pattern Matching

// Payload bindings: names in Variant(args) are bound in the arm body
func GetValue(opt: Option) -> int {
    match opt {
        Option::Some(value) => value,
        Option::None => 0
    }
}

match n {
    0 => 100,
    1..5 => 200,
    6..=10 => 300,
    _ => -1
}

Supported patterns:

  • Wildcard: _
  • Literal: 42, "hello", true
  • Identifier catch-all: name (binds whole subject)
  • Range: 1..9, 1..=9
  • Enum tags + payload bindings: Option::Some(value), Pair::Two(a, b)
  • Nested: Box::Val((a, b)), Shape::Dot(Point { x, y })
  • Tuple patterns: (a, b) → binds subject._0, subject._1
  • Struct patterns: Point { x: px, y: py } or shorthand Point { x, y }
  • Guard patterns: parsed; full lowering still evolving
match pair {
    (a, b) => a + b,
    _ => 0
}
match p {
    Point { x, y } => x * 10 + y,
    _ => -1
}
match bx {
    Box::Val((a, c)) => a + c,
    Box::Empty => 0
}
match sh {
    Shape::Dot(Point { x, y }) => x * 10 + y,
    Shape::Empty => -1
}

// Multi-statement arm bodies (block expression; last expr is the value)
match n {
    1 => {
        let a: int = 10;
        a + 1
    },
    _ => 0
}

// Block as expression
let r: int = {
    let x: int = 5;
    x + 6
};

Methods and Interfaces

struct Rectangle {
    width: int;
    height: int;
}

interface Drawable {
    func Draw(self: Rectangle);
}

extend Rectangle for Drawable {
    func Draw(self: Rectangle) {
        PrintLine("Drawing rectangle");
    }
}

// Or extend with standalone methods
extend Rectangle {
    func Area(self: Rectangle) -> int {
        return self.width * self.height;
    }
}

Generics

Generic Functions

Generic functions are monomorphized at compile time. Type parameters can be specified explicitly or inferred from arguments:

func Max<T>(a: T, b: T) -> T {
    if a > b { return a; }
    return b;
}

func Main() -> int {
    // Explicit type args
    let m1: int = Max<int>(10, 20);

    // Type inference — T inferred as int from arguments
    let m2: int = Max(10, 20);
    return 0;
}

Generic Structs

struct Box<T> {
    value: T,
}

// Use extend Type<T> for methods on generic structs
extend Box<T> {
    func Get(self: *Box<T>) -> T {
        return self.value;
    }

    func Set(self: *Box<T>, value: T) {
        self.value = value;
    }
}

func Main() -> int {
    let b: Box<int> = Box<int> { value: 42 };
    PrintInt(b.Get());  // 42
    b.Set(100);
    PrintInt(b.Get());  // 100
    return 0;
}

Note: extend Type<T> syntax requires type parameters on the impl block. The compiler propagates them to each method automatically.


Gradual Ownership (Phase 8.2) Implemented

Bux has gradual ownership — opt-in borrow checking. Default is permissive (C-like). Turn safety on where it matters; turn it off on hot paths with zero cost.

Three tiers

Mode Attribute Checks Cost
Default (none) None Zero — raw *T, free aliasing
Checked @[Checked] Moves, exclusive &mut, shared/&mut conflicts, dangling returns, elision Compile-time only
Release @[Release] Forced off (even if also @[Checked]) Zero — same codegen as default

Story: write most code unchecked for speed of iteration; mark critical APIs @[Checked]; mark micro-hotspots @[Release] (or both) when you need C-level performance without false positives.

// Tier 1 — default: C-like, no borrow checker
func QuickSort(arr: *int, len: int) {
    // free to alias, no move tracking
}

// Tier 2 — opt-in safety
@[Checked]
func Scale(val: &mut int) {
    *val = *val * 2;
}

// Tier 3 — zero-cost escape (e.g. hot loop helper)
@[Release]
func HotInc(p: *int) {
    *p = *p + 1;   // no checks; same as default, documents intent
}

// Release wins over Checked when both are present
@[Checked]
@[Release]
func HotButDocumented(p: &mut int) {
    *p = *p + 1;   // no borrow checks
}

Reference types

Type Syntax Description
Raw pointer *T C-style pointer, no checks
Shared ref &T Borrowed reference (read-only in checked functions)
Mutable ref &mut T Exclusive mutable borrow (allows mutation)
Owned own T Ownership type — values can be moved

Move Semantics

own T values can be moved. After a move, the original variable is uninitialized and cannot be used until reassigned.

@[Checked]
func Process(data: own String) {
    PrintLine(data);
    // data is consumed here
}

@[Checked]
func Main() {
    let msg: own String = "hello";
    Process(msg);          // move: msg is now uninitialized
    // PrintLine(msg);     // ERROR: use after move
    msg = "reassigned";    // OK: reinitialization
    PrintLine(msg);
}

Moves happen in three contexts:

  • Function call argument: Process(msg) moves msg into the parameter
  • Assignment: b = a moves a into b
  • Return: return x moves x out of the function

Rules in @[Checked] functions (not @[Release])

  • &T cannot be used to mutate data (compile-time error)
  • &mut T allows mutation
  • *T pointers are unrestricted (escape hatch)
  • &mut T coerces to &T and *T
  • Double mutable borrow: two live &mut of the same var (call args or let-bound)
    Swap(&mut x, &mut x);  // ERROR
    let a: &mut int = &mut x;
    let b: &mut int = &mut x;  // ERROR: exclusive mut already live
    
  • Use while mutably borrowed: assign/use of x while a let-bound &mut x is live
  • Shared while mut: cannot form &x while &mut x is live
  • Use after move: using a moved own T until reassigned
  • No dangling returns: cannot return a reference to a local
    @[Checked]
    func Bad(p: &int) -> &int {
        var x: int = 1;
        return &x;   // ERROR
    }
    

@[Release] (C.4 zero-cost path)

Use when a function must stay check-free:

  1. Documented hot path — same IR as unchecked, but the attribute states intent.
  2. Override Checked@[Checked] @[Release] on a method that would otherwise inherit team-wide Checked defaults.

There is no runtime cost: the attribute only disables the checker for that function body. Prefer @[Release] on the smallest possible surface; keep call boundaries @[Checked] when you still want API-level safety.

@[Checked]
func SafeApi(buf: &mut int) {
    // checked here
    HotPath(buf);
}

@[Release]
func HotPath(p: &mut int) {
    // no move / borrow tracking — write like C
    *p = *p + 1;
}

Lifetime elision (C.1)

In @[Checked] functions (and not @[Release]), most reference signatures need no lifetime annotations. Elision applies the usual single-input rules:

  1. Each elided input &T / &mut T parameter gets a distinct lifetime.
  2. If there is exactly one input lifetime, it is assigned to all elided outputs.
  3. If the first parameter is named self / Self, that input lifetime is preferred for outputs.
  4. Multiple input references + elided return → error (write an explicit lifetime).
// Elided — one input ref, return shares its lifetime
@[Checked]
func Identity(p: &int) -> &int {
    return p;   // OK
}

// Explicit — required when several inputs could be returned
@[Checked]
func Pick<'a>(a: &'a int, b: &'a int) -> &'a int {
    return a;
}

// Syntax: &'a T  and  &mut / &'a mut T  (lifetime before `mut`)
// Type parameters: func F<'a, T>(...)

Default and @[Release] functions ignore lifetime rules (C-like). Explicit 'a is optional documentation when a single input would already elide correctly.

Drop and RAII

Bux uses static destructors (no GC): when a value goes out of scope, the compiler may emit TypeName_Drop(&local). That is the RAII story — resources are released at every exit path without manual defer on every return.

Declaring cleanup

Two equivalent ways to opt a type into auto-drop:

// 1) Attribute — compiler looks up TypeName_Drop
@[Drop]
struct Token {
    id: int,
    counter: *int,
}

func Token_Drop(self: *Token) {
    // free / close / decrement …
}

// 2) Interface (stdlib `lib/Drop.bux`) — same static call, no vtable
import Drop;

extend Buffer for Drop {
    func Drop(self: *Buffer) {
        Mem_Free(self.data);
    }
}

Stdlib collections implement Drop (Array_Drop, Map_Drop, …). Calling Array_Drop is the same cleanup as Array_Free for Array<T>.

When auto-drop runs

Auto-drop is not gated on @[Checked]. Any function can receive injected Type_Drop at:

Exit Behavior
End of block / function Drop locals still owned
Early return Drop all live locals after materializing the return value
Branch scope end Only locals from the taken branch
Nested scopes Drop in reverse order of declaration
@[Drop]
struct Token { id: int, counter: *int }
func Token_Drop(self: *Token) { /* … */ }

func Early(flag: int, counter: *int) -> int {
    let t: Token = Token { id: 1, counter: counter };
    if flag == 0 {
        return 0;   // still runs Token_Drop(&t)
    }
    return 1;       // Token_Drop(&t) here too
}

See examples/drop_early_return.bux for branch-local vs fallthrough counts.

Field-move: skip Drop of the source (critical)

Problem: a local is moved by value into a struct field (or another local). If the compiler still auto-dropped the source, you get a double free — the field and the original local would both run Array_Drop on the same buffer.

Rule: after a value move out of a local, that local is not dropped.

struct Box {
    items: Array<int>;
}

func MakeBox() -> Box {
    var items: Array<int> = Array_New<int>(4);
    Array_Push<int>(&items, 10);
    Array_Push<int>(&items, 20);
    // Move `items` into the field — compiler skips Drop of `items`
    let b: Box = Box { items: items };
    return b;   // also: return-by-value skips Drop of `b` (caller owns it)
}

What the C backend does for MakeBox (simplified):

Box MakeBox(void) {
    Array_int items = Array_New_int(4);
    Array_Push_int(&items, 10);
    Array_Push_int(&items, 20);
    Box b = (Box){ .items = items };
    return b;
    /* no Array_Drop_int(&items);  — moved into b.items */
    /* no Array_Drop on b;         — moved to caller via return */
}

Ownership after MakeBox:

  1. Heap buffer lives inside b.items (and later the caller's Box).
  2. items is moved-out → skip auto-Drop.
  3. b is returned by value → skip auto-Drop at the return site; the caller (or the next owner) is responsible.

The same skip applies to:

  • Struct field initS { field: local } (field-move)
  • Assignmenta = b when b is moved (value types with Drop)
  • Call argument by value into a consuming parameter
  • return x — move-on-return

Live, unmoved Drop locals still clean up on error paths (e.g. early return before the move). That is intentional: only the successful transfer path skips Drop.

Runnable check: examples/move_field.bux (also covered by make test-selfhost-smoke on buxc2).

Partial field moves

Moving a droppable field out of a local (return or let) also skips Drop of the parent local:

@[Drop]
struct Bag {
    items: Array<int>,
    tag: int,
}
func Bag_Drop(self: *Bag) {
    Array_Drop<int>(&self.items);
}

func TakeItems() -> Array<int> {
    var items: Array<int> = Array_New<int>(4);
    Array_Push<int>(&items, 42);
    let bag: Bag = Bag { items: items, tag: 7 };
    return bag.items;   // Bag_Drop skipped — items ownership transferred
}

Rules:

  • Applies only when the field type is droppable (Array_*, @[Drop] types, etc.). Reading bag.tag (int) does not mark bag moved.
  • After let moved = bag.items, Bag_Drop(&bag) is skipped; moved owns the array and is auto-dropped at scope end.
  • Remaining fields: if the parent has other droppable fields that were not moved out, those still run their Type_Drop / collection Drop (session 70). Example: move pair.left → skip PairBag_Drop, still Tracked_Drop(&pair.right).
@[Drop]
struct PairBag {
    left: Array<int>,
    right: Tracked,   // also @[Drop]
}
func TakeLeft() -> Array<int> {
    let pair: PairBag = …;
    return pair.left;   // Tracked_Drop(&pair.right) still runs
}

Nested path moves (a.b.c)

Moving a deep droppable field also works. The full dotted path is recorded so remaining fields at every level still Drop:

@[Drop]
struct Outer {
    inner: Inner,   // Inner has items: Array + note: Tracked
    tag: Tracked,
}
func TakeNested() -> Array<int> {
    let outer: Outer = …;
    return outer.inner.items;
    // skips Outer_Drop
    // still: Tracked_Drop(&outer.inner.note) + Tracked_Drop(&outer.tag)
}

Field moves through pointers

When a local pointer aliases a local owner (let p = &bag), moving a field through the pointer marks the owner, not the pointer:

let bag: Bag = …;
let p: *Bag = &bag;
return p.items;      // same as (*p).items
// skips Bag_Drop; still Tracked_Drop(&bag.tag)

Nested paths work the same: p.inner.items resolves p → outer then path inner.items.

Cross-function pointer transfer (session 76)

When the caller passes &bag (or a pointer alias) into a function whose parameter is *Bag, and the callee moves fields of that param (return p.items / let x = p.items), the call site marks bag the same way as a local partial move — parent Bag_Drop is skipped; remaining fields Drop.

func TakeItems(p: *Bag) -> Array<int> {
    return p.items;
}

func Caller() {
    let bag: Bag = …;
    let items: Array<int> = TakeItems(&bag);
    // bag.items transferred; Tracked_Drop(&bag.tag) still runs
}

Analysis is same-module / known callee body only (bootstrap HIR today).

Golden smoke: make test-drop-move / examples/move_field_partial.bux / examples/move_field_remaining.bux / examples/move_field_nested.bux / examples/move_field_ptr.bux / examples/move_cross_fn.bux.

Manual Drop and non-Drop types

  • Types without @[Drop] / Drop impl are never auto-dropped (plain C layout).
  • You can still call Type_Drop(&x) or use defer for explicit cleanup.
  • @[Release] / default functions still get auto-drop for Drop types — Release only turns off the borrow checker, not RAII.

Limits (honest)

  • Partial field moves skip the parent Type_Drop and drop remaining droppable fields individually, including nested paths a.b.c, local pointer aliases p = &owner, and cross-function &owner args when the callee body is visible (sessions 70/73/74/76).
  • Local pointer aliases (p = &local) are tracked in-function; cross-function uses callee-body scan of pointer params (not full borrow checking).
  • Interface Drop uses a static TypeName_Drop symbol (zero cost), not dynamic dispatch through a vtable.
  • Double-free bugs in unchecked code that manually free and auto-drop are still possible if you free without invalidating the value — prefer one owner.

Compile-Time Function Execution (CTFE) Implemented

const func functions are evaluated at compile time. Their results can be used in type sizes, array lengths, or other constant contexts.

const func Factorial(n: int) -> int {
    if n <= 1 {
        return 1;
    }
    return n * Factorial(n - 1);
}

const TABLE_SIZE = Factorial(10);  // 3628800 — computed at compile time

func Main() -> int {
    let arr: [TABLE_SIZE]int;  // Array size from compile-time value
    return 0;
}

Supported in CTFE

  • Integer, boolean, and string literals
  • Arithmetic (+, -, *, /, %)
  • Comparisons and logical operators
  • if / else with constant conditions
  • Calls to other const func functions (including recursion)

Limitations

  • No while / for loops (use recursion)
  • No mut references or heap allocation
  • No non-const function calls

Error Handling

Result and Option Types

enum Result {
    Ok(int),
    Err(String)
}

enum Option {
    Some(int),
    None
}

The ? Operator

The ? operator automatically propagates errors:

func Divide(a: int, b: int) -> Result {
    if b == 0 {
        return Result_NewErr("division by zero");
    }
    return Result_NewOk(a / b);
}

func Compute() -> Result {
    let x: int = Divide(10, 2)?;  // If Err, returns immediately
    let y: int = Divide(x, 5)?;
    return Result_NewOk(y);
}

? can be used on Result and Option types in any expression context. The type of expr? is the Ok / Some payload (T in Result<T,E> or Option<T>), not always int. The enclosing function must return a compatible Result/Option so Err/None can propagate.

// Generic Result — payload type is String
func GetName() -> Result<String, String> {
    return Result_NewOk<String, String>("bux");
}
func Run() -> Result<String, String> {
    let n: String = GetName()?;  // n: String
    return Result_NewOk<String, String>(n);
}

The postfix unwrap operator expr! extracts Ok/Some or panics (and exits) on Err/None; its type is likewise the payload type.

See also examples/try_operator.bux and examples/try_generic.bux.


Modules and Imports

// Single import
import Std::Io::PrintLine;

// Multiple imports
import Std::Io::{PrintLine, PrintInt};

// Wildcard import
import Std::Io::*;

// Module declaration
module MyModule;

pub func PublicFunc() -> int {
    return 42;
}

func PrivateFunc() -> int {
    return 0;
}

Concurrency

Bux supports both async/await (stackful coroutines) and pthread-based threads with channels.

Threads and Channels

import Std::Task::{Task_Spawn, Task_Join, TaskHandle};
import Std::Channel::{Channel, Channel_New, Channel_SendInt, Channel_RecvInt, Channel_Close};

func Producer(ch: *Channel<int>) {
    Channel_SendInt(ch, 42);
    Channel_Close<int>(ch);
}

func Consumer(ch: *Channel<int>) -> int {
    let val: int = Channel_RecvInt(ch);
    return val;
}

func Main() -> int {
    let ch: Channel<int> = Channel_New<int>(1);
    let p: *void = spawn Producer(&ch);
    let c: *void = spawn Consumer(&ch);
    Task_Join(TaskHandle { handle: p });
    Task_Join(TaskHandle { handle: c });
    return 0;
}
  • spawn Func() creates a new pthread running Func
  • Channel<T> is a buffered channel with mutex/condvar
  • Channel_RecvInt returns 0 when the channel is closed and empty

Async/Await

Bux supports stackful coroutines via async/await with a round-robin scheduler.

Declaring Async Functions

async func Compute() -> int {
    PrintLine("step 1");
    bux_async_yield();
    PrintLine("step 2");
    return 42;
}

Spawning Tasks

let handle = spawn Compute();

Awaiting Results

let result: int = handle.await as int;

Full Example

import Std::Io::{PrintLine, PrintInt};

async func Compute() -> int {
    PrintLine("Compute: start");
    bux_async_yield();
    PrintLine("Compute: done");
    return 42;
}

func Main() -> int {
    let h = spawn Compute();
    let r: int = h.await as int;
    PrintInt(r);
    return 0;
}

Runtime Functions

Function Description
bux_async_yield() Yield control to the scheduler
bux_async_spawn(fn) Create a new coroutine from a function
bux_async_await(handle) Block until coroutine completes, return result
bux_async_run() Run the scheduler (called implicitly from main)
bux_async_sleep(ms) Sleep for ms milliseconds (non-blocking)
bux_async_return(value, size) Copy return value into task result buffer

Operator Overloading

Status: Implemented in bootstrap. Selfhost reserves syntax but has no method-table yet.

Overloadable operators use the naming convention TypeName_operator_<op>:

Operator Function Name Signature Example
+ operator_add func T_operator_add(self: *T, other: T) -> T
- operator_sub func T_operator_sub(self: *T, other: T) -> T
* operator_mul func T_operator_mul(self: *T, other: T) -> T
/ operator_div func T_operator_div(self: *T, other: T) -> T
% operator_mod func T_operator_mod(self: *T, other: T) -> T
== operator_eq func T_operator_eq(self: *T, other: T) -> bool
!= operator_ne func T_operator_ne(self: *T, other: T) -> bool
< operator_lt func T_operator_lt(self: *T, other: T) -> bool
<= operator_le func T_operator_le(self: *T, other: T) -> bool
> operator_gt func T_operator_gt(self: *T, other: T) -> bool
>= operator_ge func T_operator_ge(self: *T, other: T) -> bool
[] (get) operator_index_get func T_operator_index_get(self: *T, idx: int) -> U
[] (set) operator_index_set func T_operator_index_set(self: *T, idx: int, value: U)

Notes:

  • Short-circuit operators (&&, ||) cannot be overloaded.
  • Generic method instantiation is supported.

Operators

Arithmetic

+, -, *, /, %, ** (power)

Comparison

==, !=, <, <=, >, >=

Logical

&&, ||, !

Bitwise

&, |, ^, ~, <<, >>

Assignment

=, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=

Other

  • as — Cast: expr as Type
  • is — Type test: expr is Type
  • ? — Try / error propagation: expr?
  • & — Address-of: &var
  • * — Dereference: *ptr
  • :: — Path separator: Module::Name
  • .. — Range (exclusive): 0..10
  • ..= — Range (inclusive): 0..=10
  • sizeof — Size of type: sizeof(Type)

Macros

Bux supports declarative macros. Expansion runs after parse and before type-checking. Expanded AST uses call-site source locations (quote hygiene). Both bootstrap and selfhost (buxc2) expand macros.

Definition

macro! twice {
    ($x:expr) => {
        ($x) + ($x)
    }
}

// Trailing repetition
macro! sum_n {
    ( $($x:expr),* ) => {
        var acc: int = 0;
        $( acc = acc + $x; )*
        acc
    }
}

// Compound / zip: parallel lists from interleaved args
macro! add_pairs {
    ( $($a:expr, $b:expr),* ) => {
        var acc: int = 0;
        $( acc = acc + ($a + $b); )*
        acc
    }
}

// Multi-rep groups: `;` separates arg groups at the call site
macro! sum_groups {
    ( $($x:expr),* ; $($y:expr),* ) => {
        var s: int = 0;
        $( s = s + $x; )*
        $( s = s + $y; )*
        s
    }
}

// Nested template repetition (outer list → inner expands once per item)
macro! double_each_sum {
    ( $($x:expr),* ) => {
        var t: int = 0;
        $(
            $( t = t + $x; )*
            $( t = t + $x; )*
        )*
        t
    }
}

// ident fragment: bare identifier at the call site
macro! call0 {
    ( $f:ident ) => {
        $f()
    }
}

// literal (alias: lit) — only int/float/string/char/bool literals
macro! only_lit {
    ( $x:literal ) => { $x }
}

// block — only `{ … }` block expressions
macro! wrap_block {
    ( $b:block ) => { $b }
}

// stmt — one statement (let/if/… or expression-statement)
macro! with_setup {
    ( $s:stmt, $body:expr ) => {
        {
            $s
            $body
        }
    }
}
// call: with_setup!(let x: int = 10, x + 1)

// pat — match/let pattern (literals, `_`, enum variants, …)
macro! matches {
    ( $p:pat, $e:expr ) => {
        match $e { $p => 1, _ => 0 }
    }
}
// call: matches!(1, 1) · matches!(_, 99) · matches!(Opt::Some(v), opt)

// gensym: template locals renamed per expansion
macro! with_acc {
    ( $start:literal ) => {
        var n: int = $start;
        n = n + 1;
        n
    }
}
  • Introduced with the macro! keyword.

  • Each rule is ( pattern ) => { template }.

  • Fragment kinds:

    Kind Matches
    expr any expression
    ident bare identifier (ekIdent)
    tt token-tree: any single call-site AST fragment; delimiter-balanced multi-element groups (a, b) and [a, b] flatten when spliced as the sole call argument ($f($args)f(a, b), not f((a, b))). Non-group tt unwraps to the value. Broader than expr.
    literal / lit int/float/string/char/bool literal only
    block block expression { … }
    stmt one statement (let/if/… or expression-stmt)
    pat / pattern match pattern (_, literals, Enum::Var(…), …)
    type type expression from call-site shape: named (int), pointer (*int); spliced into sizeof($t), as $t, let x: $t
  • Fragment names start with $ (lexer $ident).

  • Repetition: $( $x:expr ),* / $( $x:expr )* — one or more rep fragments per pattern.

  • Compound rep: $( $a:expr, $b:expr ),* — interleaved args zip into parallel lists.

  • Multi-rep: two (or more) $(…)* in one pattern; call site uses ; between groups: sum_groups!(1, 2; 10, 20, 30).

  • Template $( stmt; … )* expands once per list item (zip when multiple lists used).

  • Nested $( $(…)* )*: after outer binds list items as singles, inner expands once.

  • Expression-level rep in templates: $f( $($a),* )$( expr ),* / $( expr )* inside call arguments expands to N positional args (session 84).

  • Delimiter-balanced :tt groups: apply_tt!(Add, (3, 4)) or apply_tt!(Add, [3, 4]) with ($f:ident, $args:tt) => { $f($args) } expands to Add(3, 4). Contrast :expr, which keeps the group as one value.

  • Free-form juxta (session 86): pattern $f:ident $args:tt (comma optional between fragments) matches a single call-site argument that is a call expression: apply_juxta!(Add(2, 5)) → binds $f=Add, $args = arg-list group, then $f($args) flattens to Add(2, 5).

  • Type fragments (session 87+): named, pointer, and generic types (Array<int>, *int); $t substitutes in sizeof / cast / let types and monomorph call type args (Array_New<$t>).

    macro! size_of {
        ( $t:type ) => { sizeof($t) as int }
    }
    macro! new_array {
        ( $t:type, $cap:expr ) => { Array_New<$t>($cap) }
    }
    let n: int = size_of!(int);
    let p: int = size_of!(*int);
    let s: int = size_of!(Array<int>);
    var a: Array<int> = new_array!(int, 4);
    
  • Operators-only :tt paste:

    macro! apply_op {
        ( $op:tt, $a:expr, $b:expr ) => { $op($a, $b) }
    }
    macro! flip_op {
        ( $a:expr, $op:tt, $b:expr ) => { $op($b, $a) }
    }
    let x: int = apply_op!(+, 3, 4);   // 7
    let y: int = apply_op!(*, 6, 7);   // 42
    let z: int = flip_op!(10 - 3);     // -7  (juxta binary split)
    

Invocation

let n = twice!(21);
let s = sum_n!(1, 2, 3);          // 6
let z = sum_n!();                 // 0
let p = add_pairs!(1, 10, 2, 20); // (1+10)+(2+20) = 33
let g = sum_groups!(1, 2; 10, 20, 30); // 63
let d = double_each_sum!(3, 4);   // 14
call0!(SomeFunc);
let a = with_acc!(10);            // 11
let b = with_acc!(20);            // 21 — different gensym'd `n`
let c = only_lit!(7);
// only_lit!(1 + 2);              // ERROR: no matching rule
let w = wrap_block!({ 1 + 2 });   // 3
  • Syntax: name!( arg, … ) (not unwrap: unwrap is expr! without ().
  • Matching: fixed-arity by count; kind constraints; rep by groups / remaining args / chunk.

Built-in quote!

let x = quote!(1 + 2);   // identity expand; locations grafted to call site

Hygiene

Two layers (both bootstrap + selfhost):

  1. Call-site graft — expanded AST uses the call sites line/col/sourceFile (so diagnostics and #line point at the user call, not the macro definition).

  2. Gensym of template binders — each expansion renames:

    • let / var locals introduced by the template
    • for loop binders in the template
    • Nested scopes (if/while/for bodies, MacroRep bodies)

    so two expansions of the same macro in one function do not collide under the C backends function-scoped locals (e.g. __m1_n and __m2_n).

Spliced $frags in expression positions are not gensymd — they keep call-site names/values.

Unhygienic binders (var $name)

To introduce a binder whose name comes from the call site, use a $frag as the binder itself. That name is not gensymd:

macro! let_mut {
    ( $name:ident, $init:literal ) => {
        var $name: int = $init;   // unhygienic: becomes `counter`, not __m1_…
        $name = $name + 1;
        $name
    }
}

// expands with local `counter` (and hygienic locals still unique)
let a = let_mut!(counter, 10);   // 11
let b = let_mut!(other, 20);     // 21

macro! double_acc {
    ( $start:literal ) => {
        var acc: int = $start;   // hygienic → __m1_acc / __m2_acc
        acc = acc + acc;
        acc
    }
}
Binder form After expand Gensym?
var acc = … (plain name in template) __mN_acc yes
var $name = … with $name:ident call-site ident no
for $i in … with $i:ident call-site ident no

The binder must be a :ident fragment bound to a bare identifier. A plain template name is always hygienic.

Examples: examples/macro_hygiene.bux, examples/macro_unhygienic.bux.

Limits

  • Up to two named rep lists per rule on selfhost (enough for zip + multi-rep).
  • Compound chunk size currently 1 or 2.
  • Nested macro calls expanded recursively (depth limit 32).
  • Unhygienic binders only rename let/var/for binders — not full Scheme/Rust colored identifiers or stmt/pat token trees.
  • Macro expansion still yields a block expression; unhygienic names are scoped to that block (not automatically injected into the caller scope).
  • Expression-level $(…)* is only parsed inside call argument lists in templates (not as a free-standing primary expression).
  • Raw delimiter-balanced tt covers tuple (a, b) and slice lit [a, b] groups, plus juxta call-split for $f:ident $args:tt matching F(a, b).
  • Operators-only paste: bare binary ops as :tt (+, *, ==, …) and juxta binary split $a:expr $op:tt $b:expr on a single binary arg. Template form $op($a, $b) rebuilds a OP b. See examples/macro_op_paste.bux.
  • :type generics: Array<int>, *int, nested type args; $t splices into sizeof($t), casts, and Array_New<$t>(…). See examples/macro_type.bux, examples/macro_type_generic.bux.
  • Fully free-form token streams (unparsed soup) remain out of scope.

Examples: examples/macro_tt.bux, examples/macro_tt_raw.bux, examples/macro_repeat.bux, examples/macro_nested.bux, examples/macro_type_generic.bux, examples/macro_op_paste.bux.