a785747c37
ci / build (ubuntu) (push) Has been cancelled
ci / macos smoke (push) Has been cancelled
ci / windows smoke (push) Has been cancelled
selfhost-loop / bootstrap determinism (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 / CI gate (push) Has been cancelled
Ship the QUALITY_PLAN platform focus: thin/minimal runtime, --static/--target, Nexus HTTPS/mTLS with graceful stop, lock checksums + install --locked, selfhost registry (search/add/HTTP), containers, and CI smokes for cloud path.
1391 lines
35 KiB
Markdown
1391 lines
35 KiB
Markdown
# Bux Language Reference
|
||
|
||
This document describes the Bux programming language as implemented by the bootstrap compiler.
|
||
|
||
---
|
||
|
||
## Table of Contents
|
||
|
||
1. [Lexical Structure](#lexical-structure)
|
||
2. [Types](#types)
|
||
3. [Variables](#variables)
|
||
4. [Functions](#functions)
|
||
5. [Control Flow](#control-flow)
|
||
6. [Structs](#structs)
|
||
7. [Enums](#enums)
|
||
8. [Pattern Matching](#pattern-matching)
|
||
9. [Methods and Interfaces](#methods-and-interfaces)
|
||
10. [Generics](#generics)
|
||
11. [Gradual Ownership](#gradual-ownership-phase-82--implemented) — Checked / Release / [Drop & RAII](#drop-and-raii)
|
||
12. [Error Handling](#error-handling)
|
||
13. [Modules and Imports](#modules-and-imports)
|
||
14. [Async/Await](#asyncawait)
|
||
15. [Operator Overloading](#operator-overloading)
|
||
16. [Operators](#operators)
|
||
17. [Macros](#macros)
|
||
|
||
---
|
||
|
||
## Lexical Structure
|
||
|
||
### Comments
|
||
```bux
|
||
// 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
|
||
```bux
|
||
"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
|
||
```bux
|
||
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
|
||
|
||
```bux
|
||
*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
|
||
```bux
|
||
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
|
||
```bux
|
||
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
|
||
```bux
|
||
struct Point {
|
||
x: int;
|
||
y: int;
|
||
}
|
||
```
|
||
|
||
### Enums
|
||
```bux
|
||
enum Color {
|
||
Red,
|
||
Green,
|
||
Blue
|
||
}
|
||
|
||
// Algebraic enum (tagged union)
|
||
enum Result {
|
||
Ok(int),
|
||
Err(String)
|
||
}
|
||
```
|
||
|
||
### Unions
|
||
```bux
|
||
union Bits {
|
||
asByte: uint8;
|
||
asInt: int32;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Variables
|
||
|
||
```bux
|
||
let x: int = 42; // Immutable
|
||
var y: int = 10; // Mutable
|
||
y = 20; // OK
|
||
|
||
const MAX: int = 100; // Compile-time constant
|
||
```
|
||
|
||
---
|
||
|
||
## Functions
|
||
|
||
```bux
|
||
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
|
||
```bux
|
||
if x > 0 {
|
||
PrintLine("positive");
|
||
} else if x < 0 {
|
||
PrintLine("negative");
|
||
} else {
|
||
PrintLine("zero");
|
||
}
|
||
```
|
||
|
||
### Loops
|
||
```bux
|
||
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
|
||
```bux
|
||
outer: loop {
|
||
loop {
|
||
break outer;
|
||
}
|
||
}
|
||
```
|
||
|
||
### `defer`
|
||
Runs an expression when the current scope exits (LIFO order).
|
||
|
||
```bux
|
||
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.
|
||
|
||
```bux
|
||
switch statusCode {
|
||
case 200: PrintLine("OK");
|
||
case 404: PrintLine("Not Found");
|
||
case 500: PrintLine("Server Error");
|
||
default: PrintLine("Unknown");
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Structs
|
||
|
||
```bux
|
||
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
|
||
```bux
|
||
enum Color { Red, Green, Blue }
|
||
|
||
let c: Color = Color::Red;
|
||
if c == Color::Red {
|
||
PrintLine("red");
|
||
}
|
||
```
|
||
|
||
### Algebraic Enums
|
||
```bux
|
||
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
|
||
|
||
```bux
|
||
// 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
|
||
|
||
```bux
|
||
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
|
||
|
||
```bux
|
||
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:
|
||
|
||
```bux
|
||
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
|
||
|
||
```bux
|
||
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.
|
||
|
||
```bux
|
||
// 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.
|
||
|
||
```bux
|
||
@[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)
|
||
```bux
|
||
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
|
||
```bux
|
||
@[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.
|
||
|
||
```bux
|
||
@[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).
|
||
|
||
```bux
|
||
// 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:
|
||
|
||
```bux
|
||
// 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 |
|
||
|
||
```bux
|
||
@[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.
|
||
|
||
```bux
|
||
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):
|
||
|
||
```c
|
||
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 init** — `S { field: local }` (field-move)
|
||
- **Assignment** — `a = 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:
|
||
|
||
```bux
|
||
@[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)`.
|
||
|
||
```bux
|
||
@[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:
|
||
|
||
```bux
|
||
@[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:
|
||
|
||
```bux
|
||
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.
|
||
|
||
```bux
|
||
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.
|
||
|
||
```bux
|
||
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
|
||
```bux
|
||
enum Result {
|
||
Ok(int),
|
||
Err(String)
|
||
}
|
||
|
||
enum Option {
|
||
Some(int),
|
||
None
|
||
}
|
||
```
|
||
|
||
### The `?` Operator
|
||
The `?` operator automatically propagates errors:
|
||
|
||
```bux
|
||
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.
|
||
|
||
---
|
||
|
||
## Modules and Imports
|
||
|
||
```bux
|
||
// 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
|
||
|
||
```bux
|
||
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
|
||
|
||
```bux
|
||
async func Compute() -> int {
|
||
PrintLine("step 1");
|
||
bux_async_yield();
|
||
PrintLine("step 2");
|
||
return 42;
|
||
}
|
||
```
|
||
|
||
### Spawning Tasks
|
||
|
||
```bux
|
||
let handle = spawn Compute();
|
||
```
|
||
|
||
### Awaiting Results
|
||
|
||
```bux
|
||
let result: int = handle.await as int;
|
||
```
|
||
|
||
### Full Example
|
||
|
||
```bux
|
||
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
|
||
|
||
```bux
|
||
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 (expr/ident/lit/block/stmt/pat); 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(…)`, …) |
|
||
|
||
- 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.
|
||
|
||
### Invocation
|
||
|
||
```bux
|
||
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!`
|
||
|
||
```bux
|
||
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 site’s 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 backend’s **function-scoped** locals (e.g. `__m1_n` and `__m2_n`).
|
||
|
||
Spliced `$frags` in expression positions are **not** gensym’d — 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** gensym’d:
|
||
|
||
```bux
|
||
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).
|