vtables, protocol
This commit is contained in:
167
specs.md
167
specs.md
@@ -278,6 +278,165 @@ Struct values in string interpolation print as `TypeName{field:value, ...}`:
|
||||
print("{}", v1); // Vec4{x:1.0, y:2.0, z:3.0, w:0.0}
|
||||
```
|
||||
|
||||
### Struct Methods
|
||||
Functions declared inside a struct body become methods, registered as `StructName.method`:
|
||||
```sx
|
||||
Point :: struct {
|
||||
x, y: s32;
|
||||
|
||||
sum :: (self: *Point) -> s32 { self.x + self.y; }
|
||||
}
|
||||
|
||||
p := Point.{ x = 3, y = 4 };
|
||||
print("{}\n", p.sum()); // 7
|
||||
```
|
||||
|
||||
Methods receive the struct (typically as a pointer) as their first parameter. Dot-call syntax `obj.method(args)` resolves struct methods — it is **not** UFCS for arbitrary free functions. The pipe operator `|>` remains the universal UFCS mechanism.
|
||||
|
||||
### Protocol Types
|
||||
|
||||
Protocols define a set of method signatures that types can implement. They enable:
|
||||
- **Static dispatch**: compile-time checked constraints on generic type parameters.
|
||||
- **Dynamic dispatch**: type-erased protocol values with runtime method dispatch through function pointers.
|
||||
|
||||
#### Declaration
|
||||
```sx
|
||||
Allocator :: protocol #inline {
|
||||
alloc :: (size: s64) -> *void;
|
||||
dealloc :: (ptr: *void);
|
||||
}
|
||||
```
|
||||
|
||||
Protocol methods have an **implicit receiver** — no `self` in the protocol signature. The compiler adds `*Self` automatically. The `#inline` modifier embeds function pointers directly in the protocol value (no vtable indirection).
|
||||
|
||||
#### `#inline` vs default layout
|
||||
|
||||
| Layout | Declaration | Value layout | Dispatch cost |
|
||||
|--------|-------------|--------------|---------------|
|
||||
| `#inline` | `protocol #inline { ... }` | `{ ctx: *void, fn_ptr1, fn_ptr2, ... }` | Zero indirection |
|
||||
| Default | `protocol { ... }` | `{ ctx: *void, __vtable: *Vtable }` | One pointer chase |
|
||||
|
||||
Use `#inline` for protocols with few methods where call overhead matters (e.g., allocators). Use the default layout for protocols with many methods to keep the value size small.
|
||||
|
||||
#### `impl` Blocks
|
||||
```sx
|
||||
impl Allocator for GPA {
|
||||
alloc :: (self: *GPA, size: s64) -> *void {
|
||||
self.alloc_count += 1;
|
||||
malloc(size);
|
||||
}
|
||||
dealloc :: (self: *GPA, ptr: *void) {
|
||||
self.alloc_count -= 1;
|
||||
free(ptr);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Top-level declarations (not inside struct bodies)
|
||||
- Enable retroactive conformance — implement a protocol for types you don't own
|
||||
- Impl methods are also registered as struct methods (`GPA.alloc`) for direct calls
|
||||
- Duplicate `{Protocol, Type}` pair in the same compilation unit is a compile error
|
||||
|
||||
#### Protocol Values and `xx` Conversion
|
||||
Convert a concrete type to a protocol value with `xx`:
|
||||
```sx
|
||||
gpa := GPA.init();
|
||||
a : Allocator = xx gpa; // concrete → protocol value
|
||||
ptr := a.alloc(64); // dynamic dispatch through fn-ptr
|
||||
a.dealloc(ptr);
|
||||
```
|
||||
|
||||
`xx` works at assignment, call sites, and return positions:
|
||||
```sx
|
||||
use_allocator(xx gpa); // at call site
|
||||
make_alloc :: () -> Allocator { xx gpa; } // in return position
|
||||
```
|
||||
|
||||
Protocol values can be stored in struct fields, arrays, and passed through function calls:
|
||||
```sx
|
||||
Arena :: struct {
|
||||
parent: Allocator; // protocol value as struct field
|
||||
// ...
|
||||
}
|
||||
|
||||
allocators : [2]Allocator = .[xx gpa, xx arena]; // protocol values in array
|
||||
```
|
||||
|
||||
#### Default Methods
|
||||
Protocol methods can have bodies. `self` dispatches through the vtable (dynamic dispatch):
|
||||
```sx
|
||||
Writer :: protocol {
|
||||
write :: (data: string) -> s64; // required
|
||||
write_line :: (data: string) -> s64 { // default
|
||||
n := self.write(data);
|
||||
n + self.write("\n");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Default methods are used unless overridden in the impl. Default methods calling `self.method()` dispatch through the vtable, so they work correctly with any concrete type.
|
||||
|
||||
#### `Self` Type
|
||||
`Self` is a contextual keyword in protocol declarations — resolves to the concrete type in impls:
|
||||
```sx
|
||||
Eq :: protocol { eq :: (other: Self) -> bool; }
|
||||
|
||||
impl Eq for Point {
|
||||
eq :: (self: *Point, other: Point) -> bool {
|
||||
self.x == other.x and self.y == other.y;
|
||||
}
|
||||
}
|
||||
|
||||
// Static dispatch:
|
||||
p1.eq(p2); // calls Point.eq directly
|
||||
|
||||
// Dynamic dispatch:
|
||||
e : Eq = xx p1;
|
||||
e.eq(p2); // dispatches through vtable, Self params erased to *void
|
||||
```
|
||||
|
||||
For dynamic dispatch, `Self` parameters are erased to `*void` — the caller passes a pointer to the argument, and the thunk loads the concrete value.
|
||||
|
||||
#### Generic Constraints
|
||||
`$T/Protocol` syntax validates that a type parameter implements the required protocol(s):
|
||||
```sx
|
||||
are_equal :: (a: $T/Eq, b: T) -> bool { a.eq(b); }
|
||||
|
||||
// Multiple constraints:
|
||||
eq_and_hash :: (a: $T/Eq/Hashable, b: T) -> bool { ... }
|
||||
```
|
||||
|
||||
Constraints produce clear errors at monomorphization: `"s64 does not implement Hashable"`. Dispatch is static — same as unconstrained generics but with compile-time validation.
|
||||
|
||||
Constraints also work on struct type parameters:
|
||||
```sx
|
||||
SortedPair :: struct ($T: Type/Comparable) {
|
||||
lo: T;
|
||||
hi: T;
|
||||
}
|
||||
```
|
||||
|
||||
#### Generic Struct Impls
|
||||
```sx
|
||||
Pair :: struct ($T: Type) { a: T; b: T; }
|
||||
|
||||
impl Summable for Pair($T) {
|
||||
sum :: (self: *Pair(T)) -> s32 { xx self.a + xx self.b; }
|
||||
}
|
||||
```
|
||||
|
||||
The impl is instantiated per concrete type argument, like generic struct methods.
|
||||
|
||||
#### Dispatch Rules
|
||||
|
||||
| Usage | Dispatch | Cost |
|
||||
|-------|----------|------|
|
||||
| `gpa.alloc(64)` on `*GPA` | Static — direct call | Zero |
|
||||
| `$T/Allocator` constraint | Static — monomorphized | Zero |
|
||||
| `a : Allocator = xx gpa; a.alloc(64)` | Dynamic — fn-ptr / vtable | Indirect call |
|
||||
|
||||
Static dispatch is automatic when the concrete type is known. Dynamic dispatch only when explicitly type-erased via `xx` into a protocol value.
|
||||
|
||||
### Tuple Types
|
||||
Anonymous product types with optional field names. Tuples are first-class values — they can be stored in variables, passed to functions, and returned.
|
||||
|
||||
@@ -1155,7 +1314,11 @@ if handler := btn.on_click {
|
||||
```
|
||||
|
||||
#### 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.
|
||||
Closure env is allocated via `context.allocator`. The compiler auto-initializes `context` with a default GPA (malloc/free wrapper) at the start of `main()`. Use `push Context` to override with a custom allocator. Auto-promoted closures have a null env and require no allocation.
|
||||
```sx
|
||||
f := closure((x: s64) -> s64 => x + 10); // env allocated via default GPA
|
||||
print("{}\n", f(5));
|
||||
```
|
||||
|
||||
### Function Call
|
||||
```sx
|
||||
@@ -1283,7 +1446,7 @@ Context :: struct {
|
||||
context : Context = ---; // global mutable variable
|
||||
```
|
||||
|
||||
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.
|
||||
The compiler auto-initializes `context` with a default GPA (malloc/free wrapper) at the start of `main()`. Inside the pushed block, any code (including called functions) can read `context.allocator` and `context.data`. The standard library's `cstring()`, `alloc_slice()`, and `closure()` all allocate via `context.allocator`.
|
||||
|
||||
`push` requires a global mutable variable named `context` to be in scope (provided by `std.sx`).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user