This commit is contained in:
agra
2026-02-23 13:45:44 +02:00
parent 1cc67f9b5a
commit 0cc7b69441
11 changed files with 1472 additions and 31 deletions

View File

@@ -1081,6 +1081,82 @@ SOME_FUNC :: () => 42; // () -> s32
double :: (x: $T) -> T => x + x; // generic lambda with return type
```
### Closures
A **closure** is a function bundled with captured state. It is represented as a fat pointer `{ fn_ptr, env }` (16 bytes), unlike a bare function pointer which is 8 bytes.
#### Closure Type
```sx
Closure(param_types) -> R // e.g. Closure(s32, s32) -> s32
Closure(param_types) // void return: Closure(s64) -> void
?Closure(s32) -> s32 // optional closure (null = none)
```
#### Creating Closures — `closure()` intrinsic
```sx
offset := 50;
f := closure((x: s32) -> s32 => x + offset); // expression body
g := closure((x: s32) -> s32 { // block body
if x < 0 { return 0; }
return x + offset;
});
```
The `closure()` intrinsic:
1. Analyzes the lambda body for free variables (variables from outer scope)
2. Allocates an env struct on the heap (via `malloc`) containing captured values
3. Generates a trampoline function with signature `(env: *void, params...) -> R`
4. Returns a `Closure` value `{ trampoline, env_ptr }`
**Capture semantics**: capture by value (snapshot at creation time). Mutating the original variable after creating the closure does not affect the captured value.
```sx
n := 10;
f := closure((x: s64) -> s64 => x + n);
n = 999;
print("{}\n", f(5)); // 15, not 1004
```
#### Calling Closures
Closures are called with normal function call syntax:
```sx
result := f(10);
```
The compiler prepends the env pointer to the argument list and does an indirect call through the fn_ptr.
#### Auto-Promotion
A bare function can be implicitly promoted to a `Closure` where one is expected. The compiler generates a static thunk that ignores the env parameter, with a null env pointer.
```sx
double :: (x: s32) -> s32 { return x * 2; }
apply :: (f: Closure(s32) -> s32, x: s32) -> s32 { return f(x); }
apply(double, 10); // double auto-promoted to Closure
```
#### Factory Functions
Functions can return closures, enabling the factory pattern:
```sx
make_adder :: (n: s32) -> Closure(s32) -> s32 {
return closure((x: s32) -> s32 => x + n);
}
add5 := make_adder(5);
print("{}\n", add5(100)); // 105
```
#### Optional Closures
`?Closure` is supported for nullable callbacks. Uses `fn_ptr == null` as the none sentinel (zero overhead — same layout as `Closure`).
```sx
Button :: struct {
label: string;
on_click: ?Closure(s64) -> void;
}
btn := Button.{ label = "OK", on_click = null };
if handler := btn.on_click {
handler(1);
}
```
#### Memory
Closure env is heap-allocated via `malloc`. The caller is responsible for freeing `closure.env` when the closure is no longer needed. Auto-promoted closures have a null env and require no freeing.
### Function Call
```sx
callee(args)
@@ -1192,7 +1268,7 @@ Statements are terminated by `;`.
The `push` statement temporarily overrides a global `context` variable for the duration of a block. The previous context is saved before the block and restored after it exits.
```sx
push Context.{ arena = @arena, data = xx @logger } {
push Context.{ allocator = arena.allocator(), data = xx @logger } {
handle(client); // inside here, `context` has the new value
}
// context is restored to its previous value here
@@ -1201,13 +1277,13 @@ push Context.{ arena = @arena, data = xx @logger } {
**`Context` struct** — defined in `std.sx`:
```sx
Context :: struct {
arena: *Arena; // pointer to active arena allocator (or null)
data: *void; // opaque pointer for application-specific data
allocator: Allocator; // active allocator for dynamic allocation
data: *void; // opaque pointer for application-specific data
}
context : Context = ---; // global mutable variable
```
Inside the pushed block, any code (including called functions) can read `context.arena` and `context.data`. The standard library's `cstring()` function checks `context.arena` and uses it for allocation when available, falling back to `malloc()` otherwise.
Inside the pushed block, any code (including called functions) can read `context.allocator` and `context.data`. The standard library's `cstring()` and `alloc_slice()` functions use `context.allocator` for allocation when its `.ctx` is non-null, falling back to `malloc()` otherwise.
`push` requires a global mutable variable named `context` to be in scope (provided by `std.sx`).