lang: require explicit receiver in protocol method declarations

Protocol method declarations now declare their receiver explicitly as the first
parameter — 'self: *Self' (or 'self: Self') — matching the impl method signature,
instead of the old implicit-receiver form where the listed params were only the
extra args. That asymmetry repeatedly caused confusion over whether the first
param was the receiver or an argument.

The parser validates the first param is 'self' typed Self/*Self, then strips it,
so all downstream lowering and the dispatch ABI are unchanged (impl blocks and
call sites are unaffected). A protocol method missing the receiver is now a parse
error.

Migrated all 129 protocol method signatures across library + examples (+ one
inline-sx test in sema.zig) to the explicit form. Updated specs.md + readme.md.

New: examples/0418-protocols-explicit-receiver.sx (feature),
examples/1190-diagnostics-protocol-missing-receiver.sx (negative/diagnostic).
This commit is contained in:
agra
2026-06-21 11:02:16 +03:00
parent eb93c63c45
commit 6b0ebdd92b
66 changed files with 249 additions and 146 deletions

View File

@@ -7,7 +7,7 @@
#import "modules/std.sx";
Show :: protocol {
show :: () -> string;
show :: (self: *Self) -> string;
}
A :: struct { x: i64; }
B :: struct { s: string; }

View File

@@ -10,11 +10,11 @@ mul :: (a: i32, b: i32) -> i32 { a * b }
// P4 edge: Chained default→default calls
Chained :: protocol {
base :: (msg: string) -> i32;
wrap :: (msg: string) -> i32 {
base :: (self: *Self, msg: string) -> i32;
wrap :: (self: *Self, msg: string) -> i32 {
self.base(msg) + 1
}
double_wrap :: (msg: string) -> i32 {
double_wrap :: (self: *Self, msg: string) -> i32 {
self.wrap(msg) + self.wrap(msg)
}
}

View File

@@ -28,11 +28,11 @@ apply :: (f: (i32, i32) -> i32, x: i32, y: i32) -> i32 {
// P4 edge: Chained default→default calls
Chained :: protocol {
base :: (msg: string) -> i32;
wrap :: (msg: string) -> i32 {
base :: (self: *Self, msg: string) -> i32;
wrap :: (self: *Self, msg: string) -> i32 {
self.base(msg) + 1
}
double_wrap :: (msg: string) -> i32 {
double_wrap :: (self: *Self, msg: string) -> i32 {
self.wrap(msg) + self.wrap(msg)
}
}

View File

@@ -9,12 +9,12 @@ Point :: struct { x, y: i32; }
add :: (a: i32, b: i32) -> i32 { a + b }
Counter :: protocol {
inc :: ();
get :: () -> i32;
inc :: (self: *Self);
get :: (self: *Self) -> i32;
}
Summable :: protocol {
sum :: () -> i32;
sum :: (self: *Self) -> i32;
}
SimpleCounter :: struct { val: i32; }

View File

@@ -28,7 +28,7 @@ Tag :: union {
// Protocol method SIGNATURE spelled with a reserved type name — bare is legal.
Speaker :: protocol {
i2 :: () -> i64;
i2 :: (self: *Self) -> i64;
}
Dog :: struct { n: i64; }

View File

@@ -5,7 +5,7 @@
#import "modules/math";
Lerpable :: protocol #inline {
lerp :: (b: Self, t: f32) -> Self;
lerp :: (self: *Self, b: Self, t: f32) -> Self;
}
Size :: struct {

View File

@@ -4,7 +4,7 @@
#import "modules/std.sx";
MyProtocol :: protocol {
get_value :: () -> i64;
get_value :: (self: *Self) -> i64;
}
MyImpl :: struct { value: i64; }

View File

@@ -3,7 +3,7 @@
// `#inline` erasure.
Lerpable :: protocol #inline {
lerp :: (b: Self, t: f32) -> Self;
lerp :: (self: *Self, b: Self, t: f32) -> Self;
}
impl Lerpable for f32 {

View File

@@ -5,7 +5,7 @@
#import "modules/std.sx";
Sizable :: protocol {
size :: () -> i64;
size :: (self: *Self) -> i64;
}
Widget :: struct { value: i64; }

View File

@@ -5,7 +5,7 @@
#import "modules/std.sx";
Sizable :: protocol {
size :: () -> i64;
size :: (self: *Self) -> i64;
}
Leaf :: struct { value: i64; }

View File

@@ -5,7 +5,7 @@
#import "modules/std.sx";
Sizable :: protocol {
size :: () -> i64;
size :: (self: *Self) -> i64;
}
Leaf :: struct { value: i64; }

View File

@@ -5,10 +5,10 @@
#import "modules/std.sx";
Drawable :: protocol {
draw :: () -> i32;
name :: () -> string;
layout :: (x: i32) -> i32;
handle :: (event: i32) -> bool;
draw :: (self: *Self) -> i32;
name :: (self: *Self) -> string;
layout :: (self: *Self, x: i32) -> i32;
handle :: (self: *Self, event: i32) -> bool;
}
Circle :: struct { radius: i32; }

View File

@@ -6,7 +6,7 @@
Fmt :: enum { a; b; }
Proto :: protocol {
take_fmt :: (f: Fmt);
take_fmt :: (self: *Self, f: Fmt);
}
Impl :: struct {}

View File

@@ -7,7 +7,7 @@
#import "modules/std.sx";
Proto :: protocol {
get :: () -> *u8;
get :: (self: *Self) -> *u8;
}
Impl :: struct {

View File

@@ -7,7 +7,7 @@
#import "modules/std.sx";
GPU :: protocol {
ping :: () -> i64;
ping :: (self: *Self) -> i64;
}
Impl :: struct {}

View File

@@ -6,7 +6,7 @@
#import "modules/std.sx";
VL :: protocol(T: Type) { get :: () -> T; }
VL :: protocol(T: Type) { get :: (self: *Self) -> T; }
IntCell :: struct { v: i64; }
StrCell :: struct { s: string; }
impl VL(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; }

View File

@@ -9,7 +9,7 @@
#import "modules/std.sx";
VL :: protocol(T: Type) { get :: () -> T; }
VL :: protocol(T: Type) { get :: (self: *Self) -> T; }
IntCell :: struct { v: i64; }
impl VL(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; }

View File

@@ -203,12 +203,12 @@ c_abs :: (n: i32) -> i32 extern libc "abs";
// --- Protocol declarations (Phase 1: static dispatch only) ---
Counter :: protocol {
inc :: ();
get :: () -> i32;
inc :: (self: *Self);
get :: (self: *Self) -> i32;
}
Summable :: protocol {
sum :: () -> i32;
sum :: (self: *Self) -> i32;
}
SimpleCounter :: struct { val: i32; }
@@ -226,8 +226,8 @@ impl Summable for Point {
// Phase 2: #inline protocol for dynamic dispatch
Adder :: protocol #inline {
add :: (n: i32);
value :: () -> i32;
add :: (self: *Self, n: i32);
value :: (self: *Self) -> i32;
}
Accumulator :: struct {
@@ -250,8 +250,8 @@ impl Adder for Doubler {
// Phase 4: default methods
Repeater :: protocol {
say :: (msg: string);
say_twice :: (msg: string) {
say :: (self: *Self, msg: string);
say_twice :: (self: *Self, msg: string) {
self.say(msg);
self.say(msg);
}
@@ -270,11 +270,11 @@ impl Repeater for Printer {
// P4 edge: Chained default→default calls
Chained :: protocol {
base :: (msg: string) -> i32;
wrap :: (msg: string) -> i32 {
base :: (self: *Self, msg: string) -> i32;
wrap :: (self: *Self, msg: string) -> i32 {
self.base(msg) + 1
}
double_wrap :: (msg: string) -> i32 {
double_wrap :: (self: *Self, msg: string) -> i32 {
self.wrap(msg) + self.wrap(msg)
}
}
@@ -291,7 +291,7 @@ impl Chained for ChainImpl {
// Phase 5: Self type
Eq :: protocol {
eq :: (other: Self) -> bool;
eq :: (self: *Self, other: Self) -> bool;
}
impl Eq for Point {
@@ -301,7 +301,7 @@ impl Eq for Point {
}
Cloneable :: protocol {
clone :: () -> Self;
clone :: (self: *Self) -> Self;
}
impl Cloneable for Point {
@@ -324,7 +324,7 @@ are_equal :: ($T: Type/Eq, a: T, b: T) -> bool {
}
Hashable :: protocol {
hash :: () -> i64;
hash :: (self: *Self) -> i64;
}
impl Hashable for Point {

View File

@@ -9,12 +9,12 @@ Point :: struct { x, y: i32; }
add :: (a: i32, b: i32) -> i32 { a + b }
Counter :: protocol {
inc :: ();
get :: () -> i32;
inc :: (self: *Self);
get :: (self: *Self) -> i32;
}
Summable :: protocol {
sum :: () -> i32;
sum :: (self: *Self) -> i32;
}
SimpleCounter :: struct { val: i32; }
@@ -32,8 +32,8 @@ impl Summable for Point {
// Phase 2: #inline protocol for dynamic dispatch
Adder :: protocol #inline {
add :: (n: i32);
value :: () -> i32;
add :: (self: *Self, n: i32);
value :: (self: *Self) -> i32;
}
Accumulator :: struct {

View File

@@ -24,7 +24,7 @@ Keycode :: enum { unknown; escape; enter; }
KeyData :: struct { key: Keycode; }
Event :: enum { none; key_up: KeyData; }
Plat :: protocol { one_event :: () -> Event; }
Plat :: protocol { one_event :: (self: *Self) -> Event; }
Impl :: struct { dummy: i64; }
impl Plat for Impl {

View File

@@ -0,0 +1,47 @@
// Protocol methods declare their receiver EXPLICITLY as the first parameter —
// `self: *Self` (or `self: Self`) — matching the `impl` method signature. This
// is required (the old implicit-receiver form is a parse error). The receiver
// annotation is validated then erased, so dispatch and call sites are unchanged:
// a method with N declared extra args is still called with N args.
#import "modules/std.sx";
Size :: struct { w: i32; h: i32; }
// `self: *Self` receiver; one no-arg method and one with extra args.
Measurable :: protocol #inline {
measure :: (self: *Self) -> Size;
scaled :: (self: *Self, factor: i32) -> Size;
}
// `self: Self` (by-value) receiver is also accepted.
Named :: protocol {
name :: (self: Self) -> string;
}
Widget :: struct { base: i32; }
impl Measurable for Widget {
measure :: (self: *Widget) -> Size { return Size.{ w = self.base, h = self.base * 2 }; }
scaled :: (self: *Widget, factor: i32) -> Size {
return Size.{ w = self.base * factor, h = self.base * 2 * factor };
}
}
impl Named for Widget {
name :: (self: Widget) -> string { return "widget"; }
}
main :: () -> i32 {
w : Widget = .{ base = 5 };
p : *Widget = @w;
m : Measurable = xx p;
s := m.measure(); // 0 extra args
s2 := m.scaled(3); // 1 extra arg
print("measure={}x{}\n", s.w, s.h); // 5x10
print("scaled={}x{}\n", s2.w, s2.h); // 15x30
n : Named = xx p;
print("name={}\n", n.name()); // widget
return 0;
}

View File

@@ -24,7 +24,7 @@
// User-defined parameterised protocol + an impl, so has_impl can
// confirm parameterised matching works with a known-true case.
Wrap :: protocol(Target: Type) {
wrap :: () -> Target;
wrap :: (self: *Self) -> Target;
}
impl Wrap(i64) for i32 {

View File

@@ -9,7 +9,7 @@
#import "modules/std.sx";
Greeter :: protocol {
greet :: () -> i64;
greet :: (self: *Self) -> i64;
}
Dog :: struct { age: i64; }

View File

@@ -10,7 +10,7 @@
#import "modules/std.sx";
Box :: protocol(T: Type) {
get :: () -> T;
get :: (self: *Self) -> T;
}
IntCell :: struct { v: i64; }

View File

@@ -7,7 +7,7 @@
#import "modules/std.sx";
Box :: protocol(T: Type) {
get :: () -> T;
get :: (self: *Self) -> T;
}
IntCell :: struct { v: i64; }
impl Box(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; }

View File

@@ -8,7 +8,7 @@
#import "modules/std.sx";
Box :: protocol(T: Type) {
get :: () -> T;
get :: (self: *Self) -> T;
}
IntCell :: struct { v: i64; }

View File

@@ -6,7 +6,7 @@
#import "modules/std.sx";
Box :: protocol(T: Type) {
get :: () -> i64;
get :: (self: *Self) -> i64;
}
IntCell :: struct { v: i64; }
Dbl :: struct { n: i64; }

View File

@@ -6,7 +6,7 @@
#import "modules/std.sx";
Box :: protocol(T: Type) {
get :: () -> T;
get :: (self: *Self) -> T;
}
IntCell :: struct { v: i64; }
StrCell :: struct { s: string; }

View File

@@ -7,7 +7,7 @@
#import "modules/std.sx";
Box :: protocol(T: Type) {
get :: () -> T;
get :: (self: *Self) -> T;
}
IntCell :: struct { v: i64; }

View File

@@ -9,7 +9,7 @@
#import "modules/std.sx";
Show :: protocol { show :: () -> string; }
Show :: protocol { show :: (self: *Self) -> string; }
A :: struct { x: i64; }
B :: struct { s: string; }
impl Show for A { show :: (self: *A) -> string => "A"; }

View File

@@ -5,7 +5,7 @@
#import "modules/std.sx";
Show :: protocol { show :: () -> string; }
Show :: protocol { show :: (self: *Self) -> string; }
A :: struct {}
impl Show for A { show :: (self: *A) -> string => "A"; }

View File

@@ -5,7 +5,7 @@
#import "modules/std.sx";
Show :: protocol { show :: () -> string; }
Show :: protocol { show :: (self: *Self) -> string; }
A :: struct {}
B :: struct { s: string; }
impl Show for A { show :: (self: *A) -> string => "A"; }

View File

@@ -7,7 +7,7 @@
#import "modules/std.sx";
VL :: protocol(T: Type) { get :: () -> T; }
VL :: protocol(T: Type) { get :: (self: *Self) -> T; }
IntCell :: struct { v: i64; }
StrCell :: struct { s: string; }
impl VL(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; }

View File

@@ -6,7 +6,7 @@
#import "modules/std.sx";
VL :: protocol(T: Type) { get :: () -> T; }
VL :: protocol(T: Type) { get :: (self: *Self) -> T; }
IntCell :: struct { v: i64; }
impl VL(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; }

View File

@@ -5,7 +5,7 @@
#import "modules/std.sx";
VL :: protocol(T: Type) { get :: () -> T; }
VL :: protocol(T: Type) { get :: (self: *Self) -> T; }
IntCell :: struct { v: i64; }
StrCell :: struct { s: string; }
impl VL(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; }

View File

@@ -4,7 +4,7 @@
#import "modules/std.sx";
VL :: protocol(T: Type) { get :: () -> T; }
VL :: protocol(T: Type) { get :: (self: *Self) -> T; }
IntCell :: struct { v: i64; }
impl VL(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; }

View File

@@ -12,7 +12,7 @@
#import "modules/std.sx";
VL :: protocol(T: Type) { get :: () -> T; }
VL :: protocol(T: Type) { get :: (self: *Self) -> T; }
IntCell :: struct { v: i64; }
impl VL(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; }

View File

@@ -11,7 +11,7 @@
#import "modules/std.sx";
Show :: protocol { show :: () -> string; }
Show :: protocol { show :: (self: *Self) -> string; }
IntBox :: struct { v: i64; }
StrBox :: struct { s: string; }
impl Show for IntBox { show :: (self: *IntBox) -> string { int_to_string(self.v) } }

View File

@@ -11,7 +11,7 @@
#import "modules/std.sx";
VL :: protocol(T: Type) { get :: () -> T; }
VL :: protocol(T: Type) { get :: (self: *Self) -> T; }
IntCell :: struct { v: i64; }
impl VL(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; }

View File

@@ -7,7 +7,7 @@
#import "modules/std.sx";
VL :: protocol(T: Type) { get :: () -> T; }
VL :: protocol(T: Type) { get :: (self: *Self) -> T; }
IntCell :: struct { v: i64; }
impl VL(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; }
Doubler :: struct { n: i64; }

View File

@@ -15,7 +15,7 @@
Box :: struct { m: i32; }
Provider :: protocol {
get :: () -> Box;
get :: (self: *Self) -> Box;
}
Holder :: struct { val: i32 = 7; }

View File

@@ -10,7 +10,7 @@
#import "0821-protocols-same-name-method-ambiguous/b.sx";
Provider :: protocol {
get :: () -> Box;
get :: (self: *Self) -> Box;
}
main :: () -> i32 {

View File

@@ -21,17 +21,17 @@ Holder :: struct { b: Box = ---; }
Provider :: protocol {
// discriminating wrapped/compound RETURNS
getp :: () -> *Box;
geto :: () -> ?Box;
gett :: () -> (Box, Box);
geta :: () -> [2]Box;
getp :: (self: *Self) -> *Box;
geto :: (self: *Self) -> ?Box;
gett :: (self: *Self) -> (Box, Box);
geta :: (self: *Self) -> [2]Box;
// routing-only wrapped/compound PARAMS
sump :: (p: *Box) -> i32;
sumo :: (o: ?Box) -> i32;
sums :: (s: []Box) -> i32;
suma :: (a: [2]Box) -> i32;
sumt :: (t: (Box, Box)) -> i32;
sumn :: (n: *?[]Box) -> i32;
sump :: (self: *Self, p: *Box) -> i32;
sumo :: (self: *Self, o: ?Box) -> i32;
sums :: (self: *Self, s: []Box) -> i32;
suma :: (self: *Self, a: [2]Box) -> i32;
sumt :: (self: *Self, t: (Box, Box)) -> i32;
sumn :: (self: *Self, n: *?[]Box) -> i32;
}
impl Provider for Holder {

View File

@@ -15,7 +15,7 @@
#import "0825-protocols-same-name-method-wrapped-ambiguous/b.sx";
Provider :: protocol {
getp :: () -> *Box;
getp :: (self: *Self) -> *Box;
}
main :: () -> i32 {

View File

@@ -15,7 +15,7 @@ Box :: struct { m: i32; }
Holder :: struct { n: i32; }
Tagged :: protocol(T: Type) {
tag :: () -> i32;
tag :: (self: *Self) -> i32;
}
impl Tagged(*Box) for Holder {

View File

@@ -22,7 +22,7 @@
Block :: struct { tag: i32; }
Sink :: protocol(T: Type) {
convert :: () -> T;
convert :: (self: *Self) -> T;
}
impl Sink(Block) for Closure(*Box, ..$args) -> $R {

View File

@@ -10,7 +10,7 @@ ProposedSize :: struct {
}
Sizable :: protocol {
size :: (proposal: ProposedSize) -> f32;
size :: (self: *Self, proposal: ProposedSize) -> f32;
}
Widget :: struct {}

View File

@@ -16,7 +16,7 @@ direct_size :: (proposal: ProposedSize) -> f32 {
}
Sizable :: protocol {
size :: (proposal: ProposedSize) -> f32;
size :: (self: *Self, proposal: ProposedSize) -> f32;
}
Widget :: struct {}

View File

@@ -6,7 +6,7 @@
#import "modules/std.sx";
Show :: protocol { show :: () -> string; }
Show :: protocol { show :: (self: *Self) -> string; }
IntBox :: struct { v: i64; }
impl Show for IntBox { show :: (self: *IntBox) -> string { int_to_string(self.v) } }

View File

@@ -14,7 +14,7 @@ Foo :: struct {
weird :: (self: *Foo) -> $T { 0 }
}
Show2 :: protocol { show2 :: () -> string; }
Show2 :: protocol { show2 :: (self: *Self) -> string; }
IntBox :: struct { v: i64; }
impl Show2 for IntBox {
show2 :: (self: *IntBox) -> string { "x" }

View File

@@ -0,0 +1,12 @@
// A protocol method that omits the explicit receiver (the old implicit form)
// is now a parse error — the receiver `self: *Self`/`self: Self` is required as
// the first parameter. This guards the diagnostic.
#import "modules/std.sx";
Show :: protocol {
show :: () -> string; // ERROR: missing `self` receiver
}
main :: () -> i32 {
return 0;
}

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,3 @@
measure=5x10
scaled=15x30
name=widget

View File

@@ -0,0 +1,5 @@
error: protocol method must declare its receiver as the first parameter: `self: *Self` (or `self: Self`)
--> examples/1190-diagnostics-protocol-missing-receiver.sx:7:16
|
7 | show :: () -> string; // ERROR: missing `self` receiver
| ^^

View File

@@ -11,47 +11,47 @@ GPU :: protocol {
// Bind the GPU to a backend-specific render target (e.g. a
// CAMetalLayer on iOS). pixel_w/pixel_h are the drawable's pixel
// dimensions; call resize when they change.
init :: (target: *void, pixel_w: i32, pixel_h: i32) -> bool;
shutdown :: ();
init :: (self: *Self, target: *void, pixel_w: i32, pixel_h: i32) -> bool;
shutdown :: (self: *Self);
resize :: (pixel_w: i32, pixel_h: i32);
resize :: (self: *Self, pixel_w: i32, pixel_h: i32);
begin_frame :: (clear: ClearColor) -> bool;
begin_frame :: (self: *Self, clear: ClearColor) -> bool;
// target_time is the host clock time at which the drawable should be
// presented (units match the platform's CADisplayLink.targetTimestamp
// on Apple). Metal forwards it to presentDrawable:atTime: to cap the
// pipeline at one frame so the inset slide lands on the same vsync as
// UIKit's keyboard view. GL backends ignore it.
end_frame :: (target_time: f64);
end_frame :: (self: *Self, target_time: f64);
create_shader :: (vsrc: string, fsrc: string) -> ShaderHandle;
create_buffer :: (size_bytes: i64) -> BufferHandle;
update_buffer :: (buf: BufferHandle, data: *void, size_bytes: i64);
create_shader :: (self: *Self, vsrc: string, fsrc: string) -> ShaderHandle;
create_buffer :: (self: *Self, size_bytes: i64) -> BufferHandle;
update_buffer :: (self: *Self, buf: BufferHandle, data: *void, size_bytes: i64);
// Sub-buffer write at a byte offset. Required for Metal where re-using
// the same buffer slice across multiple draws in a single command
// encoder is a race: the GPU executes draws asynchronously and reads
// shared-storage buffer contents at execution time, so the LAST writer
// wins if every flush targets offset 0. Renderers that issue more than
// one draw per frame must advance their write offset between flushes.
update_buffer_at :: (buf: BufferHandle, data: *void, size_bytes: i64, byte_offset: i64);
create_texture :: (w: i32, h: i32, format: TextureFormat, pixels: *void) -> TextureHandle;
update_texture_region :: (tex: TextureHandle, x: i32, y: i32, w: i32, h: i32, pixels: *void);
update_buffer_at :: (self: *Self, buf: BufferHandle, data: *void, size_bytes: i64, byte_offset: i64);
create_texture :: (self: *Self, w: i32, h: i32, format: TextureFormat, pixels: *void) -> TextureHandle;
update_texture_region :: (self: *Self, tex: TextureHandle, x: i32, y: i32, w: i32, h: i32, pixels: *void);
// Release a GPU resource. Implementations release the backing object and
// null the slot so the handle becomes inert. Calling with handle 0 or
// an already-destroyed handle is a no-op. Handles are not re-used; the
// backing List entry stays at its index with a null sentinel.
destroy_shader :: (sh: ShaderHandle);
destroy_buffer :: (buf: BufferHandle);
destroy_texture :: (tex: TextureHandle);
destroy_shader :: (self: *Self, sh: ShaderHandle);
destroy_buffer :: (self: *Self, buf: BufferHandle);
destroy_texture :: (self: *Self, tex: TextureHandle);
set_shader :: (sh: ShaderHandle);
set_vertex_buffer :: (buf: BufferHandle);
set_texture :: (slot: u32, tex: TextureHandle);
set_vertex_constants :: (slot: u32, data: *void, size_bytes: i64);
set_scissor :: (x: i32, y: i32, w: i32, h: i32);
disable_scissor :: ();
set_shader :: (self: *Self, sh: ShaderHandle);
set_vertex_buffer :: (self: *Self, buf: BufferHandle);
set_texture :: (self: *Self, slot: u32, tex: TextureHandle);
set_vertex_constants :: (self: *Self, slot: u32, data: *void, size_bytes: i64);
set_scissor :: (self: *Self, x: i32, y: i32, w: i32, h: i32);
disable_scissor :: (self: *Self);
draw_triangles :: (vertex_offset: i32, vertex_count: i32);
draw_triangles :: (self: *Self, vertex_offset: i32, vertex_count: i32);
}

View File

@@ -4,24 +4,24 @@
#import "modules/platform/types.sx";
Platform :: protocol {
init :: (title: [:0]u8, w: i32, h: i32) -> bool;
init :: (self: *Self, title: [:0]u8, w: i32, h: i32) -> bool;
run_frame_loop :: (frame_fn: Closure());
run_frame_loop :: (self: *Self, frame_fn: Closure());
poll_events :: () -> []Event;
poll_events :: (self: *Self) -> []Event;
begin_frame :: () -> FrameContext;
end_frame :: ();
begin_frame :: (self: *Self) -> FrameContext;
end_frame :: (self: *Self);
safe_insets :: () -> EdgeInsets;
keyboard :: () -> KeyboardState;
show_keyboard :: ();
hide_keyboard :: ();
safe_insets :: (self: *Self) -> EdgeInsets;
keyboard :: (self: *Self) -> KeyboardState;
show_keyboard :: (self: *Self);
hide_keyboard :: (self: *Self);
// Request the run loop to stop. On iOS/Android this is a no-op
// (mobile apps don't quit on user request); on SDL it tears down the
// `while !quit` loop.
stop :: ();
stop :: (self: *Self);
shutdown :: ();
shutdown :: (self: *Self);
}

View File

@@ -60,8 +60,8 @@ string :: []u8 #builtin;
// Bytes-level primitives carry the `_bytes` suffix so the typed
// helpers in std/mem.sx own the bare names (`alloc(T, n)`, `free(s)`).
Allocator :: protocol #inline {
alloc_bytes :: (size: i64) -> *void;
dealloc_bytes :: (ptr: *void);
alloc_bytes :: (self: *Self, size: i64) -> *void;
dealloc_bytes :: (self: *Self, ptr: *void);
}
// --- Io capability protocol (impls live in std/io.sx) ---
@@ -98,12 +98,12 @@ ParkToken :: struct {
}
Io :: protocol #inline {
spawn_raw :: (entry: *void, arg: *void, opts: SpawnOpts) -> *void;
suspend_raw :: (park: ParkToken) -> !;
ready :: (park: ParkToken);
poll :: (deadline_ms: i64) -> i64;
now_ms :: () -> i64;
arm_timer :: (deadline_ms: i64, park: ParkToken) -> *void;
spawn_raw :: (self: *Self, entry: *void, arg: *void, opts: SpawnOpts) -> *void;
suspend_raw :: (self: *Self, park: ParkToken) -> !;
ready :: (self: *Self, park: ParkToken);
poll :: (self: *Self, deadline_ms: i64) -> i64;
now_ms :: (self: *Self) -> i64;
arm_timer :: (self: *Self, deadline_ms: i64, park: ParkToken) -> *void;
}
// --- Context ---
@@ -127,5 +127,5 @@ Context :: struct {
// and emits a direct call. Compile-time only — no vtable, no runtime
// dispatch.
Into :: protocol(Target: Type) {
convert :: () -> Target;
convert :: (self: *Self) -> Target;
}

View File

@@ -4,7 +4,7 @@
// --- Lerpable protocol (inline — static dispatch, no vtable) ---
Lerpable :: protocol #inline {
lerp :: (b: Self, t: f32) -> Self;
lerp :: (self: *Self, b: Self, t: f32) -> Self;
}
// --- Easing Functions ---

View File

@@ -4,16 +4,16 @@
View :: protocol {
// Measure: given a size proposal, return desired size
size_that_fits :: (proposal: ProposedSize) -> Size;
size_that_fits :: (self: *Self, proposal: ProposedSize) -> Size;
// Place: position children within the given bounds
layout :: (bounds: Frame);
layout :: (self: *Self, bounds: Frame);
// Render: emit render nodes
render :: (ctx: *RenderContext, frame: Frame);
render :: (self: *Self, ctx: *RenderContext, frame: Frame);
// Event handling: return true if the event was consumed
handle_event :: (event: *Event, frame: Frame) -> bool;
handle_event :: (self: *Self, event: *Event, frame: Frame) -> bool;
}
// A child view with its computed frame (set during layout)

View File

@@ -184,7 +184,7 @@ function declaration, an `impl` method definition, or a `::` type declaration
(`i2 :: 5` and `i2 :: (n) { … }` are rejected just like `i2 := 5`). **Member-name
positions are exempt**: a struct *field*, a union *tag*, and a protocol
*method-signature* may be a bare reserved spelling (`struct { i2: i64 }`,
`union { u8: … }`, `protocol { i2 :: () -> i64 }`) — they are reached via `obj.name`,
`union { u8: … }`, `protocol { i2 :: (self: *Self) -> i64 }`) — they are reached via `obj.name`,
so they never mis-lower. The bare exemption covers only the identifier-classified
reserved names (`i1`..`i64`, `u1`..`u64`, `bool`, `string`, `void`, `usize`,
`isize`, `Any`); `f32` and `f64` are lexer keywords, so even in a member slot they
@@ -334,7 +334,7 @@ Closures capture by value. Bare functions auto-promote to closures when needed.
```sx
Drawable :: protocol {
draw :: (x: i32, y: i32);
draw :: (self: *Self, x: i32, y: i32); // receiver is explicit + required
}
impl Drawable for Circle {
@@ -345,11 +345,15 @@ shape : Drawable = xx my_circle; // type erasure via xx
shape.draw(10, 20); // dynamic dispatch
```
Every protocol method declares its receiver explicitly as the first parameter
(`self: *Self` or `self: Self`), matching the `impl` signature; the annotation is
erased before dispatch, so the call site is unchanged.
`#inline` protocols store function pointers directly (no vtable indirection):
```sx
Allocator :: protocol #inline {
alloc :: (size: i64) -> *void;
dealloc :: (ptr: *void);
alloc :: (self: *Self, size: i64) -> *void;
dealloc :: (self: *Self, ptr: *void);
}
```

View File

@@ -45,7 +45,7 @@ reserved spellings — `i1`..`i64`, `u1`..`u64`, `bool`, `string`, `cstring`, `v
member-name slots require an identifier token; a bare `f32` / `f64` is therefore
rejected at parse (`expected field name in struct`) even in a member position. Use
the backtick there too — `` struct { `f32: i64; } `` / `` union { `f64: … } `` /
`` protocol { `f32 :: () -> i64; } `` work as field / tag / method names.
`` protocol { `f32 :: (self: *Self) -> i64; } `` work as field / tag / method names.
```sx
i2 := 2.5; // ERROR: 'i2' is a reserved type name and cannot be used as an identifier
@@ -563,12 +563,12 @@ Protocols define a set of method signatures that types can implement. They enabl
#### Declaration
```sx
Allocator :: protocol #inline {
alloc :: (size: i64) -> *void;
dealloc :: (ptr: *void);
alloc :: (self: *Self, size: i64) -> *void;
dealloc :: (self: *Self, 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).
Protocol methods declare their receiver **explicitly** as the first parameter — `self: *Self` (or `self: Self`) — matching the corresponding `impl` method signature. This is **required**: a protocol method whose first parameter is not `self: *Self`/`self: Self` is a parse error. (It removes the old implicit-receiver ambiguity over whether the first listed parameter was the receiver or an extra argument.) The receiver annotation is validated then erased — the dispatch ABI is unchanged, so existing `impl` blocks and call sites are unaffected. The `#inline` modifier embeds function pointers directly in the protocol value (no vtable indirection).
#### `#inline` vs default layout
@@ -675,8 +675,8 @@ s : Sizable = xx @w; // identical to `xx w` — borrows w
Protocol methods can have bodies. `self` dispatches through the vtable (dynamic dispatch):
```sx
Writer :: protocol {
write :: (data: string) -> i64; // required
write_line :: (data: string) -> i64 { // default
write :: (self: *Self, data: string) -> i64; // required
write_line :: (self: *Self, data: string) -> i64 { // default
n := self.write(data);
n + self.write("\n");
}
@@ -688,7 +688,7 @@ Default methods are used unless overridden in the impl. Default methods calling
#### `Self` Type
`Self` is a contextual keyword in protocol declarations — resolves to the concrete type in impls:
```sx
Eq :: protocol { eq :: (other: Self) -> bool; }
Eq :: protocol { eq :: (self: *Self, other: Self) -> bool; }
impl Eq for Point {
eq :: (self: *Point, other: Point) -> bool {
@@ -755,7 +755,7 @@ in `modules/std.sx`:
```sx
Into :: protocol(Target: Type) {
convert :: () -> Target;
convert :: (self: *Self) -> Target;
}
```

View File

@@ -1223,6 +1223,27 @@ pub const Parser = struct {
}
try self.expect(.r_paren);
// Every protocol method must declare its receiver EXPLICITLY as the
// first parameter — `self: *Self` (or `self: Self`) — matching how
// `impl` methods and ordinary methods are written. This removes the
// old implicit-receiver ambiguity (was the first listed param the
// receiver, or an extra arg?). The receiver is validated and then
// stripped here, so downstream lowering sees only the EXTRA-arg
// params, exactly as it did under the implicit form.
if (param_names.items.len == 0 or !std.mem.eql(u8, param_names.items[0], "self")) {
return self.fail("protocol method must declare its receiver as the first parameter: `self: *Self` (or `self: Self`)");
}
{
const rtype = param_types.items[0];
const is_self_val = rtype.data == .type_expr and std.mem.eql(u8, rtype.data.type_expr.name, "Self");
const is_self_ptr = rtype.data == .pointer_type_expr and
rtype.data.pointer_type_expr.pointee_type.data == .type_expr and
std.mem.eql(u8, rtype.data.pointer_type_expr.pointee_type.data.type_expr.name, "Self");
if (!is_self_val and !is_self_ptr) {
return self.fail("protocol method receiver must be typed `*Self` or `Self`");
}
}
// Optional return type
var return_type: ?*Node = null;
if (self.current.tag == .arrow) {
@@ -1238,12 +1259,19 @@ pub const Parser = struct {
if (self.current.tag == .semicolon) self.advance();
}
// Strip the receiver (index 0) — the method's stored params are the
// extra args only.
const all_param_types = try param_types.toOwnedSlice(self.allocator);
const all_param_names = try param_names.toOwnedSlice(self.allocator);
const all_param_name_spans = try param_name_spans.toOwnedSlice(self.allocator);
const all_param_name_is_raw = try param_name_is_raw.toOwnedSlice(self.allocator);
try methods.append(self.allocator, .{
.name = method_name,
.params = try param_types.toOwnedSlice(self.allocator),
.param_names = try param_names.toOwnedSlice(self.allocator),
.param_name_spans = try param_name_spans.toOwnedSlice(self.allocator),
.param_name_is_raw = try param_name_is_raw.toOwnedSlice(self.allocator),
.params = all_param_types[1..],
.param_names = all_param_names[1..],
.param_name_spans = all_param_name_spans[1..],
.param_name_is_raw = all_param_name_is_raw[1..],
.return_type = return_type,
.default_body = default_body,
});

View File

@@ -2205,7 +2205,7 @@ test "sema: method-return slice + .ptr index + tagged-enum element" {
const source =
"Event :: enum { none; click: i64; }" ++
"Plat :: protocol { poll :: () -> []Event; }" ++
"Plat :: protocol { poll :: (self: *Self) -> []Event; }" ++
"go :: (p: *Plat) { evs := p.poll(); e := evs.ptr[0]; }";
var parser = parser_mod.Parser.init(alloc, source);
const root = try parser.parse();