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).
618 lines
14 KiB
Plaintext
618 lines
14 KiB
Plaintext
#import "modules/std.sx";
|
|
#import "modules/math";
|
|
#import "modules/build.sx";
|
|
#import "modules/std/test.sx";
|
|
pkg :: #import "tests/fixtures/testpkg";
|
|
|
|
Point :: struct { x, y: i32; }
|
|
|
|
Color :: enum { red; green; blue; }
|
|
|
|
Shape :: enum {
|
|
circle: f32;
|
|
rect: struct { w, h: f32; };
|
|
none;
|
|
}
|
|
|
|
Overlay :: union {
|
|
f: f32;
|
|
i: i32;
|
|
}
|
|
|
|
Vec2 :: union {
|
|
data: [2]f32;
|
|
struct { x, y: f32; };
|
|
}
|
|
|
|
Defaults :: struct {
|
|
a: i32;
|
|
b: i32 = 99;
|
|
c: i32 = ---;
|
|
}
|
|
|
|
OptNode :: struct {
|
|
value: i32;
|
|
next: ?i32;
|
|
}
|
|
|
|
OptInner :: struct { val: i32; }
|
|
|
|
OptOuter :: struct { inner: ?OptInner; }
|
|
|
|
MyFloat :: f64;
|
|
|
|
Perms :: enum flags { read; write; execute; }
|
|
|
|
Status :: enum u8 { ok; err; timeout; }
|
|
|
|
WindowFlags :: enum flags u32 { vsync :: 64; resizable :: 4; hidden :: 128; }
|
|
|
|
// --- Top-level functions ---
|
|
|
|
add :: (a: i32, b: i32) -> i32 { a + b }
|
|
|
|
mul :: (a: i32, b: i32) -> i32 { a * b }
|
|
|
|
identity :: (x: $T) -> T { x }
|
|
|
|
pair_add :: (a: $T, b: $U) -> i64 {
|
|
cast(i64) a + cast(i64) b
|
|
}
|
|
|
|
typed_sum :: (..args: []i32) -> i32 {
|
|
result := 0;
|
|
for args (it) { result = result + it; }
|
|
result
|
|
}
|
|
|
|
apply :: (f: (i32, i32) -> i32, x: i32, y: i32) -> i32 {
|
|
f(x, y)
|
|
}
|
|
|
|
void_return :: () {
|
|
return;
|
|
}
|
|
|
|
implicit_return :: (x: i32) -> i32 {
|
|
x * 2
|
|
}
|
|
|
|
early_return :: (x: i32) -> i32 {
|
|
if x > 10 { return 99; }
|
|
x
|
|
}
|
|
|
|
vec3 :: (x: f32, y: f32, z: f32) -> Vector(3, f32) {
|
|
.[x, y, z]
|
|
}
|
|
|
|
point_sum :: (p: Point) -> i32 { p.x + p.y }
|
|
|
|
// #run compile-time constants
|
|
|
|
// #run compile-time constants
|
|
CT_VAL :: #run add(10, 15);
|
|
|
|
CT_MUL :: #run mul(6, 7);
|
|
|
|
CT_CHAIN :: #run add(CT_VAL, 5);
|
|
|
|
// #run compile-time optional tests
|
|
|
|
// #run compile-time optional tests
|
|
ct_opt_coalesce :: () -> i32 {
|
|
x: ?i32 = 42;
|
|
y: ?i32 = null;
|
|
return (x ?? 0) + (y ?? 99);
|
|
}
|
|
|
|
ct_opt_unwrap :: () -> i32 {
|
|
x: ?i32 = 77;
|
|
return x!;
|
|
}
|
|
|
|
ct_opt_guard :: () -> i32 {
|
|
x: ?i32 = 10;
|
|
if x == null { return -1; }
|
|
return x;
|
|
}
|
|
|
|
CT_OPT_COALESCE :: #run ct_opt_coalesce();
|
|
|
|
CT_OPT_UNWRAP :: #run ct_opt_unwrap();
|
|
|
|
CT_OPT_GUARD :: #run ct_opt_guard();
|
|
|
|
// #insert helpers
|
|
|
|
// #insert helpers
|
|
gen_code :: () -> string {
|
|
return "print(\"insert-ok\\n\");";
|
|
}
|
|
|
|
gen_val :: () -> string {
|
|
return "print(\"insert-gen: {}\\n\", 42);";
|
|
}
|
|
|
|
// --- Error handling (failable functions: sets, raise/try/catch/or/onfail) ---
|
|
|
|
SmokeErr :: error { Empty, BadDigit, Overflow }
|
|
|
|
// value-carrying, named set: raise three tags or succeed
|
|
|
|
// value-carrying, named set: raise three tags or succeed
|
|
sm_parse :: (n: i32) -> (i32, !SmokeErr) {
|
|
if n < 0 { raise error.BadDigit; }
|
|
if n == 0 { raise error.Empty; }
|
|
if n > 99 { raise error.Overflow; }
|
|
return n * 2;
|
|
}
|
|
|
|
// pure failable, inferred set (ad-hoc tag minted into `!`)
|
|
|
|
// pure failable, inferred set (ad-hoc tag minted into `!`)
|
|
sm_check :: (ok: bool) -> ! {
|
|
if !ok { raise error.NotReady; }
|
|
return;
|
|
}
|
|
|
|
// multi-value, inferred set: `try` propagates; the SCC pass absorbs SmokeErr
|
|
|
|
// multi-value, inferred set: `try` propagates; the SCC pass absorbs SmokeErr
|
|
sm_pair :: (a: i32, b: i32) -> (i32, i32, !) {
|
|
x := try sm_parse(a);
|
|
y := try sm_parse(b);
|
|
return (x, y);
|
|
}
|
|
|
|
// `catch` block that diverges (logs the tag, then returns a fallback)
|
|
|
|
// `catch` block that diverges (logs the tag, then returns a fallback)
|
|
sm_or_default :: (n: i32) -> i32 {
|
|
return sm_parse(n) catch (e) {
|
|
print(" logged {}\n", e);
|
|
return -1;
|
|
};
|
|
}
|
|
|
|
// `onfail` + `defer` interleave: cleanup runs only on the error path
|
|
|
|
// `onfail` + `defer` interleave: cleanup runs only on the error path
|
|
sm_acquire :: (fail: bool) -> (i32, !) {
|
|
defer print(" smoke defer A\n");
|
|
onfail print(" smoke onfail B\n");
|
|
if fail { raise error.Acquire; }
|
|
return 7;
|
|
}
|
|
|
|
// `or`-chain: try a, fall to try b; propagate if both fail
|
|
|
|
// `or`-chain: try a, fall to try b; propagate if both fail
|
|
sm_first :: (a: i32, b: i32) -> (i32, !) {
|
|
v := try sm_parse(a) or try sm_parse(b);
|
|
return v;
|
|
}
|
|
|
|
// --- Extern function binding ---
|
|
|
|
// --- Extern function binding ---
|
|
libc :: #library "c";
|
|
|
|
c_abs :: (n: i32) -> i32 extern libc "abs";
|
|
|
|
// --- Protocol declarations (Phase 1: static dispatch only) ---
|
|
|
|
Counter :: protocol {
|
|
inc :: (self: *Self);
|
|
get :: (self: *Self) -> i32;
|
|
}
|
|
|
|
Summable :: protocol {
|
|
sum :: (self: *Self) -> i32;
|
|
}
|
|
|
|
SimpleCounter :: struct { val: i32; }
|
|
|
|
impl Counter for SimpleCounter {
|
|
inc :: (self: *SimpleCounter) { self.val += 1; }
|
|
get :: (self: *SimpleCounter) -> i32 { self.val }
|
|
}
|
|
|
|
impl Summable for Point {
|
|
sum :: (self: *Point) -> i32 { self.x + self.y }
|
|
}
|
|
|
|
// Phase 2: #inline protocol for dynamic dispatch
|
|
|
|
// Phase 2: #inline protocol for dynamic dispatch
|
|
Adder :: protocol #inline {
|
|
add :: (self: *Self, n: i32);
|
|
value :: (self: *Self) -> i32;
|
|
}
|
|
|
|
Accumulator :: struct {
|
|
total: i32;
|
|
}
|
|
|
|
impl Adder for Accumulator {
|
|
add :: (self: *Accumulator, n: i32) { self.total += n; }
|
|
value :: (self: *Accumulator) -> i32 { self.total }
|
|
}
|
|
|
|
Doubler :: struct { val: i32; }
|
|
|
|
impl Adder for Doubler {
|
|
add :: (self: *Doubler, n: i32) { self.val = self.val + n + n; }
|
|
value :: (self: *Doubler) -> i32 { self.val }
|
|
}
|
|
|
|
// Phase 4: default methods
|
|
|
|
// Phase 4: default methods
|
|
Repeater :: protocol {
|
|
say :: (self: *Self, msg: string);
|
|
say_twice :: (self: *Self, msg: string) {
|
|
self.say(msg);
|
|
self.say(msg);
|
|
}
|
|
}
|
|
|
|
Printer :: struct { count: i32; }
|
|
|
|
impl Repeater for Printer {
|
|
say :: (self: *Printer, msg: string) {
|
|
self.count += 1;
|
|
out(msg);
|
|
}
|
|
}
|
|
|
|
// P4 edge: Chained default→default calls
|
|
|
|
// P4 edge: Chained default→default calls
|
|
Chained :: protocol {
|
|
base :: (self: *Self, msg: string) -> i32;
|
|
wrap :: (self: *Self, msg: string) -> i32 {
|
|
self.base(msg) + 1
|
|
}
|
|
double_wrap :: (self: *Self, msg: string) -> i32 {
|
|
self.wrap(msg) + self.wrap(msg)
|
|
}
|
|
}
|
|
|
|
ChainImpl :: struct { val: i32; }
|
|
impl Chained for ChainImpl {
|
|
base :: (self: *ChainImpl, msg: string) -> i32 {
|
|
self.val += 1;
|
|
msg.len
|
|
}
|
|
}
|
|
|
|
// Phase 5: Self type
|
|
|
|
// Phase 5: Self type
|
|
Eq :: protocol {
|
|
eq :: (self: *Self, other: Self) -> bool;
|
|
}
|
|
|
|
impl Eq for Point {
|
|
eq :: (self: *Point, other: Point) -> bool {
|
|
self.x == other.x and self.y == other.y
|
|
}
|
|
}
|
|
|
|
Cloneable :: protocol {
|
|
clone :: (self: *Self) -> Self;
|
|
}
|
|
|
|
impl Cloneable for Point {
|
|
clone :: (self: *Point) -> Point {
|
|
Point.{ x = self.x, y = self.y }
|
|
}
|
|
}
|
|
|
|
impl Eq for i64 {
|
|
eq :: (self: *i64, other: i64) -> bool {
|
|
self.* == other
|
|
}
|
|
}
|
|
|
|
// Phase 6: Generic constraints
|
|
|
|
// Phase 6: Generic constraints
|
|
are_equal :: ($T: Type/Eq, a: T, b: T) -> bool {
|
|
a.eq(b)
|
|
}
|
|
|
|
Hashable :: protocol {
|
|
hash :: (self: *Self) -> i64;
|
|
}
|
|
|
|
impl Hashable for Point {
|
|
hash :: (self: *Point) -> i64 {
|
|
xx self.x * 31 + xx self.y
|
|
}
|
|
}
|
|
|
|
eq_and_hash :: ($T: Type/Eq/Hashable, a: T, b: T) -> bool {
|
|
if a.hash() != b.hash() { return false; }
|
|
a.eq(b)
|
|
}
|
|
|
|
// P6.4: inline constraint syntax ($T/Protocol)
|
|
|
|
// P6.4: inline constraint syntax ($T/Protocol)
|
|
sum_of_inline :: (a: $T/Summable, b: T) -> i32 {
|
|
a.sum() + b.sum()
|
|
}
|
|
|
|
// Phase 7: Generic struct impls
|
|
|
|
// Phase 7: Generic struct impls
|
|
Pair :: struct ($T: Type) {
|
|
a: T;
|
|
b: T;
|
|
}
|
|
|
|
impl Summable for Pair($T) {
|
|
sum :: (self: *Pair(T)) -> i32 {
|
|
xx self.a + xx self.b
|
|
}
|
|
}
|
|
|
|
// P6.5: Struct type param constraints
|
|
|
|
// P6.5: Struct type param constraints
|
|
SumBox :: struct ($T: Type/Summable) {
|
|
val: T;
|
|
}
|
|
|
|
// ============================================================
|
|
// Struct constants test
|
|
|
|
// ============================================================
|
|
// Struct constants test
|
|
Phys :: struct {
|
|
x, y: f32;
|
|
GRAVITY :f32: 9.81;
|
|
MAX_SPEED :: 100;
|
|
}
|
|
|
|
// Init block test struct
|
|
|
|
// Init block test struct
|
|
Builder :: struct {
|
|
total: i32;
|
|
count: i32;
|
|
|
|
add :: (self: *Builder, val: i32) {
|
|
self.total += val;
|
|
self.count += 1;
|
|
}
|
|
}
|
|
|
|
// Global variable for address-of test
|
|
|
|
// Global variable for address-of test
|
|
g_smoke_val : i32 = 42;
|
|
|
|
write_to_ptr :: (p: *i32) {
|
|
p.* = 99;
|
|
}
|
|
|
|
main :: () {
|
|
|
|
// ========================================================
|
|
// PROTOCOLS (Phase 1: static dispatch)
|
|
// ========================================================
|
|
print("=== Protocols ===\n");
|
|
|
|
// P1.1: Basic protocol + impl, direct call on concrete type
|
|
{
|
|
sc := SimpleCounter.{ val = 0 };
|
|
sc.inc();
|
|
sc.inc();
|
|
sc.inc();
|
|
print("P1.1: {}\n", sc.get());
|
|
}
|
|
|
|
// P1.2: impl in separate scope (retroactive conformance)
|
|
{
|
|
p := Point.{ x = 10, y = 20 };
|
|
print("P1.2: {}\n", p.sum());
|
|
}
|
|
|
|
// P2.1: #inline protocol — xx conversion + dynamic dispatch
|
|
{
|
|
acc := Accumulator.{ total = 0 };
|
|
a : Adder = xx @acc;
|
|
a.add(10);
|
|
a.add(20);
|
|
a.add(12);
|
|
print("P2.1: {}\n", a.value());
|
|
}
|
|
|
|
// P2.2: pass protocol value to function
|
|
{
|
|
use_adder :: (a: Adder, n: i32) -> i32 {
|
|
a.add(n);
|
|
a.value()
|
|
}
|
|
acc := Accumulator.{ total = 100 };
|
|
result := use_adder(xx @acc, 50);
|
|
print("P2.2: {}\n", result);
|
|
}
|
|
|
|
// P2.3: different impls through same protocol type
|
|
{
|
|
acc := Accumulator.{ total = 0 };
|
|
dbl := Doubler.{ val = 0 };
|
|
a1 : Adder = xx @acc;
|
|
a2 : Adder = xx @dbl;
|
|
a1.add(5);
|
|
a2.add(5);
|
|
print("P2.3: {} {}\n", a1.value(), a2.value());
|
|
}
|
|
|
|
// P3.1: vtable-pointer protocol (default, no #inline)
|
|
{
|
|
sc := SimpleCounter.{ val = 0 };
|
|
c : Counter = xx @sc;
|
|
c.inc();
|
|
c.inc();
|
|
c.inc();
|
|
c.inc();
|
|
c.inc();
|
|
print("P3.1: {}\n", c.get());
|
|
}
|
|
|
|
// P3.2: vtable protocol passed to function
|
|
{
|
|
use_counter :: (c: Counter) -> i32 {
|
|
c.inc();
|
|
c.inc();
|
|
c.get()
|
|
}
|
|
sc := SimpleCounter.{ val = 10 };
|
|
result := use_counter(xx @sc);
|
|
print("P3.2: {}\n", result);
|
|
}
|
|
|
|
// P4.1: default method calls required method (static dispatch)
|
|
{
|
|
pr := Printer.{ count = 0 };
|
|
pr.say_twice("hi ");
|
|
print("\nP4.1: {}\n", pr.count);
|
|
}
|
|
|
|
// P4.2: default method via dynamic dispatch (vtable)
|
|
{
|
|
pr := Printer.{ count = 0 };
|
|
r : Repeater = xx @pr;
|
|
r.say_twice("yo ");
|
|
print("\nP4.2: {}\n", pr.count);
|
|
}
|
|
|
|
// P4.3: chained default→default calls via vtable
|
|
{
|
|
ci := ChainImpl.{ val = 0 };
|
|
ch : Chained = xx @ci;
|
|
// double_wrap calls wrap twice, wrap calls base once each
|
|
// base("hi") returns 2 (len), wrap adds 1 → 3, double_wrap = 3 + 3 = 6
|
|
result := ch.double_wrap("hi");
|
|
// base was called 2 times (once per wrap call)
|
|
print("P4.3: {} {}\n", result, ci.val);
|
|
}
|
|
|
|
// P5.1: Self type in protocol — static dispatch
|
|
{
|
|
p1 := Point.{ x = 1, y = 2 };
|
|
p2 := Point.{ x = 1, y = 2 };
|
|
p3 := Point.{ x = 3, y = 4 };
|
|
print("P5.1: {} {}\n", p1.eq(p2), p1.eq(p3));
|
|
}
|
|
|
|
// P5.2: Self in return position
|
|
{
|
|
p := Point.{ x = 10, y = 20 };
|
|
p2 := p.clone();
|
|
print("P5.2: {} {}\n", p2.x, p2.y);
|
|
}
|
|
|
|
// P5.5: impl for primitive type
|
|
{
|
|
x := 42;
|
|
y := 42;
|
|
z := 99;
|
|
r1 := x.eq(y);
|
|
r2 := x.eq(z);
|
|
print("P5.5: {} {}\n", r1, r2);
|
|
}
|
|
|
|
// P5.3: Self with dynamic dispatch (erased to *void)
|
|
{
|
|
p1 := Point.{ x = 1, y = 2 };
|
|
p2 := Point.{ x = 1, y = 2 };
|
|
p3 := Point.{ x = 3, y = 4 };
|
|
e : Eq = xx p1;
|
|
print("P5.3: {} {}\n", e.eq(p2), e.eq(p3));
|
|
}
|
|
|
|
// P6.1: Single constraint — constrained generic function
|
|
{
|
|
p1 := Point.{ x = 1, y = 2 };
|
|
p2 := Point.{ x = 1, y = 2 };
|
|
p3 := Point.{ x = 3, y = 4 };
|
|
print("P6.1: {} {}\n", are_equal(p1, p2), are_equal(p1, p3));
|
|
}
|
|
|
|
// P6.2: Constraint with primitive type
|
|
{
|
|
print("P6.2: {} {}\n", are_equal(42, 42), are_equal(42, 99));
|
|
}
|
|
|
|
// P6.3: Multiple constraints
|
|
{
|
|
p1 := Point.{ x = 1, y = 2 };
|
|
p2 := Point.{ x = 1, y = 2 };
|
|
p3 := Point.{ x = 3, y = 4 };
|
|
print("P6.3: {} {}\n", eq_and_hash(p1, p2), eq_and_hash(p1, p3));
|
|
}
|
|
|
|
// P6.4: inline constraint syntax ($T/Protocol)
|
|
{
|
|
// sum_of_inline uses $T/Summable inline (not $T: Type/Summable)
|
|
p1 := Point.{ x = 10, y = 20 };
|
|
p2 := Point.{ x = 3, y = 7 };
|
|
print("P6.4: {}\n", sum_of_inline(p1, p2));
|
|
}
|
|
|
|
// P6.5: Struct type param constraints ($T: Type/Summable)
|
|
{
|
|
box := SumBox(Point).{ val = Point.{ x = 5, y = 15 } };
|
|
print("P6.5: {}\n", box.val.sum());
|
|
}
|
|
|
|
// P7.1: impl for generic struct
|
|
{
|
|
p := Pair(i32).{ a = 10, b = 20 };
|
|
print("P7.1: {}\n", p.sum());
|
|
}
|
|
|
|
// P7.2: generic struct impl with different type arg
|
|
{
|
|
p1 := Pair(i32).{ a = 3, b = 7 };
|
|
p2 := Pair(i64).{ a = 100, b = 200 };
|
|
print("P7.2: {} {}\n", p1.sum(), p2.sum());
|
|
}
|
|
|
|
// P2.4: xx in function return position (tested in standalone test_return.sx)
|
|
// Covered by: make_adder :: (acc: *Accumulator) -> Adder { xx acc; }
|
|
|
|
// P2.6: protocol values in arrays
|
|
{
|
|
acc := Accumulator.{ total = 0 };
|
|
dbl := Doubler.{ val = 0 };
|
|
adders : [2]Adder = .[xx @acc, xx @dbl];
|
|
i := 0;
|
|
while i < 2 {
|
|
adders[i].add(5);
|
|
i += 1;
|
|
}
|
|
print("P2.6: {} {}\n", acc.total, dbl.val);
|
|
}
|
|
|
|
// P2.7: xx on inline struct literal (no intermediate variable)
|
|
{
|
|
use_adder :: (a: Adder) -> i32 { a.add(10); a.value() }
|
|
result := use_adder(xx Accumulator.{ total = 5 });
|
|
print("P2.7: {}\n", result);
|
|
}
|
|
|
|
// P3.3: xx on inline struct literal with vtable protocol
|
|
{
|
|
use_counter :: (c: Counter) -> i32 { c.inc(); c.inc(); c.get() }
|
|
result := use_counter(xx SimpleCounter.{ val = 100 });
|
|
print("P3.3: {}\n", result);
|
|
}
|
|
}
|