closures
This commit is contained in:
84
specs.md
84
specs.md
@@ -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`).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user