diff --git a/examples/24-list.sx b/examples/24-list.sx index cbaae2b..17ac33b 100644 --- a/examples/24-list.sx +++ b/examples/24-list.sx @@ -3,7 +3,7 @@ main :: () { list : List(s32) = .{}; - append(list, 1); + list.append(1); list.append(3); list.append(4); diff --git a/examples/50-smoke.sx b/examples/50-smoke.sx index 2cd17f9..a11e59c 100644 --- a/examples/50-smoke.sx +++ b/examples/50-smoke.sx @@ -126,6 +126,156 @@ gen_val :: () -> string { libc :: #library "c"; c_abs :: (n: s32) -> s32 #foreign libc "abs"; +// --- Protocol declarations (Phase 1: static dispatch only) --- + +Counter :: protocol { + inc :: (); + get :: () -> s32; +} + +Summable :: protocol { + sum :: () -> s32; +} + +SimpleCounter :: struct { val: s32; } + +impl Counter for SimpleCounter { + inc :: (self: *SimpleCounter) { self.val += 1; } + get :: (self: *SimpleCounter) -> s32 { self.val; } +} + +impl Summable for Point { + sum :: (self: *Point) -> s32 { self.x + self.y; } +} + +// Phase 2: #inline protocol for dynamic dispatch +Adder :: protocol #inline { + add :: (n: s32); + value :: () -> s32; +} + +Accumulator :: struct { + total: s32; +} + +impl Adder for Accumulator { + add :: (self: *Accumulator, n: s32) { self.total += n; } + value :: (self: *Accumulator) -> s32 { self.total; } +} + +Doubler :: struct { val: s32; } + +impl Adder for Doubler { + add :: (self: *Doubler, n: s32) { self.val = self.val + n + n; } + value :: (self: *Doubler) -> s32 { self.val; } +} + +// Phase 4: default methods +Repeater :: protocol { + say :: (msg: string); + say_twice :: (msg: string) { + self.say(msg); + self.say(msg); + } +} + +Printer :: struct { count: s32; } + +impl Repeater for Printer { + say :: (self: *Printer, msg: string) { + self.count += 1; + out(msg); + } +} + +// P4 edge: Chained default→default calls +Chained :: protocol { + base :: (msg: string) -> s32; + wrap :: (msg: string) -> s32 { + self.base(msg) + 1; + } + double_wrap :: (msg: string) -> s32 { + self.wrap(msg) + self.wrap(msg); + } +} + +ChainImpl :: struct { val: s32; } +impl Chained for ChainImpl { + base :: (self: *ChainImpl, msg: string) -> s32 { + self.val += 1; + msg.len; + } +} + +// Phase 5: Self type +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; + } +} + +Cloneable :: protocol { + clone :: () -> Self; +} + +impl Cloneable for Point { + clone :: (self: *Point) -> Point { + Point.{ x = self.x, y = self.y }; + } +} + +impl Eq for s64 { + eq :: (self: *s64, other: s64) -> bool { + self.* == other; + } +} + +// Phase 6: Generic constraints +are_equal :: ($T: Type/Eq, a: T, b: T) -> bool { + a.eq(b); +} + +Hashable :: protocol { + hash :: () -> s64; +} + +impl Hashable for Point { + hash :: (self: *Point) -> s64 { + 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) +sum_of_inline :: (a: $T/Summable, b: T) -> s32 { + a.sum() + b.sum(); +} + +// Phase 7: Generic struct impls +Pair :: struct ($T: Type) { + a: T; + b: T; +} + +impl Summable for Pair($T) { + sum :: (self: *Pair(T)) -> s32 { + xx self.a + xx self.b; + } +} + +// P6.5: Struct type param constraints +SumBox :: struct ($T: Type/Summable) { + val: T; +} + // ============================================================ main :: { @@ -1186,13 +1336,13 @@ END; } // ======================================================== - // 20. UFCS RETURN TYPE INFERENCE + // 20. PIPE UFCS RETURN TYPE INFERENCE // ======================================================== print("=== 20. UFCS Return Type ===\n"); { p := Point.{3, 4}; print("direct: {}\n", point_sum(p)); - print("ufcs: {}\n", p.point_sum()); + print("ufcs: {}\n", p |> point_sum()); } // ======================================================== @@ -1319,7 +1469,7 @@ END; print("{}\n", zeroed.1); } - // --- UFCS Aliases --- + // --- UFCS Aliases & Pipe --- { print("=== UFCS Aliases ===\n"); @@ -1328,18 +1478,17 @@ END; print("{}\n", num_sum(40, 2)); // 42 — direct call print("{}\n", sum(40, 2)); // 42 — alias direct call - print("{}\n", 40.sum(2)); // 42 — UFCS via alias + print("{}\n", 40 |> sum(2)); // 42 — pipe UFCS via alias - // Tuple UFCS splatting - print("{}\n", (40, 2).sum()); // 42 — full splat - print("{}\n", (40,).sum(2)); // 42 — partial splat + print("{}\n", num_sum(40, 2)); // 42 — direct (was tuple full-splat) + print("{}\n", 40 |> sum(2)); // 42 — pipe (was tuple partial-splat) compute :: (a: s64, b: s64, c: s64, d: s64) -> s64 { a + b * c - d; } calc :: ufcs compute; - print("{}\n", (1, 2, 3, 4).calc()); // 1+2*3-4 = 3 - print("{}\n", (1, 2).calc(3, 4)); // same = 3 - print("{}\n", 1.calc(2, 3, 4)); // same = 3 + print("{}\n", compute(1, 2, 3, 4)); // 1+2*3-4 = 3 (was tuple full-splat) + print("{}\n", compute(1, 2, 3, 4)); // same = 3 (was tuple partial-splat) + print("{}\n", 1 |> calc(2, 3, 4)); // same = 3 — pipe UFCS // Tuple return type swap :: (a: s64, b: s64) -> (s64, s64) { (b, a); } @@ -1446,7 +1595,7 @@ END; // ── GPA ───────────────────────────────────────────────── { gpa_state : GPA = .{ alloc_count = 0 }; - gpa := gpa_create(@gpa_state); + gpa := gpa_state.create(); p1 := gpa.alloc(64); p2 := gpa.alloc(128); print("gpa allocs: {}\n", gpa_state.alloc_count); // gpa allocs: 2 @@ -1458,9 +1607,9 @@ END; // ── Arena backed by GPA (multi-chunk) ─────────────────── { gpa_state3 : GPA = .{ alloc_count = 0 }; - gpa3 := gpa_create(@gpa_state3); + gpa3 := gpa_state3.create(); arena_state : Arena = ---; - arena := arena_create(@arena_state, gpa3, 32); + arena := arena_state.create(gpa3, 32); // First chunk fits 80 usable bytes a1 := arena.alloc(40); a2 := arena.alloc(40); @@ -1476,11 +1625,11 @@ END; print("arena a1: {}\n", p1[0]); // arena a1: 42 print("arena a3: {}\n", p3[0]); // arena a3: 99 // Reset retains newest chunk - arena_reset(@arena_state); + arena_state.reset(); print("arena reset idx: {}\n", arena_state.end_index); // arena reset idx: 0 print("arena reset gpa: {}\n", gpa_state3.alloc_count);// arena reset gpa: 1 // Deinit frees all - arena_deinit(@arena_state); + arena_state.deinit(); print("arena deinit: {}\n", gpa_state3.alloc_count); // arena deinit: 0 } @@ -1488,14 +1637,14 @@ END; { stack_buf : [128]u8 = ---; buf_state : BufAlloc = ---; - bufalloc := buf_create(@buf_state, @stack_buf[0], 128); + bufalloc := buf_state.create(@stack_buf[0], 128); b1 := bufalloc.alloc(24); b2 := bufalloc.alloc(24); print("buf pos: {}\n", buf_state.pos); // buf pos: 48 b3 := bufalloc.alloc(200); b3_i : s64 = xx b3; print("buf overflow: {}\n", b3_i); // buf overflow: 0 - buf_reset(@buf_state); + buf_state.reset(); print("buf reset: {}\n", buf_state.pos); // buf reset: 0 } @@ -1775,12 +1924,12 @@ END; // --- closure calling convention --- { Env :: struct { n: s32; } - impl :: (env: *void, x: s32) -> s32 { + impl_fn :: (env: *void, x: s32) -> s32 { e : *Env = xx env; return x + e.n; } env := Env.{ n = 5 }; - fn_ptr : *void = xx impl; + fn_ptr : *void = xx impl_fn; env_ptr : *void = xx @env; c : Closure(s32) -> s32 = .{ fn_ptr = fn_ptr, env = env_ptr }; print("closure-call: {}\n", c(10)); @@ -1906,7 +2055,6 @@ END; print("closure-factory: {} {}\n", add5(100), add10(100)); // C5.A5: capture struct - Point :: struct { x: s32; y: s32; } origin := Point.{ x = 10, y = 20 }; f_st := closure(() { print("closure-struct: {} {}\n", origin.x, origin.y); @@ -1951,6 +2099,739 @@ END; btn2 := Btn.{ label = "Cancel", on_click = null }; if h := btn1.on_click { h(1); } if h := btn2.on_click { h(2); } else { print("opt-closure-btn: null\n"); } + + // C5.A6: capture pointer (shared mutable state) + count_a6 : s32 = 0; + p_a6 := @count_a6; + inc_fn := closure(() { p_a6.* += 1; }); + inc_fn(); inc_fn(); inc_fn(); + print("closure-ptr: {}\n", count_a6); + + // C5.A9: capture enum value (as s32 tag) + c_a9 : s32 = 2; // simulate enum tag + f_a9 := closure(() -> s32 => c_a9); + print("closure-enum: {}\n", f_a9()); + + // C5.C4: return string + tag_c4 := "INFO"; + f_c4 := closure((msg: string) -> string => format("[{}] {}", tag_c4, msg)); + print("closure-rstr: {}\n", f_c4("ok")); + + // C5.C5: return struct + off_c5 := Point.{ x = 10, y = 20 }; + f_c5 := closure((p: Point) -> Point => Point.{ x = p.x + off_c5.x, y = p.y + off_c5.y }); + res_c5 := f_c5(Point.{ x = 1, y = 2 }); + print("closure-rstruct: {} {}\n", res_c5.x, res_c5.y); + + // C5.G2: factory with multiple captures + make_linear :: (m: s32, b: s32) -> Closure(s32) -> s32 { + return closure((x: s32) -> s32 => m * x + b); + } + lin := make_linear(3, 7); + print("closure-linear: {}\n", lin(10)); + + // C5.G3: factory returning clamper + make_clamper :: (lo: s32, hi: s32) -> Closure(s32) -> s32 { + return closure((x: s32) -> s32 { + if x < lo { return lo; } + if x > hi { return hi; } + return x; + }); + } + clamp_fn := make_clamper(0, 255); + cv1 : s32 = xx -10; + cv2 : s32 = 100; + cv3 : s32 = 999; + print("closure-clamp: {} {} {}\n", clamp_fn(cv1), clamp_fn(cv2), clamp_fn(cv3)); + + // C5.H2: compose + compose :: (f_h2: Closure(s32) -> s32, g_h2: Closure(s32) -> s32) -> Closure(s32) -> s32 { + return closure((x: s32) -> s32 => f_h2(g_h2(x))); + } + one_h2 : s32 = 1; + two_h2 : s32 = 2; + add1_h2 := closure((x: s32) -> s32 => x + one_h2); + mul2_h2 := closure((x: s32) -> s32 => x * two_h2); + composed := compose(mul2_h2, add1_h2); + print("closure-compose2: {}\n", composed(5)); + + // C5.H3: chain of closures + ch_k1 : s32 = 1; + ch_k2 : s32 = 2; + ch_k10 : s32 = 10; + ch_a := closure((x: s32) -> s32 => x + ch_k1); + ch_b := closure((x: s32) -> s32 => ch_a(x) * ch_k2); + ch_c := closure((x: s32) -> s32 => ch_b(x) + ch_k10); + print("closure-chain: {}\n", ch_c(5)); + + // C5.D1: map + map_cl :: (arr: [*]s32, cnt: s64, f_map: Closure(s32) -> s32, result: [*]s32) { + i := 0; + while i < cnt { result[i] = f_map(arr[i]); i += 1; } + } + map_src : [5]s32 = .[1, 2, 3, 4, 5]; + map_dst : [5]s32 = .[0, 0, 0, 0, 0]; + factor_d1 : s32 = 3; + map_cl(xx @map_src, 5, closure((x: s32) -> s32 => x * factor_d1), xx @map_dst); + print("closure-map: {} {} {} {} {}\n", map_dst[0], map_dst[1], map_dst[2], map_dst[3], map_dst[4]); + + // C5.D2: filter + filter_cl :: (arr: [*]s32, cnt: s64, pred: Closure(s32) -> bool, result: [*]s32) -> s64 { + j := 0; + i := 0; + while i < cnt { + if pred(arr[i]) { result[j] = arr[i]; j += 1; } + i += 1; + } + return j; + } + min_val : s32 = 3; + filt_dst : [5]s32 = .[0, 0, 0, 0, 0]; + kept := filter_cl(xx @map_src, 5, closure((x: s32) -> bool => x >= min_val), xx @filt_dst); + print("closure-filter: {} [{} {} {}]\n", kept, filt_dst[0], filt_dst[1], filt_dst[2]); + + // C5.D4: sort comparator (bubble sort) + sort_cl :: (arr: [*]s32, cnt: s64, less: Closure(s32, s32) -> bool) { + i := 0; + while i < cnt { + j := 0; + while j < cnt - 1 - i { + if less(arr[j + 1], arr[j]) { + tmp := arr[j]; + arr[j] = arr[j + 1]; + arr[j + 1] = tmp; + } + j += 1; + } + i += 1; + } + } + sort_arr : [5]s32 = .[5, 3, 1, 4, 2]; + descending := true; + sort_cl(xx @sort_arr, 5, closure((a: s32, b: s32) -> bool { + if descending { return a > b; } + return a < b; + })); + print("closure-sort: {} {} {} {} {}\n", sort_arr[0], sort_arr[1], sort_arr[2], sort_arr[3], sort_arr[4]); + + // C5.D5: for_each with index + for_each_cl :: (arr: [*]s32, cnt: s64, f_fe: Closure(s32, s64) -> void) { + i : s64 = 0; + while i < cnt { f_fe(arr[i], i); i += 1; } + } + fe_label := "item"; + fe_arr : [3]s32 = .[10, 20, 30]; + for_each_cl(xx @fe_arr, 3, closure((val: s32, idx: s64) { + print("closure-fe: {} {}={}\n", fe_label, idx, val); + })); + + // C5.D6: find + find_cl :: (arr: [*]s32, cnt: s64, pred_f: Closure(s32) -> bool) -> s64 { + i : s64 = 0; + while i < cnt { + if pred_f(arr[i]) { return i; } + i += 1; + } + return -1; + } + target : s32 = 30; + found_idx := find_cl(xx @fe_arr, 3, closure((x: s32) -> bool => x == target)); + print("closure-find: {}\n", found_idx); + + // C5.D7: any + any_cl :: (arr: [*]s32, cnt: s64, pred_a: Closure(s32) -> bool) -> bool { + i : s64 = 0; + while i < cnt { + if pred_a(arr[i]) { return true; } + i += 1; + } + return false; + } + has_big := any_cl(xx @fe_arr, 3, closure((x: s32) -> bool => x > 100)); + has_20 := any_cl(xx @fe_arr, 3, closure((x: s32) -> bool => x == 20)); + print("closure-any: {} {}\n", has_big, has_20); + + // C5.E4: auto-promotion in struct field assignment + Widget :: struct { transform: Closure(s32) -> s32; } + negate_fn :: (x: s32) -> s32 { return 0 - x; } + w_e4 := Widget.{ transform = negate_fn }; + print("closure-struct-field: {}\n", w_e4.transform(5)); + + // C5.F1: single closure callback in struct + Button :: struct { + label: string; + on_press: Closure(s32) -> void; + } + btn_x2 := 99; + btn_cb := closure((id: s32) { + print("closure-btn: {} {}\n", id, btn_x2); + }); + btn3 := Button.{ label = "OK", on_press = btn_cb }; + btn3.on_press(1); + + // C5.J1: stateful counter via pointer capture + state_j1 : s32 = 0; + p_j1 := @state_j1; + inc_j1 := closure(() -> s32 { p_j1.* += 1; return p_j1.*; }); + print("closure-counter: {} {} {}\n", inc_j1(), inc_j1(), inc_j1()); + + // C5.J2: stateful accumulator + state_j2 : s32 = 100; + p_j2 := @state_j2; + acc_j2 := closure((x: s32) -> s32 { p_j2.* += x; return p_j2.*; }); + print("closure-acc: {} {}\n", acc_j2(5), acc_j2(10)); + + // C5.K2: block-body with local variables and loops + base_k2 : s32 = 100; + sum_fn := closure((items: [*]s32, cnt: s64) -> s32 { + total : s32 = 0; + i : s64 = 0; + while i < cnt { + total += items[i]; + i += 1; + } + return total + base_k2; + }); + k2_arr : [5]s32 = .[1, 2, 3, 4, 5]; + print("closure-loop: {}\n", sum_fn(xx @k2_arr, 5)); + + // C5.M3: reassigning a closure variable + n_m3 : s32 = 1; + f_m3 := closure((x: s32) -> s32 => x + n_m3); + print("closure-reassign: {}\n", f_m3(10)); + m_m3 : s32 = 2; + f_m3 = closure((x: s32) -> s32 => x * m_m3); + print("closure-reassign: {}\n", f_m3(10)); + + // C5.M6b: snapshot verified with struct capture + pt_m6 := Point.{ x = 5, y = 10 }; + f_m6 := closure(() -> s32 => pt_m6.x + pt_m6.y); + pt_m6 = Point.{ x = 99, y = 99 }; + print("closure-snapstruct: {}\n", f_m6()); + + // C5.M2: closure capturing auto-promoted closure + double_m2 :: (x: s32) -> s32 { return x * 2; } + base_m2 : Closure(s32) -> s32 = double_m2; + n_m2 : s32 = 1; + f_m2 := closure((x: s32) -> s32 => base_m2(x) + n_m2); + print("closure-cap-promoted: {}\n", f_m2(5)); + + // C5.M5: immediately invoked closure (via temp var) + n_m5 : s32 = 5; + iife := closure((x: s32) -> s32 => x + n_m5); + result_m5 := iife(10); + print("closure-iife: {}\n", result_m5); + + // C5.F2: optional callback (none) + Toggle :: struct { on_change: ?Closure(bool) -> void; } + t_f2 := Toggle.{ on_change = null }; + if h := t_f2.on_change { h(true); } else { print("closure-toggle: none\n"); } + + // C5.F3: optional callback (some) + t_f3_cb := closure((enabled: bool) { print("closure-toggle: {}\n", enabled); }); + t_f3 := Toggle.{ on_change = t_f3_cb }; + if h := t_f3.on_change { h(true); } + + // C5.F5: callback receiving caller context + Panel :: struct { + title: string; + on_resize: Closure(string, s32, s32) -> void; + } + p_f5_cb := closure((title: string, w: s32, h: s32) { + print("closure-panel: {} {}x{}\n", title, w, h); + }); + p_f5 := Panel.{ title = "main", on_resize = p_f5_cb }; + p_f5.on_resize(p_f5.title, 800, 600); + + // C5.E6: protocol value passed through multiple function calls + step3 :: (a: Allocator) -> *void { a.alloc(8); } + step2 :: (a: Allocator) -> *void { step3(a); } + step1 :: (a: Allocator) -> *void { step2(a); } + gpa_e6 : GPA = .{ alloc_count = 0 }; + a_e6 : Allocator = xx @gpa_e6; + ptr_e6 := step1(a_e6); + print("closure-chain-call: {}\n", ptr_e6 != null); + a_e6.dealloc(ptr_e6); + + // C5.I1: creating closures in a loop (each captures different value) + cl_arr : [5]Closure(s32) -> s32 = ---; + i_loop := 0; + while i_loop < 5 { + val_loop : s32 = xx (i_loop * 10); + cl_arr[i_loop] = closure((x: s32) -> s32 => x + val_loop); + i_loop += 1; + } + // I2: calling closures from array + tmp_cl := cl_arr[0]; print("closure-loop-0: {}\n", tmp_cl(1)); + tmp_cl = cl_arr[1]; print("closure-loop-1: {}\n", tmp_cl(1)); + tmp_cl = cl_arr[4]; print("closure-loop-4: {}\n", tmp_cl(1)); + + // C5.M4: closure in conditional expression (via temp var) + use_fast := true; + k_fast : s32 = 2; + k_slow : s32 = 10; + f_fast := closure((x: s32) -> s32 => x * k_fast); + f_slow := closure((x: s32) -> s32 => x + k_slow); + f_cond : Closure(s32) -> s32 = if use_fast then f_fast else f_slow; + print("closure-cond: {}\n", f_cond(5)); + + // C5.F4: multiple callbacks on one struct + Form :: struct { + on_submit: ?Closure() -> void; + on_cancel: ?Closure() -> void; + } + msg_f4 := "submitted"; + sub_cb := closure(() { print("closure-form: {}\n", msg_f4); }); + form_f4 := Form.{ on_submit = sub_cb, on_cancel = null }; + if h := form_f4.on_submit { h(); } + if h := form_f4.on_cancel { h(); } else { print("closure-form: no cancel\n"); } + + // C5.L3: auto-promoted closure env is null (no free needed) + double_l3 :: (x: s32) -> s32 { return x * 2; } + f_l3 : Closure(s32) -> s32 = double_l3; + print("closure-null-env: {}\n", f_l3.env == null); + + // C5.A7: capture slice (fat pointer like string) + sl_a7 : [3]s32 = .[10, 20, 30]; + ptr_a7 : [*]s32 = xx @sl_a7; + f_a7 := closure((i: s64) -> s32 => ptr_a7[i]); + print("closure-slice: {} {} {}\n", f_a7(0), f_a7(1), f_a7(2)); + + // C5.L1: arena bulk free (closures allocated on arena, freed in bulk) + gpa_l1 : GPA = .{ alloc_count = 0 }; + a_l1 : Allocator = xx @gpa_l1; + arena_l1 : Arena = ---; + arena_alloc := arena_l1.create(a_l1, 4096); + push Context.{ allocator = arena_alloc } { + n_l1 : s32 = 5; + f_l1 := closure((x: s32) -> s32 => x + n_l1); + print("closure-arena: {}\n", f_l1(10)); + } + arena_l1.deinit(); + + // C5.L2: GPA manual free (verify env alloc/dealloc) + gpa_l2 : GPA = .{ alloc_count = 0 }; + a_l2 : Allocator = xx @gpa_l2; + n_l2 : s32 = 7; + result_l2 : s32 = 0; + push Context.{ allocator = a_l2 } { + f_l2 := closure((x: s32) -> s32 => x + n_l2); + result_l2 = f_l2(10); + a_l2.dealloc(f_l2.env); + } + print("closure-gpa: {} allocs={}\n", result_l2, gpa_l2.alloc_count); + + // C5.A10: capture optional + val_a10 : ?s32 = 42; + f_a10 := closure(() -> s32 { + if v := val_a10 { return v; } + return 0; + }); + print("closure-opt: {}\n", f_a10()); + + // C5.C6: return optional + limit_c6 : s32 = 100; + f_c6 := closure((x: s32) -> ?s32 { + if x > limit_c6 { return null; } + return x; + }); + r1_c6 := f_c6(50); + r2_c6 := f_c6(200); + if v := r1_c6 { print("closure-ropt: {}\n", v); } + if v := r2_c6 { print("should-not-print\n"); } else { print("closure-ropt: none\n"); } + + // C5.M8: array of closures with mixed origins + double_m8 :: (x: s32) -> s32 { return x * 2; } + n_m8 : s32 = 10; + fns_m8 : [3]Closure(s32) -> s32 = ---; + fns_m8[0] = double_m8; // auto-promoted + fns_m8[1] = closure((x: s32) -> s32 => x + n_m8); // captured + fns_m8[2] = closure((x: s32) -> s32 => x * x); // no capture + tmp_m8 := fns_m8[0]; print("closure-mixed: {}\n", tmp_m8(5)); + tmp_m8 = fns_m8[1]; print("closure-mixed: {}\n", tmp_m8(5)); + tmp_m8 = fns_m8[2]; print("closure-mixed: {}\n", tmp_m8(5)); + + // C5.E1: independent closures from same factory (each has own env) + mk_e1 :: (n: s32) -> Closure(s32) -> s32 { + return closure((x: s32) -> s32 => x * n); + } + f1_e1 := mk_e1(2); + f2_e1 := mk_e1(3); + f3_e1 := mk_e1(4); + print("closure-factory-indep: {} {} {}\n", f1_e1(10), f2_e1(10), f3_e1(10)); + + // C5.E2: deep chain — closure capturing closure capturing closure + v_e2 : s32 = 1; + k2_e2 : s32 = 2; + k100_e2 : s32 = 100; + f0_e2 := closure((x: s32) -> s32 => x + v_e2); + f1_e2 := closure((x: s32) -> s32 => f0_e2(x) * k2_e2); + f2_e2 := closure((x: s32) -> s32 => f1_e2(x) + k100_e2); + print("closure-deep-chain: {}\n", f2_e2(10)); + + // C5.E3: many captures (stress env struct) + c1_e3 : s32 = 1; + c2_e3 : s32 = 2; + c3_e3 : s32 = 3; + c4_e3 : s32 = 4; + c5_e3 : s32 = 5; + c6_e3 : s32 = 6; + c7_e3 : s32 = 7; + c8_e3 : s32 = 8; + big_env := closure(() -> s32 => c1_e3 + c2_e3 + c3_e3 + c4_e3 + c5_e3 + c6_e3 + c7_e3 + c8_e3); + print("closure-8cap: {}\n", big_env()); + + // C5.E5: closure with many parameters (4 params) + multi_param := closure((a: s32, b: s32, c: s32, d: s32) -> s32 => a + b + c + d); + a_e5 : s32 = 1; b_e5 : s32 = 2; c_e5 : s32 = 3; d_e5 : s32 = 4; + print("closure-4param: {}\n", multi_param(a_e5, b_e5, c_e5, d_e5)); + + // C5.E7: two closures sharing the same captured pointer + shared : s32 = 0; + shared_p := @shared; + inc_shared := closure(() { shared_p.* += 1; }); + add5_shared := closure(() { shared_p.* += 5; }); + inc_shared(); + add5_shared(); + inc_shared(); + print("closure-shared-ptr: {}\n", shared); + + // C5.E8: closure with f64 arithmetic + pi_e8 : f64 = 3.14159; + area_fn := closure((r: f64) -> f64 => pi_e8 * r * r); + a_e8 := area_fn(10.0); + print("closure-f64: {}\n", a_e8 > 314.0); + + // C5.E9: zero-capture closure (env should be null, like auto-promoted) + no_cap := closure((x: s32) -> s32 => x * x); + print("closure-zerocap: {} {}\n", no_cap(7), no_cap.env == null); + + // C5.E10: closure capturing and calling struct method + pt_e10 := Point.{ x = 3, y = 4 }; + p_e10 := @pt_e10; + get_xy := closure(() -> s32 => p_e10.x + p_e10.y); + print("closure-struct-method: {}\n", get_xy()); + + // C5.E11: multiple closures from same factory with different captures + fns_e11 : [3]Closure(s32) -> s32 = ---; + i_e11 := 0; + while i_e11 < 3 { + multiplier : s32 = xx (i_e11 + 1); + fns_e11[i_e11] = closure((x: s32) -> s32 => x * multiplier); + i_e11 += 1; + } + t_e11 := fns_e11[0]; print("closure-multi-factory: {}\n", t_e11(10)); + t_e11 = fns_e11[1]; print("closure-multi-factory: {}\n", t_e11(10)); + t_e11 = fns_e11[2]; print("closure-multi-factory: {}\n", t_e11(10)); + + // C5.E12: closure capturing bool + flag_e12 := true; + check_fn := closure((x: s32) -> bool { + if flag_e12 { return x > 0; } + return x < 0; + }); + pos_e12 : s32 = 5; + neg_e12 : s32 = xx -3; + print("closure-bool-cap: {} {}\n", check_fn(pos_e12), check_fn(neg_e12)); + + // C5.E13: closure as argument to another closure + apply_fn := closure((f_app: Closure(s32) -> s32, val: s32) -> s32 => f_app(val)); + k_e13 : s32 = 100; + inner_fn := closure((x: s32) -> s32 => x + k_e13); + print("closure-as-arg: {}\n", apply_fn(inner_fn, 42)); + + // C5.E14: closure capturing string and formatting + prefix_e14 := "hello"; + greet_fn := closure((name: string) -> string => format("{} {}", prefix_e14, name)); + print("closure-strfmt: {}\n", greet_fn("world")); + + // C5.E15: reassigning shared pointer target between closure calls + val_e15 : s32 = 10; + p_e15 := @val_e15; + read_fn := closure(() -> s32 => p_e15.*); + print("closure-ptr-before: {}\n", read_fn()); + val_e15 = 42; + print("closure-ptr-after: {}\n", read_fn()); + + // C5.E16: closure returning negative value + off_e16 : s32 = 100; + neg_fn := closure((x: s32) -> s32 => x - off_e16); + val_e16 : s32 = 30; + print("closure-neg: {}\n", neg_fn(val_e16)); + + // C5.E17: closure with protocol value capture (#inline protocol) + gpa_e17 : GPA = .{ alloc_count = 0 }; + a_e17 : Allocator = xx @gpa_e17; + alloc_fn := closure((size: s64) -> *void => a_e17.alloc(size)); + ptr_e17 := alloc_fn(32); + print("closure-proto-cap: {}\n", ptr_e17 != null); + a_e17.dealloc(ptr_e17); + + // C5.E18: chained factory — compose two factories + make_scaler :: (factor: s32) -> Closure(s32) -> s32 { + return closure((x: s32) -> s32 => x * factor); + } + make_offset :: (off: s32) -> Closure(s32) -> s32 { + return closure((x: s32) -> s32 => x + off); + } + s_fn := make_scaler(3); + o_fn := make_offset(7); + // manually compose: scale then offset + print("closure-chain-factory: {}\n", o_fn(s_fn(10))); + + // C5.E19: closure in while loop condition helper + threshold : s32 = 50; + above_fn := closure((x: s32) -> bool => x >= threshold); + vals_e19 : [5]s32 = .[10, 30, 50, 70, 90]; + count_above : s32 = 0; + idx_e19 : s64 = 0; + while idx_e19 < 5 { + if above_fn(vals_e19[idx_e19]) { count_above += 1; } + idx_e19 += 1; + } + print("closure-while-cond: {}\n", count_above); + + // ---- Inferred closure parameter types ---- + + // CI.1: inferred params from typed variable + f_ci1 : Closure(s32, s32) -> s32 = closure((a, b) => a + b); + a_ci1 : s32 = 3; + b_ci1 : s32 = 4; + print("closure-infer: {}\n", f_ci1(a_ci1, b_ci1)); + + // CI.2: inferred params from function argument + apply_ci :: (f: Closure(s32) -> s32, x: s32) -> s32 { return f(x); } + k_ci : s32 = 10; + v_ci : s32 = 5; + print("closure-infer-arg: {}\n", apply_ci(closure((x) => x + k_ci), v_ci)); + + // CI.3: inferred with block body + h_ci : Closure(s32, s32) -> s32 = closure((a, b) { return a * b; }); + print("closure-infer-block: {}\n", h_ci(a_ci1, b_ci1)); + + // CI.4: inferred with captures + cap_ci : s32 = 100; + f_ci4 : Closure(s32) -> s32 = closure((x) => x + cap_ci); + print("closure-infer-cap: {}\n", f_ci4(v_ci)); + + // CI.5: inferred in factory return + mk_ci :: (n: s32) -> Closure(s32) -> s32 { return closure((x) => x * n); } + f_ci5 := mk_ci(7); + print("closure-infer-factory: {}\n", f_ci5(v_ci)); + + // CI.6: inferred with higher-order (closure taking closure) + compose_ci :: (f: Closure(s32) -> s32, g: Closure(s32) -> s32) -> Closure(s32) -> s32 { + return closure((x: s32) -> s32 => f(g(x))); + } + one_ci : s32 = 1; + two_ci : s32 = 2; + c_ci := compose_ci(closure((x) => x + one_ci), closure((x) => x * two_ci)); + print("closure-infer-compose: {}\n", c_ci(v_ci)); + + // CI.7: inferred void return + msg_ci := "infer-void"; + cb_ci : Closure(s32) -> void = closure((x) { print("closure-{}: {}\n", msg_ci, x); }); + cb_ci(42); + } + + // ======================================================== + // 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: s32) -> s32 { + 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) -> s32 { + 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(s32).{ a = 10, b = 20 }; + print("P7.1: {}\n", p.sum()); + } + + // P7.2: generic struct impl with different type arg + { + p1 := Pair(s32).{ a = 3, b = 7 }; + p2 := Pair(s64).{ 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); } print("=== DONE ===\n"); diff --git a/examples/modules/allocators.sx b/examples/modules/allocators.sx index 1cf3b84..2ac0127 100644 --- a/examples/modules/allocators.sx +++ b/examples/modules/allocators.sx @@ -1,47 +1,34 @@ #import "std.sx"; -// --- Allocator protocol --- +// --- Allocator protocol (inline: ctx + fn-ptrs, no vtable indirection) --- -Allocator :: struct { - ctx: *void; - alloc_fn: (*void, s64) -> *void; - free_fn: (*void, *void) -> void; +Allocator :: protocol #inline { + alloc :: (size: s64) -> *void; + dealloc :: (ptr: *void); } -allocator_alloc :: (a: Allocator, size: s64) -> *void { - a.alloc_fn(a.ctx, size); -} - -allocator_dealloc :: (a: Allocator, ptr: *void) { - a.free_fn(a.ctx, ptr); -} - -alloc :: ufcs allocator_alloc; -dealloc :: ufcs allocator_dealloc; - // --- GPA: general purpose allocator (malloc/free wrapper) --- GPA :: struct { alloc_count: s64; + + create :: (gpa: *GPA) -> Allocator { + xx gpa; + } } -gpa_alloc :: (ctx: *void, size: s64) -> *void { - gpa : *GPA = xx ctx; - gpa.alloc_count += 1; - malloc(size); +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); + } } -gpa_free :: (ctx: *void, ptr: *void) { - gpa : *GPA = xx ctx; - gpa.alloc_count -= 1; - free(ptr); -} - -gpa_create :: (gpa: *GPA) -> Allocator { - Allocator.{ ctx = gpa, alloc_fn = gpa_alloc, free_fn = gpa_free }; -} - -// --- Arena: multi-chunk bump allocator (Zig-inspired) --- +// --- Arena: multi-chunk bump allocator --- ArenaChunk :: struct { next: *ArenaChunk; @@ -52,73 +39,71 @@ Arena :: struct { first: *ArenaChunk; end_index: s64; parent: Allocator; -} -arena_add_chunk :: (a: *Arena, min_size: s64) { - prev_cap := if a.first != null then a.first.cap else 0; - needed := min_size + 16 + 16; - len := (prev_cap + needed) * 3 / 2; - raw := a.parent.alloc(len); - chunk : *ArenaChunk = xx raw; - chunk.next = a.first; - chunk.cap = len; - a.first = chunk; - a.end_index = 0; -} - -arena_alloc :: (ctx: *void, size: s64) -> *void { - a : *Arena = xx ctx; - aligned := (size + 7) & (0 - 8); - if a.first != null { - usable := a.first.cap - 16; - if a.end_index + aligned <= usable { - buf : [*]u8 = xx a.first; - ptr := @buf[16 + a.end_index]; - a.end_index = a.end_index + aligned; - return ptr; - } + add_chunk :: (a: *Arena, min_size: s64) { + prev_cap := if a.first != null then a.first.cap else 0; + needed := min_size + 16 + 16; + len := (prev_cap + needed) * 3 / 2; + raw := a.parent.alloc(len); + chunk : *ArenaChunk = xx raw; + chunk.next = a.first; + chunk.cap = len; + a.first = chunk; + a.end_index = 0; } - arena_add_chunk(a, aligned); - buf : [*]u8 = xx a.first; - ptr := @buf[16 + a.end_index]; - a.end_index = a.end_index + aligned; - ptr; -} -arena_free :: (ctx: *void, ptr: *void) { -} + create :: (a: *Arena, parent: Allocator, size: s64) -> Allocator { + a.first = null; + a.end_index = 0; + a.parent = parent; + a.add_chunk(size); + xx a; + } -arena_create :: (a: *Arena, parent: Allocator, size: s64) -> Allocator { - a.first = null; - a.end_index = 0; - a.parent = parent; - arena_add_chunk(a, size); - Allocator.{ ctx = a, alloc_fn = arena_alloc, free_fn = arena_free }; -} + reset :: (a: *Arena) { + if a.first != null { + it := a.first.next; + while it != null { + next := it.next; + a.parent.dealloc(it); + it = next; + } + a.first.next = null; + } + a.end_index = 0; + } -arena_reset :: (a: *Arena) { - // Keep first chunk (newest/largest), free the rest - if a.first != null { - it := a.first.next; + deinit :: (a: *Arena) { + it := a.first; while it != null { next := it.next; a.parent.dealloc(it); it = next; } - a.first.next = null; + a.first = null; + a.end_index = 0; } - a.end_index = 0; } -arena_deinit :: (a: *Arena) { - it := a.first; - while it != null { - next := it.next; - a.parent.dealloc(it); - it = next; +impl Allocator for Arena { + alloc :: (self: *Arena, size: s64) -> *void { + aligned := (size + 7) & (0 - 8); + if self.first != null { + usable := self.first.cap - 16; + if self.end_index + aligned <= usable { + buf : [*]u8 = xx self.first; + ptr := @buf[16 + self.end_index]; + self.end_index = self.end_index + aligned; + return ptr; + } + } + self.add_chunk(aligned); + buf : [*]u8 = xx self.first; + ptr := @buf[16 + self.end_index]; + self.end_index = self.end_index + aligned; + ptr; } - a.first = null; - a.end_index = 0; + dealloc :: (self: *Arena, ptr: *void) {} } // --- BufAlloc: bump allocator backed by a user-provided slice --- @@ -127,29 +112,28 @@ BufAlloc :: struct { buf: [*]u8; len: s64; pos: s64; -} -buf_alloc :: (ctx: *void, size: s64) -> *void { - b : *BufAlloc = xx ctx; - aligned := (size + 7) & (0 - 8); - if b.pos + aligned > b.len { - return null; + create :: (b: *BufAlloc, buf: [*]u8, len: s64) -> Allocator { + b.buf = buf; + b.len = len; + b.pos = 0; + xx b; + } + + reset :: (b: *BufAlloc) { + b.pos = 0; } - ptr := @b.buf[b.pos]; - b.pos = b.pos + aligned; - ptr; } -buf_free :: (ctx: *void, ptr: *void) { -} - -buf_create :: (b: *BufAlloc, buf: [*]u8, len: s64) -> Allocator { - b.buf = buf; - b.len = len; - b.pos = 0; - Allocator.{ ctx = b, alloc_fn = buf_alloc, free_fn = buf_free }; -} - -buf_reset :: (b: *BufAlloc) { - b.pos = 0; +impl Allocator for BufAlloc { + alloc :: (self: *BufAlloc, size: s64) -> *void { + aligned := (size + 7) & (0 - 8); + if self.pos + aligned > self.len { + return null; + } + ptr := @self.buf[self.pos]; + self.pos = self.pos + aligned; + ptr; + } + dealloc :: (self: *BufAlloc, ptr: *void) {} } diff --git a/examples/modules/std.sx b/examples/modules/std.sx index 7e6f9b8..68533a8 100644 --- a/examples/modules/std.sx +++ b/examples/modules/std.sx @@ -31,12 +31,8 @@ context : Context = ---; // --- Slice & string allocation --- -context_alloc :: (size: s64) -> *void { - if context.allocator.ctx != null then context.allocator.alloc(size) else malloc(size); -} - cstring :: (size: s64) -> string { - raw := context_alloc(size + 1); + raw := context.allocator.alloc(size + 1); memset(raw, 0, size + 1); s : string = ---; s.ptr = xx raw; @@ -45,7 +41,7 @@ cstring :: (size: s64) -> string { } alloc_slice :: ($T: Type, count: s64) -> []T { - raw := context_alloc(count * size_of(T)); + raw := context.allocator.alloc(count * size_of(T)); memset(raw, 0, count * size_of(T)); s : []T = ---; s.ptr = xx raw; @@ -337,19 +333,19 @@ List :: struct ($T: Type) { items: [*]T = null; len: s64 = 0; cap: s64 = 0; -} -append ::(list: *List($T), item: T) { - if list.len >= list.cap { - new_cap := if list.cap == 0 then 4 else list.cap * 2; - new_items : [*]T = xx malloc(new_cap * size_of(T)); - if list.len > 0 { - memcpy(new_items, list.items, list.len * size_of(T)); - free(list.items); + append :: (list: *List(T), item: T) { + if list.len >= list.cap { + new_cap := if list.cap == 0 then 4 else list.cap * 2; + new_items : [*]T = xx context.allocator.alloc(new_cap * size_of(T)); + if list.len > 0 { + memcpy(new_items, list.items, list.len * size_of(T)); + context.allocator.dealloc(list.items); + } + list.items = new_items; + list.cap = new_cap; } - list.items = new_items; - list.cap = new_cap; + list.items[list.len] = item; + list.len += 1; } - list.items[list.len] = item; - list.len += 1; } \ No newline at end of file diff --git a/specs.md b/specs.md index a1c3994..d921d84 100644 --- a/specs.md +++ b/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`). diff --git a/src/ast.zig b/src/ast.zig index 47105e6..71802a8 100644 --- a/src/ast.zig +++ b/src/ast.zig @@ -64,6 +64,7 @@ pub const Node = struct { break_expr: void, continue_expr: void, undef_literal: void, + inferred_type: void, builtin_expr: void, foreign_expr: ForeignExpr, library_decl: LibraryDecl, @@ -73,6 +74,8 @@ pub const Node = struct { tuple_literal: TupleLiteral, ufcs_alias: UfcsAlias, c_import_decl: CImportDecl, + protocol_decl: ProtocolDecl, + impl_block: ImplBlock, pub fn declName(self: Data) ?[]const u8 { return switch (self) { @@ -85,6 +88,7 @@ pub const Node = struct { .namespace_decl => |d| d.name, .ufcs_alias => |d| d.name, .c_import_decl => |d| d.name, + .protocol_decl => |d| d.name, else => null, }; } @@ -274,6 +278,7 @@ pub const UnionDecl = struct { pub const StructTypeParam = struct { name: []const u8, // e.g. "N" or "T" (without $) constraint: *Node, // type_expr: "u32" for value param, "Type" for type param + protocol_constraints: []const []const u8 = &.{}, // e.g. ["Eq", "Hashable"] for $T/Eq/Hashable }; pub const UsingEntry = struct { @@ -288,6 +293,7 @@ pub const StructDecl = struct { field_defaults: []const ?*Node, // default value per field, null if none type_params: []const StructTypeParam = &.{}, using_entries: []const UsingEntry = &.{}, + methods: []const *Node = &.{}, // fn_decl nodes for struct methods }; pub const StructFieldInit = struct { @@ -311,6 +317,7 @@ pub const Lambda = struct { pub const TypeExpr = struct { name: []const u8, is_generic: bool = false, + protocol_constraints: []const []const u8 = &.{}, // e.g. ["Eq", "Hashable"] for $T/Eq/Hashable }; pub const DeferStmt = struct { @@ -465,3 +472,24 @@ pub const CImportDecl = struct { name: ?[]const u8 = null, bitcode_paths: []const []const u8 = &.{}, // populated during import resolution }; + +pub const ProtocolMethodDecl = struct { + name: []const u8, + params: []const *Node, // type_expr nodes for parameter types (excluding implicit self) + param_names: []const []const u8, // parameter names (excluding implicit self) + return_type: ?*Node, // null = void return + default_body: ?*Node, // null = required method, non-null = default implementation +}; + +pub const ProtocolDecl = struct { + name: []const u8, + methods: []const ProtocolMethodDecl, + is_inline: bool = false, // #inline — embedded fn ptrs instead of vtable pointer +}; + +pub const ImplBlock = struct { + protocol_name: []const u8, + target_type: []const u8, + target_type_params: []const StructTypeParam = &.{}, // for `impl P for List($T)` + methods: []const *Node, // fn_decl nodes +}; diff --git a/src/codegen.zig b/src/codegen.zig index 88809b7..a66740a 100644 --- a/src/codegen.zig +++ b/src/codegen.zig @@ -138,6 +138,8 @@ pub const CodeGen = struct { current_function: c.LLVMValueRef, // Return type of the current function being generated current_return_type: Type = .void_type, + // Expected closure type for inferring untyped closure params + closure_expected_type: ?types.Type.ClosureTypeInfo = null, // Scope stack: each entry records shadowed names and deferred expressions for one scope scope_stack: std.ArrayList(Scope), // Compile-time globals: maps name to global variable info for #run results @@ -213,6 +215,12 @@ pub const CodeGen = struct { ufcs_aliases: std.StringHashMap([]const u8), // Closure thunk cache: original function name → thunk LLVM function (dedup) closure_thunks: std.StringHashMap(c.LLVMValueRef), + // Protocol declarations: maps protocol name → ProtocolDecl AST node + protocol_decls: std.StringHashMap(ast.ProtocolDecl), + // Impl registrations: maps "ProtoName\x00TypeName" → ImplBlock AST node + impl_blocks: std.StringHashMap(ast.ImplBlock), + // Protocol thunks: maps "ProtoName\x00TypeName" → array of thunk LLVM functions (one per protocol method, in order) + protocol_thunks: std.StringHashMap([]const c.LLVMValueRef), // Target configuration (triple, cpu, opt level, lib paths, linker) target_config: TargetConfig = .{}, // Cached primitive LLVM types (initialized once in init(), avoids repeated FFI calls) @@ -432,6 +440,9 @@ pub const CodeGen = struct { .tuple_alloca_types = std.AutoHashMap(usize, Type).init(allocator), .ufcs_aliases = std.StringHashMap([]const u8).init(allocator), .closure_thunks = std.StringHashMap(c.LLVMValueRef).init(allocator), + .protocol_decls = std.StringHashMap(ast.ProtocolDecl).init(allocator), + .impl_blocks = std.StringHashMap(ast.ImplBlock).init(allocator), + .protocol_thunks = std.StringHashMap([]const c.LLVMValueRef).init(allocator), .target_config = target_config, .cached_i1 = c.LLVMInt1TypeInContext(ctx), .cached_i8 = c.LLVMInt8TypeInContext(ctx), @@ -603,12 +614,15 @@ pub const CodeGen = struct { .pointer_type, .many_pointer_type, .function_type => self.ptrType(), .closure_type => self.getClosureStructType(), .optional_type => |info| { + // ?Closure(...) → same layout as Closure { ptr, ptr } — fn_ptr null = none + if (std.mem.startsWith(u8, info.child_name, "Closure(")) { + return self.getClosureStructType(); + } // ?*T, ?[*]T, ?fn → bare pointer (null = none) const child_type = self.resolveTypeFromName(info.child_name) orelse unreachable; if (child_type.isPointer() or child_type.isManyPointer() or child_type.isFunctionType()) { return self.ptrType(); } - // ?Closure → same layout as Closure { ptr, ptr } — fn_ptr null = none if (child_type.isClosureType()) { return self.getClosureStructType(); } @@ -1364,6 +1378,12 @@ pub const CodeGen = struct { .ufcs_alias => |ua| { try self.ufcs_aliases.put(ua.name, ua.target); }, + .protocol_decl => |pd| { + try self.registerProtocolDecl(pd); + }, + .impl_block => |ib| { + try self.registerImplBlock(ib); + }, else => {}, } } @@ -1391,6 +1411,18 @@ pub const CodeGen = struct { }, .namespace_decl => |ns| { try self.genNamespaceBodies(ns); + // Generate struct method bodies within namespace + for (ns.decls) |nd| { + if (nd.data == .struct_decl) { + try self.genStructMethodBodies(nd.data.struct_decl); + } + } + }, + .struct_decl => |sd| { + try self.genStructMethodBodies(sd); + }, + .impl_block => |ib| { + try self.genImplMethodBodies(ib); }, else => {}, } @@ -1724,6 +1756,14 @@ pub const CodeGen = struct { // Check type parameter bindings (during generic instantiation) if (tn.data == .type_expr or tn.data == .identifier) { const name = if (tn.data == .type_expr) tn.data.type_expr.name else tn.data.identifier.name; + // Self type: resolves to concrete type when inside impl, or *void when in protocol decl context + if (std.mem.eql(u8, name, "Self")) { + if (self.current_namespace) |ns| { + return .{ .struct_type = ns }; + } + // In protocol context (no namespace): Self erased to *void for dynamic dispatch + return .{ .pointer_type = .{ .pointee_name = "void" } }; + } // Try primitive type name first if (Type.fromName(name)) |t| return t; if (self.type_param_bindings) |bindings| { @@ -1814,6 +1854,21 @@ pub const CodeGen = struct { } } + // Check protocol constraints on struct type params: $T: Type/Eq/Hashable + for (sd.type_params) |tp| { + if (tp.protocol_constraints.len > 0) { + if (type_bindings.get(tp.name)) |bound_ty| { + const type_name = bound_ty.toName() orelse "unknown"; + for (tp.protocol_constraints) |proto_name| { + const key = try std.fmt.allocPrint(self.allocator, "{s}\x00{s}", .{ proto_name, type_name }); + if (!self.impl_blocks.contains(key)) { + return self.emitErrorFmt("{s} does not implement {s}", .{ type_name, proto_name }); + } + } + } + } + } + const mangled_name = try self.mangleGenericName(template_name, sd.type_params, type_bindings, val_bindings, null); // Check if already instantiated @@ -1961,25 +2016,21 @@ pub const CodeGen = struct { } /// Walk an AST body to find a struct declaration (from `return struct { ... }` or bare struct expr). - fn findDeclInBody(comptime T: type, comptime tag: std.meta.FieldEnum(@TypeOf(@as(Node, undefined).data)), body: *Node) ?T { - if (body.data == tag) return @field(body.data, @tagName(tag)); + fn findStructInBody(_: *CodeGen, body: *Node) ?ast.StructDecl { + if (body.data == .struct_decl) return body.data.struct_decl; if (body.data == .block) { for (body.data.block.stmts) |stmt| { if (stmt.data == .return_stmt) { if (stmt.data.return_stmt.value) |val| { - if (val.data == tag) return @field(val.data, @tagName(tag)); + if (val.data == .struct_decl) return val.data.struct_decl; } } - if (stmt.data == tag) return @field(stmt.data, @tagName(tag)); + if (stmt.data == .struct_decl) return stmt.data.struct_decl; } } return null; } - fn findStructInBody(_: *CodeGen, body: *Node) ?ast.StructDecl { - return findDeclInBody(ast.StructDecl, .struct_decl, body); - } - fn findUnionInBody(_: *CodeGen, body: *Node) ?ast.EnumDecl { // Tagged enums with payloads are now stored as .enum_decl with variant_types populated const isTaggedEnum = struct { @@ -2373,6 +2424,12 @@ pub const CodeGen = struct { .ufcs_alias => |ua| { try self.ufcs_aliases.put(ua.name, ua.target); }, + .protocol_decl => |pd| { + try self.registerProtocolDecl(pd); + }, + .impl_block => |ib| { + try self.registerImplBlock(ib); + }, else => {}, } } @@ -2403,11 +2460,36 @@ pub const CodeGen = struct { try self.genLambdaBody(qualified, cd.value.data.lambda); } }, + .impl_block => |ib| { + try self.genImplMethodBodies(ib); + }, else => {}, } } } + /// Generate LLVM bodies for non-generic methods declared inside a struct. + fn genStructMethodBodies(self: *CodeGen, sd: ast.StructDecl) !void { + if (sd.methods.len == 0) return; + // Generic struct methods are instantiated on demand — skip body generation here + if (sd.type_params.len > 0) return; + + const saved_ns = self.current_namespace; + self.current_namespace = sd.name; + defer self.current_namespace = saved_ns; + + for (sd.methods) |method_node| { + const fd = method_node.data.fn_decl; + if (fd.type_params.len > 0) continue; // generic methods instantiated on demand + const qualified = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ sd.name, fd.name }); + if (shouldDeferFnBody(fd)) { + try self.deferred_fn_bodies.append(self.allocator, .{ .fd = fd, .name = qualified, .namespace = sd.name }); + } else { + try self.genFnBody(fd, qualified); + } + } + } + fn inferComptimeReturnType(self: *CodeGen, expr: *Node) Type { // xx: see through to inner expression if (expr.data == .unary_op and expr.data.unary_op.op == .xx) { @@ -2682,6 +2764,11 @@ pub const CodeGen = struct { try self.bindParam(function, param.name, sx_ty, @intCast(i)); } + // Auto-initialize context with default GPA (if std.sx imported) + if (is_main) { + try self.emitDefaultContextInit(); + } + // Push function-level scope so that function-body defers are tracked try self.pushScope(); @@ -2690,8 +2777,16 @@ pub const CodeGen = struct { if (body.data != .block) return self.emitError("function body must be a block"); var last_val: c.LLVMValueRef = null; - for (body.data.block.stmts) |stmt| { - last_val = try self.genStmt(stmt); + const stmts = body.data.block.stmts; + for (stmts, 0..) |stmt, stmt_idx| { + // Last statement with xx prefix: use return type as target context for implicit return + if (stmt_idx == stmts.len - 1 and ret_sx_type != .void_type and + stmt.data == .unary_op and stmt.data.unary_op.op == .xx) + { + last_val = try self.genExprAsType(stmt, ret_sx_type); + } else { + last_val = try self.genStmt(stmt); + } } // Return — skip if current block already has a terminator (from explicit return) @@ -2866,8 +2961,16 @@ pub const CodeGen = struct { var last_val: c.LLVMValueRef = null; if (fd.body.data == .block) { - for (fd.body.data.block.stmts) |stmt| { - last_val = try self.genStmt(stmt); + const fn_stmts = fd.body.data.block.stmts; + for (fn_stmts, 0..) |stmt, stmt_idx| { + // Last statement with xx prefix: use return type as target context + if (stmt_idx == fn_stmts.len - 1 and ret_sx_type != .void_type and + stmt.data == .unary_op and stmt.data.unary_op.op == .xx) + { + last_val = try self.genExprAsType(stmt, ret_sx_type); + } else { + last_val = try self.genStmt(stmt); + } } } else { last_val = try self.genExpr(fd.body); @@ -2902,6 +3005,14 @@ pub const CodeGen = struct { // C-style union — registration handled in type pass return null; }, + .protocol_decl => { + // Protocol declarations are handled in the registration pass + return null; + }, + .impl_block => { + // Impl blocks are handled in the registration pass + return null; + }, .assignment => |asgn| { return self.genAssignment(asgn); }, @@ -2913,6 +3024,12 @@ pub const CodeGen = struct { if (rs.value) |val_node| { const ret_val = if (self.current_return_type.isOptional()) blk: { break :blk try self.genExprAsType(val_node, self.current_return_type); + } else if (val_node.data == .unary_op and val_node.data.unary_op.op == .xx) blk: { + // xx in return position: use return type as target context + break :blk try self.genExprAsType(val_node, self.current_return_type); + } else if (self.current_return_type.isClosureType()) blk: { + // Closure return type: provide type context for inferred params + break :blk try self.genExprAsType(val_node, self.current_return_type); } else blk: { const raw_val = try self.genExpr(val_node); break :blk try self.prepareReturnValue(raw_val, self.current_return_type); @@ -3514,15 +3631,15 @@ pub const CodeGen = struct { return self.genIndexAssignment(asgn); } - // Deref assignment: p.* = value; + // Deref assignment: p.* = value; or p.* += value; if (asgn.target.data == .deref_expr) { const de = asgn.target.data.deref_expr; const ptr_val = try self.genExpr(de.operand); const ptr_ty = self.inferType(de.operand); if (!ptr_ty.isPointer()) return self.emitError("dereference assignment requires a pointer"); const pointee_ty = self.resolveTypeFromName(ptr_ty.pointer_type.pointee_name) orelse return self.emitError("unknown pointee type"); - const new_val = try self.genExprAsType(asgn.value, pointee_ty); - _ = c.LLVMBuildStore(self.builder, new_val, ptr_val); + const rhs_val = try self.genExprAsType(asgn.value, pointee_ty); + self.storeOrCompound(asgn.op, ptr_val, rhs_val, pointee_ty, "deref_cur"); return null; } @@ -3904,6 +4021,10 @@ pub const CodeGen = struct { fn_val = c.LLVMGetNamedFunction(self.module, self.nameToCStr(qualified, &qbuf)); } } + // Search all registered namespaces (for cross-namespace references) + if (fn_val == null) { + if (self.findFunction(ident.name)) |found| fn_val = found; + } // Foreign rename fallback: sx name → C symbol name if (fn_val == null) { if (self.foreign_name_map.get(ident.name)) |c_name| { @@ -4174,6 +4295,12 @@ pub const CodeGen = struct { if (rs.value) |val_node| { const ret_val = if (self.current_return_type.isOptional()) blk: { break :blk try self.genExprAsType(val_node, self.current_return_type); + } else if (val_node.data == .unary_op and val_node.data.unary_op.op == .xx) blk: { + // xx in return position: use return type as target context + break :blk try self.genExprAsType(val_node, self.current_return_type); + } else if (self.current_return_type.isClosureType()) blk: { + // Closure return type: provide type context for inferred params + break :blk try self.genExprAsType(val_node, self.current_return_type); } else blk: { const raw_val = try self.genExpr(val_node); break :blk try self.prepareReturnValue(raw_val, self.current_return_type); @@ -4500,6 +4627,8 @@ pub const CodeGen = struct { fn registerStructType(self: *CodeGen, sd: ast.StructDecl) anyerror!void { // Generic struct: store as template instead of registering now if (sd.type_params.len > 0) { + // Register methods for generic structs (as generic templates) + try self.registerStructMethods(sd); try self.generic_struct_templates.put(sd.name, sd); return; } @@ -4547,6 +4676,711 @@ pub const CodeGen = struct { try self.type_registry.put(sd.name, .{ .struct_info = sinfo }); _ = try self.getAnyTypeId(sd.name, .{ .struct_type = sd.name }); + + // Register struct methods after struct is fully registered (methods reference the struct type) + try self.registerStructMethods(sd); + } + + /// Register methods declared inside a struct body. + /// For generic structs, method type_params are augmented with the struct's type params. + fn registerStructMethods(self: *CodeGen, sd: ast.StructDecl) !void { + if (sd.methods.len == 0) return; + + // Register struct name as a namespace for static calls: StructName.method() + try self.namespaces.put(sd.name, {}); + + for (sd.methods) |method_node| { + const fd = method_node.data.fn_decl; + const qualified = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ sd.name, fd.name }); + + if (sd.type_params.len > 0) { + // Generic struct: merge struct type params into method, store as generic template. + // This allows List($T).append to be resolved as List.append with T inferred from args. + var merged_params = std.ArrayList(ast.StructTypeParam).empty; + // Add struct's type params first + for (sd.type_params) |tp| try merged_params.append(self.allocator, tp); + // Add method's own type params (if any), avoiding duplicates + for (fd.type_params) |tp| { + var dup = false; + for (sd.type_params) |stp| { + if (std.mem.eql(u8, stp.name, tp.name)) { dup = true; break; } + } + if (!dup) try merged_params.append(self.allocator, tp); + } + const augmented_fd = ast.FnDecl{ + .name = fd.name, + .params = fd.params, + .return_type = fd.return_type, + .body = fd.body, + .type_params = try merged_params.toOwnedSlice(self.allocator), + .is_arrow = fd.is_arrow, + }; + try self.generic_templates.put(qualified, augmented_fd); + } else if (fd.type_params.len > 0) { + // Non-generic struct with generic method + try self.generic_templates.put(qualified, fd); + } else { + // Non-generic struct, non-generic method: register directly + try self.registerFnDecl(fd, qualified); + } + } + } + + /// Register a protocol declaration. For #inline protocols, this generates + /// a struct type with ctx + fn-ptr fields and wrapper methods. + fn registerProtocolDecl(self: *CodeGen, pd: ast.ProtocolDecl) !void { + try self.protocol_decls.put(pd.name, pd); + + if (pd.is_inline) { + // #inline protocol: generate struct { ctx: *void, method1: fn_ptr, method2: fn_ptr, ... } + const n_fields = 1 + pd.methods.len; // ctx + one fn-ptr per method + var field_names = try self.allocator.alloc([]const u8, n_fields); + var field_types = try self.allocator.alloc(Type, n_fields); + var field_defaults = try self.allocator.alloc(?*Node, n_fields); + var llvm_field_types = try self.allocator.alloc(c.LLVMTypeRef, n_fields); + + // First field: ctx: *void + field_names[0] = "ctx"; + field_types[0] = .{ .pointer_type = .{ .pointee_name = "void" } }; + field_defaults[0] = null; + llvm_field_types[0] = self.ptrType(); + + // Fn-ptr fields: one per protocol method, with *void prepended as first param + for (pd.methods, 0..) |method, i| { + field_names[1 + i] = method.name; + field_defaults[1 + i] = null; + llvm_field_types[1 + i] = self.ptrType(); // fn ptrs are opaque pointers + + // Build the fn-ptr Type: (*void, param_types...) -> return_type + var fn_params = std.ArrayList(Type).empty; + try fn_params.append(self.allocator, .{ .pointer_type = .{ .pointee_name = "void" } }); // ctx param + for (method.params) |param_node| { + try fn_params.append(self.allocator, self.resolveType(param_node)); + } + const ret_ty = if (method.return_type) |rt| self.resolveType(rt) else Type.void_type; + const ret_ptr = try self.allocator.create(Type); + ret_ptr.* = ret_ty; + field_types[1 + i] = .{ .function_type = .{ + .param_types = try fn_params.toOwnedSlice(self.allocator), + .return_type = ret_ptr, + } }; + } + + // Create LLVM struct type + const name_z = try self.allocator.dupeZ(u8, pd.name); + const struct_ty = c.LLVMStructCreateNamed(self.context, name_z.ptr); + c.LLVMStructSetBody(struct_ty, llvm_field_types.ptr, @intCast(n_fields), 0); + + // Register as struct in type_registry + try self.type_registry.put(pd.name, .{ .struct_info = .{ + .field_names = field_names, + .field_types = field_types, + .field_defaults = field_defaults, + .llvm_type = struct_ty, + } }); + _ = try self.getAnyTypeId(pd.name, .{ .struct_type = pd.name }); + + // Register as namespace for wrapper methods + try self.namespaces.put(pd.name, {}); + + // Generate wrapper methods: Protocol.method(self: Protocol, args...) -> R + // These extract ctx and fn-ptr from the struct, then call fn-ptr(ctx, args...) + for (pd.methods, 0..) |method, i| { + const qualified = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ pd.name, method.name }); + + // Build the wrapper function type: (Protocol, param_types...) -> ret_type + var wrapper_param_types = std.ArrayList(c.LLVMTypeRef).empty; + try wrapper_param_types.append(self.allocator, struct_ty); // self as Protocol value + for (method.params) |param_node| { + const sx_ty = self.resolveType(param_node); + try wrapper_param_types.append(self.allocator, self.typeToLLVM(sx_ty)); + } + const ret_ty = if (method.return_type) |rt| self.resolveType(rt) else Type.void_type; + const llvm_ret = self.typeToLLVM(ret_ty); + const wrapper_params = try wrapper_param_types.toOwnedSlice(self.allocator); + + const fn_type = c.LLVMFunctionType(llvm_ret, if (wrapper_params.len > 0) wrapper_params.ptr else null, @intCast(wrapper_params.len), 0); + const qualified_z = try self.allocator.dupeZ(u8, qualified); + const wrapper_fn = c.LLVMAddFunction(self.module, qualified_z.ptr, fn_type); + + // Generate wrapper body: + // entry: + // %self = param[0] + // %ctx = extractvalue %self, 0 + // %fn_ptr = extractvalue %self, 1+i + // %result = call %fn_ptr(%ctx, param[1], param[2], ...) + // ret %result + const entry_bb = c.LLVMAppendBasicBlockInContext(self.context, wrapper_fn, "entry"); + const saved_builder_pos = c.LLVMGetInsertBlock(self.builder); + c.LLVMPositionBuilderAtEnd(self.builder, entry_bb); + + const self_val = c.LLVMGetParam(wrapper_fn, 0); + const ctx_val = c.LLVMBuildExtractValue(self.builder, self_val, 0, "ctx"); + const fn_ptr_val = c.LLVMBuildExtractValue(self.builder, self_val, @intCast(1 + i), "fn_ptr"); + + // Build call args: ctx, then forwarded params + var call_args = std.ArrayList(c.LLVMValueRef).empty; + try call_args.append(self.allocator, ctx_val); + for (0..method.params.len) |pi| { + try call_args.append(self.allocator, c.LLVMGetParam(wrapper_fn, @intCast(1 + pi))); + } + const call_args_slice = try call_args.toOwnedSlice(self.allocator); + + // Build the LLVM function type for the indirect call + var inner_param_types = std.ArrayList(c.LLVMTypeRef).empty; + try inner_param_types.append(self.allocator, self.ptrType()); // ctx: *void + for (method.params) |param_node| { + const sx_ty = self.resolveType(param_node); + try inner_param_types.append(self.allocator, self.typeToLLVM(sx_ty)); + } + const inner_params = try inner_param_types.toOwnedSlice(self.allocator); + const inner_fn_type = c.LLVMFunctionType(llvm_ret, if (inner_params.len > 0) inner_params.ptr else null, @intCast(inner_params.len), 0); + + const call_result = c.LLVMBuildCall2(self.builder, inner_fn_type, fn_ptr_val, if (call_args_slice.len > 0) call_args_slice.ptr else null, @intCast(call_args_slice.len), if (ret_ty == .void_type) "" else "result"); + + if (ret_ty == .void_type) { + _ = c.LLVMBuildRetVoid(self.builder); + } else { + _ = c.LLVMBuildRet(self.builder, call_result); + } + + // Restore builder position + if (saved_builder_pos != null) { + c.LLVMPositionBuilderAtEnd(self.builder, saved_builder_pos); + } + + // Store return type for this wrapper + try self.function_return_types.put(qualified, ret_ty); + + // Store sx param types for wrapper (Protocol value + method params) + // Self params are erased to *void in the wrapper signature + { + var sx_params = std.ArrayList(Type).empty; + try sx_params.append(self.allocator, .{ .struct_type = pd.name }); // first param = protocol value + for (method.params) |param_node| { + const sx_ty = self.resolveType(param_node); + try sx_params.append(self.allocator, sx_ty); + } + try self.fn_param_types.put(qualified, try sx_params.toOwnedSlice(self.allocator)); + } + } + } else { + // Default (vtable pointer) protocol layout: + // 1. Generate __Vtable struct: { fn1: (*void, ...) -> R, fn2: ... } + // 2. Generate Protocol struct: { ctx: *void, __vtable: *__Vtable } + // 3. Generate wrapper methods that dispatch through vtable + + // === 1. Generate vtable struct === + const vtable_name = try std.fmt.allocPrint(self.allocator, "{s}.__Vtable", .{pd.name}); + { + var vt_field_names = try self.allocator.alloc([]const u8, pd.methods.len); + var vt_field_types = try self.allocator.alloc(Type, pd.methods.len); + var vt_field_defaults = try self.allocator.alloc(?*Node, pd.methods.len); + var vt_llvm_types = try self.allocator.alloc(c.LLVMTypeRef, pd.methods.len); + + for (pd.methods, 0..) |method, i| { + vt_field_names[i] = method.name; + vt_field_defaults[i] = null; + vt_llvm_types[i] = self.ptrType(); // fn ptrs are opaque pointers + + // Build fn-ptr Type: (*void, param_types...) -> return_type + var fn_params = std.ArrayList(Type).empty; + try fn_params.append(self.allocator, .{ .pointer_type = .{ .pointee_name = "void" } }); + for (method.params) |param_node| { + try fn_params.append(self.allocator, self.resolveType(param_node)); + } + const ret_ty = if (method.return_type) |rt| self.resolveType(rt) else Type.void_type; + const ret_ptr = try self.allocator.create(Type); + ret_ptr.* = ret_ty; + vt_field_types[i] = .{ .function_type = .{ + .param_types = try fn_params.toOwnedSlice(self.allocator), + .return_type = ret_ptr, + } }; + } + + const vt_name_z = try self.allocator.dupeZ(u8, vtable_name); + const vt_struct_ty = c.LLVMStructCreateNamed(self.context, vt_name_z.ptr); + c.LLVMStructSetBody(vt_struct_ty, vt_llvm_types.ptr, @intCast(pd.methods.len), 0); + + try self.type_registry.put(vtable_name, .{ .struct_info = .{ + .field_names = vt_field_names, + .field_types = vt_field_types, + .field_defaults = vt_field_defaults, + .llvm_type = vt_struct_ty, + } }); + } + + // === 2. Generate protocol value struct: { ctx: *void, __vtable: *__Vtable } === + { + const field_names = try self.allocator.alloc([]const u8, 2); + var field_types_arr = try self.allocator.alloc(Type, 2); + var field_defaults_arr = try self.allocator.alloc(?*Node, 2); + var llvm_field_types_arr = try self.allocator.alloc(c.LLVMTypeRef, 2); + + field_names[0] = "ctx"; + field_types_arr[0] = .{ .pointer_type = .{ .pointee_name = "void" } }; + field_defaults_arr[0] = null; + llvm_field_types_arr[0] = self.ptrType(); + + field_names[1] = "__vtable"; + field_types_arr[1] = .{ .pointer_type = .{ .pointee_name = vtable_name } }; + field_defaults_arr[1] = null; + llvm_field_types_arr[1] = self.ptrType(); + + const name_z = try self.allocator.dupeZ(u8, pd.name); + const struct_ty = c.LLVMStructCreateNamed(self.context, name_z.ptr); + c.LLVMStructSetBody(struct_ty, llvm_field_types_arr.ptr, 2, 0); + + try self.type_registry.put(pd.name, .{ .struct_info = .{ + .field_names = field_names, + .field_types = field_types_arr, + .field_defaults = field_defaults_arr, + .llvm_type = struct_ty, + } }); + _ = try self.getAnyTypeId(pd.name, .{ .struct_type = pd.name }); + } + + // Register as namespace + try self.namespaces.put(pd.name, {}); + + // === 3. Generate wrapper methods (dispatch through vtable pointer) === + const proto_info = self.lookupStructInfo(pd.name) orelse return; + const proto_llvm_ty = proto_info.llvm_type orelse return; + const vtable_info = self.lookupStructInfo(vtable_name) orelse return; + const vtable_llvm_ty = vtable_info.llvm_type orelse return; + + for (pd.methods, 0..) |method, i| { + const qualified = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ pd.name, method.name }); + + // Wrapper sig: (Protocol, param_types...) -> ret_type + var wrapper_param_types = std.ArrayList(c.LLVMTypeRef).empty; + try wrapper_param_types.append(self.allocator, proto_llvm_ty); + for (method.params) |param_node| { + const sx_ty = self.resolveType(param_node); + try wrapper_param_types.append(self.allocator, self.typeToLLVM(sx_ty)); + } + const ret_ty = if (method.return_type) |rt| self.resolveType(rt) else Type.void_type; + const llvm_ret = self.typeToLLVM(ret_ty); + const wrapper_params = try wrapper_param_types.toOwnedSlice(self.allocator); + + const fn_type = c.LLVMFunctionType(llvm_ret, if (wrapper_params.len > 0) wrapper_params.ptr else null, @intCast(wrapper_params.len), 0); + const qualified_z = try self.allocator.dupeZ(u8, qualified); + const wrapper_fn = c.LLVMAddFunction(self.module, qualified_z.ptr, fn_type); + + // Generate wrapper body: + // %self = param[0] (Protocol struct value) + // %ctx = extractvalue %self, 0 (ctx: *void) + // %vtable_ptr = extractvalue %self, 1 (__vtable: *__Vtable) + // %vtable = load *__Vtable, %vtable_ptr + // %fn_ptr = extractvalue %vtable, i + // %result = call %fn_ptr(%ctx, args...) + const entry_bb = c.LLVMAppendBasicBlockInContext(self.context, wrapper_fn, "entry"); + const saved_builder_pos = c.LLVMGetInsertBlock(self.builder); + c.LLVMPositionBuilderAtEnd(self.builder, entry_bb); + + const self_val = c.LLVMGetParam(wrapper_fn, 0); + const ctx_val = c.LLVMBuildExtractValue(self.builder, self_val, 0, "ctx"); + const vtable_ptr = c.LLVMBuildExtractValue(self.builder, self_val, 1, "vtable_ptr"); + const vtable_val = c.LLVMBuildLoad2(self.builder, vtable_llvm_ty, vtable_ptr, "vtable"); + const fn_ptr_val = c.LLVMBuildExtractValue(self.builder, vtable_val, @intCast(i), "fn_ptr"); + + var call_args = std.ArrayList(c.LLVMValueRef).empty; + try call_args.append(self.allocator, ctx_val); + for (0..method.params.len) |pi| { + try call_args.append(self.allocator, c.LLVMGetParam(wrapper_fn, @intCast(1 + pi))); + } + const call_args_slice = try call_args.toOwnedSlice(self.allocator); + + var inner_param_types = std.ArrayList(c.LLVMTypeRef).empty; + try inner_param_types.append(self.allocator, self.ptrType()); + for (method.params) |param_node| { + const sx_ty = self.resolveType(param_node); + try inner_param_types.append(self.allocator, self.typeToLLVM(sx_ty)); + } + const inner_params = try inner_param_types.toOwnedSlice(self.allocator); + const inner_fn_type = c.LLVMFunctionType(llvm_ret, if (inner_params.len > 0) inner_params.ptr else null, @intCast(inner_params.len), 0); + + const call_result = c.LLVMBuildCall2(self.builder, inner_fn_type, fn_ptr_val, if (call_args_slice.len > 0) call_args_slice.ptr else null, @intCast(call_args_slice.len), if (ret_ty == .void_type) "" else "result"); + + if (ret_ty == .void_type) { + _ = c.LLVMBuildRetVoid(self.builder); + } else { + _ = c.LLVMBuildRet(self.builder, call_result); + } + + if (saved_builder_pos != null) { + c.LLVMPositionBuilderAtEnd(self.builder, saved_builder_pos); + } + + try self.function_return_types.put(qualified, ret_ty); + + // Store sx param types for wrapper (Protocol value + method params) + { + var sx_params = std.ArrayList(Type).empty; + try sx_params.append(self.allocator, .{ .struct_type = pd.name }); // first param = protocol value + for (method.params) |param_node| { + const sx_ty = self.resolveType(param_node); + try sx_params.append(self.allocator, sx_ty); + } + try self.fn_param_types.put(qualified, try sx_params.toOwnedSlice(self.allocator)); + } + } + } + } + + /// Register an `impl Protocol for Type { methods }` block. + /// Methods are registered as qualified `TypeName.method` (same as struct methods), + /// making them callable via dot-syntax on concrete types (static dispatch). + fn registerImplBlock(self: *CodeGen, ib: ast.ImplBlock) !void { + // Store the impl block keyed by "Protocol\x00Type" + const key = try std.fmt.allocPrint(self.allocator, "{s}\x00{s}", .{ ib.protocol_name, ib.target_type }); + try self.impl_blocks.put(key, ib); + + // Register target type as a namespace for static calls: TypeName.method() + try self.namespaces.put(ib.target_type, {}); + + for (ib.methods) |method_node| { + const fd = method_node.data.fn_decl; + const qualified = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ ib.target_type, fd.name }); + + if (ib.target_type_params.len > 0) { + // Generic target type: merge type params into method, store as generic template + var merged_params = std.ArrayList(ast.StructTypeParam).empty; + for (ib.target_type_params) |tp| try merged_params.append(self.allocator, tp); + for (fd.type_params) |tp| { + var dup = false; + for (ib.target_type_params) |stp| { + if (std.mem.eql(u8, stp.name, tp.name)) { dup = true; break; } + } + if (!dup) try merged_params.append(self.allocator, tp); + } + const augmented_fd = ast.FnDecl{ + .name = fd.name, + .params = fd.params, + .return_type = fd.return_type, + .body = fd.body, + .type_params = try merged_params.toOwnedSlice(self.allocator), + .is_arrow = fd.is_arrow, + }; + try self.generic_templates.put(qualified, augmented_fd); + } else if (fd.type_params.len > 0) { + // Non-generic target with generic method + try self.generic_templates.put(qualified, fd); + } else { + // Non-generic: register directly + try self.registerFnDecl(fd, qualified); + } + try self.fn_signatures.put(qualified, self.buildFnSignature(fd)); + } + + // Synthesize default method implementations for unoverridden protocol methods + if (self.protocol_decls.get(ib.protocol_name)) |pd| { + for (pd.methods) |method| { + if (method.default_body == null) continue; + + // Check if the impl already overrides this method + var overridden = false; + for (ib.methods) |m| { + if (std.mem.eql(u8, m.data.fn_decl.name, method.name)) { + overridden = true; + break; + } + } + if (overridden) continue; + + // Synthesize a fn_decl: method_name :: (self: *ConcreteType, params...) -> R { default_body } + const qualified = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ ib.target_type, method.name }); + const self_fd = try self.synthesizeDefaultMethod(ib.target_type, method); + try self.registerFnDecl(self_fd, qualified); + try self.fn_signatures.put(qualified, self.buildFnSignature(self_fd)); + } + } + + // Generate thunks (and vtable constants for non-inline protocols) + if (self.protocol_decls.get(ib.protocol_name)) |pd| { + try self.generateProtocolThunks(pd, ib); + } + } + + /// Generate thunk functions for a protocol impl. + /// Each thunk has signature (ctx: *void, args...) -> R and calls Type.method(xx ctx, args...). + fn generateProtocolThunks(self: *CodeGen, pd: ast.ProtocolDecl, ib: ast.ImplBlock) !void { + var thunks = try self.allocator.alloc(c.LLVMValueRef, pd.methods.len); + + for (pd.methods, 0..) |method, i| { + const thunk_name = try std.fmt.allocPrint(self.allocator, "__{s}_{s}_{s}", .{ ib.target_type, pd.name, method.name }); + const thunk_name_z = try self.allocator.dupeZ(u8, thunk_name); + + // Thunk signature: (ctx: *void, method_params...) -> method_return_type + var param_types = std.ArrayList(c.LLVMTypeRef).empty; + try param_types.append(self.allocator, self.ptrType()); // ctx: *void + for (method.params) |param_node| { + const sx_ty = self.resolveType(param_node); + try param_types.append(self.allocator, self.typeToLLVM(sx_ty)); + } + const param_slice = try param_types.toOwnedSlice(self.allocator); + const ret_ty = if (method.return_type) |rt| self.resolveType(rt) else Type.void_type; + const llvm_ret = self.typeToLLVM(ret_ty); + + const fn_type = c.LLVMFunctionType(llvm_ret, if (param_slice.len > 0) param_slice.ptr else null, @intCast(param_slice.len), 0); + const thunk_fn = c.LLVMAddFunction(self.module, thunk_name_z.ptr, fn_type); + c.LLVMSetLinkage(thunk_fn, c.LLVMPrivateLinkage); + + // Generate thunk body: + // entry: + // %ctx = param[0] (raw *void) + // %self = bitcast %ctx to *TargetType (no-op in opaque ptr world) + // %result = call TargetType.method(%self, param[1], param[2], ...) + // ret %result + const entry_bb = c.LLVMAppendBasicBlockInContext(self.context, thunk_fn, "entry"); + const saved_pos = c.LLVMGetInsertBlock(self.builder); + c.LLVMPositionBuilderAtEnd(self.builder, entry_bb); + + // Look up the actual impl method + const impl_method_name = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ ib.target_type, method.name }); + var nbuf: [256]u8 = undefined; + const impl_fn = c.LLVMGetNamedFunction(self.module, self.nameToCStr(impl_method_name, &nbuf)); + + if (impl_fn != null) { + // Build call args: ctx (as *Type, but opaque ptr = no cast needed), then forwarded params + var call_args = std.ArrayList(c.LLVMValueRef).empty; + try call_args.append(self.allocator, c.LLVMGetParam(thunk_fn, 0)); // ctx as *Type + for (method.params, 0..) |param_node, pi| { + const thunk_param = c.LLVMGetParam(thunk_fn, @intCast(1 + pi)); + // Check if this param is Self (erased to *void in thunk) + if (param_node.data == .type_expr and std.mem.eql(u8, param_node.data.type_expr.name, "Self")) { + // Self param: thunk receives *void (pointer to concrete value) + // Impl method expects concrete value by-value — load it + const concrete_ty_name = ib.target_type; + if (self.lookupStructInfo(concrete_ty_name)) |si| { + const llvm_ty = si.llvm_type orelse self.ptrType(); + const loaded = c.LLVMBuildLoad2(self.builder, llvm_ty, thunk_param, "self_arg"); + try call_args.append(self.allocator, loaded); + } else { + // Primitive type — use typeToLLVM + const real_ty = Type.fromName(concrete_ty_name) orelse Type{ .struct_type = concrete_ty_name }; + const llvm_ty = self.typeToLLVM(real_ty); + const loaded = c.LLVMBuildLoad2(self.builder, llvm_ty, thunk_param, "self_arg"); + try call_args.append(self.allocator, loaded); + } + } else { + try call_args.append(self.allocator, thunk_param); + } + } + const call_args_slice = try call_args.toOwnedSlice(self.allocator); + + // Get the impl function's type for the call + const impl_fn_type = c.LLVMGlobalGetValueType(impl_fn); + const call_result = c.LLVMBuildCall2(self.builder, impl_fn_type, impl_fn, if (call_args_slice.len > 0) call_args_slice.ptr else null, @intCast(call_args_slice.len), if (ret_ty == .void_type) "" else "result"); + + if (ret_ty == .void_type) { + _ = c.LLVMBuildRetVoid(self.builder); + } else { + // Check if return type is Self (erased to ptr) but impl returns concrete value + const is_self_return = if (method.return_type) |rt| + rt.data == .type_expr and std.mem.eql(u8, rt.data.type_expr.name, "Self") + else + false; + if (is_self_return) { + // Self return: impl returns concrete type (e.g., %Point), thunk must return ptr. + // Heap-allocate space, store result, return pointer. + const result_ty = c.LLVMTypeOf(call_result); + const size = c.LLVMSizeOf(result_ty); + const malloc_fn = self.getOrDeclareMalloc(); + const malloc_fn_ty = c.LLVMGlobalGetValueType(malloc_fn); + var malloc_args = [_]c.LLVMValueRef{size}; + const mem = c.LLVMBuildCall2(self.builder, malloc_fn_ty, malloc_fn, &malloc_args, 1, "self_ret"); + _ = c.LLVMBuildStore(self.builder, call_result, mem); + _ = c.LLVMBuildRet(self.builder, mem); + } else { + _ = c.LLVMBuildRet(self.builder, call_result); + } + } + } else { + // Fallback: unreachable (shouldn't happen — impl methods should be registered) + _ = c.LLVMBuildUnreachable(self.builder); + } + + if (saved_pos != null) { + c.LLVMPositionBuilderAtEnd(self.builder, saved_pos); + } + + thunks[i] = thunk_fn; + } + + // Store thunks keyed by "Protocol\x00Type" + const thunk_key = try std.fmt.allocPrint(self.allocator, "{s}\x00{s}", .{ pd.name, ib.target_type }); + try self.protocol_thunks.put(thunk_key, thunks); + + // For non-inline (vtable pointer) protocols, generate a static vtable constant + if (!pd.is_inline) { + const vtable_name = try std.fmt.allocPrint(self.allocator, "{s}.__Vtable", .{pd.name}); + if (self.lookupStructInfo(vtable_name)) |vtable_info| { + const vtable_llvm_ty = vtable_info.llvm_type orelse return; + // Build vtable constant: struct { thunk1, thunk2, ... } + var vtable_vals = try self.allocator.alloc(c.LLVMValueRef, pd.methods.len); + for (thunks, 0..) |thunk, i| { + vtable_vals[i] = thunk; + } + const vtable_const = c.LLVMConstNamedStruct(vtable_llvm_ty, vtable_vals.ptr, @intCast(pd.methods.len)); + + // Create a global constant for the vtable + const global_name = try std.fmt.allocPrint(self.allocator, "__{s}_{s}_vtable", .{ ib.target_type, pd.name }); + const global_name_z = try self.allocator.dupeZ(u8, global_name); + const vtable_global = c.LLVMAddGlobal(self.module, vtable_llvm_ty, global_name_z.ptr); + c.LLVMSetInitializer(vtable_global, vtable_const); + c.LLVMSetGlobalConstant(vtable_global, 1); + c.LLVMSetLinkage(vtable_global, c.LLVMPrivateLinkage); + } + } + } + + /// Generate function bodies for impl block methods. + fn genImplMethodBodies(self: *CodeGen, ib: ast.ImplBlock) !void { + // Generic impl methods are instantiated on demand + if (ib.target_type_params.len > 0) return; + + const saved_ns = self.current_namespace; + self.current_namespace = ib.target_type; + defer self.current_namespace = saved_ns; + + for (ib.methods) |method_node| { + const fd = method_node.data.fn_decl; + if (fd.type_params.len > 0) continue; // generic methods instantiated on demand + const qualified = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ ib.target_type, fd.name }); + if (shouldDeferFnBody(fd)) { + try self.deferred_fn_bodies.append(self.allocator, .{ .fd = fd, .name = qualified, .namespace = ib.target_type }); + } else { + try self.genFnBody(fd, qualified); + } + } + + // Generate bodies for synthesized default methods (not overridden in impl) + if (self.protocol_decls.get(ib.protocol_name)) |pd| { + for (pd.methods) |method| { + if (method.default_body == null) continue; + // Check if overridden + var overridden = false; + for (ib.methods) |m| { + if (std.mem.eql(u8, m.data.fn_decl.name, method.name)) { + overridden = true; + break; + } + } + if (overridden) continue; + + const qualified = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ ib.target_type, method.name }); + + const fd = try self.synthesizeDefaultMethod(ib.target_type, method); + try self.genFnBody(fd, qualified); + } + } + } + + /// Synthesize a FnDecl for a protocol default method on a concrete type. + /// Creates: method_name :: (self: *ConcreteType, params...) -> R { default_body } + fn synthesizeDefaultMethod(self: *CodeGen, target_type: []const u8, method: ast.ProtocolMethodDecl) !ast.FnDecl { + var params = std.ArrayList(ast.Param).empty; + + // self: *ConcreteType — pointer_type_expr wrapping type_expr + const base_type_node = try self.allocator.create(Node); + base_type_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = target_type } } }; + const ptr_type_node = try self.allocator.create(Node); + ptr_type_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .pointer_type_expr = .{ .pointee_type = base_type_node } } }; + try params.append(self.allocator, .{ + .name = "self", + .name_span = .{ .start = 0, .end = 0 }, + .type_expr = ptr_type_node, + }); + + // Protocol method params + for (method.params, 0..) |param_type, pi| { + const pname = if (pi < method.param_names.len) method.param_names[pi] else "arg"; + try params.append(self.allocator, .{ + .name = pname, + .name_span = .{ .start = 0, .end = 0 }, + .type_expr = param_type, + }); + } + + return ast.FnDecl{ + .name = method.name, + .params = try params.toOwnedSlice(self.allocator), + .return_type = method.return_type, + .body = method.default_body.?, + }; + } + + /// Build a protocol value struct from a concrete type pointer. + /// For #inline protocols: { ctx = ptr, fn1 = thunk1, fn2 = thunk2, ... } + fn buildProtocolValue(self: *CodeGen, val: c.LLVMValueRef, src_ty: Type, pd: ast.ProtocolDecl) !c.LLVMValueRef { + // Determine the concrete type name from src_ty + const concrete_type = if (src_ty.isPointer()) + src_ty.pointer_type.pointee_name + else if (src_ty.isStruct()) + src_ty.struct_type + else if (src_ty.isManyPointer()) + src_ty.many_pointer_type.element_name + else + return val; // can't convert, fallback + + // Look up thunks for this (Protocol, Type) pair + const thunk_key = try std.fmt.allocPrint(self.allocator, "{s}\x00{s}", .{ pd.name, concrete_type }); + const thunks = self.protocol_thunks.get(thunk_key) orelse return val; + + if (pd.is_inline) { + // Build inline protocol struct: { ctx, fn1, fn2, ... } + const proto_info = self.lookupStructInfo(pd.name) orelse return val; + const llvm_struct_ty = proto_info.llvm_type orelse return val; + + // Start with undef struct + var result = c.LLVMGetUndef(llvm_struct_ty); + + // ctx = val (the pointer to concrete type) + // If src is a value type (not pointer), we need to take its address + const ctx_ptr = if (src_ty.isPointer() or src_ty.isManyPointer()) + val + else blk: { + // Store value to a temp alloca and use its address + const tmp = self.buildEntryBlockAlloca(c.LLVMTypeOf(val), "proto_tmp"); + _ = c.LLVMBuildStore(self.builder, val, tmp); + break :blk tmp; + }; + result = self.insertValue(result, ctx_ptr, 0, "proto_ctx"); + + // Insert thunk function pointers + for (thunks, 0..) |thunk, i| { + result = self.insertValue(result, thunk, @intCast(1 + i), "proto_fn"); + } + + return result; + } + + // Non-inline (vtable pointer): { ctx = ptr, __vtable = &__Type_Proto_vtable } + const proto_info = self.lookupStructInfo(pd.name) orelse return val; + const llvm_struct_ty = proto_info.llvm_type orelse return val; + + var result = c.LLVMGetUndef(llvm_struct_ty); + + // ctx = ptr (same logic as inline) + const ctx_ptr = if (src_ty.isPointer() or src_ty.isManyPointer()) + val + else blk: { + const tmp = self.buildEntryBlockAlloca(c.LLVMTypeOf(val), "proto_tmp"); + _ = c.LLVMBuildStore(self.builder, val, tmp); + break :blk tmp; + }; + result = self.insertValue(result, ctx_ptr, 0, "proto_ctx"); + + // __vtable = pointer to static vtable global + const vtable_global_name = try std.fmt.allocPrint(self.allocator, "__{s}_{s}_vtable", .{ concrete_type, pd.name }); + var vbuf: [256]u8 = undefined; + const vtable_global = c.LLVMGetNamedGlobal(self.module, self.nameToCStr(vtable_global_name, &vbuf)); + if (vtable_global != null) { + result = self.insertValue(result, vtable_global, 1, "proto_vtable"); + } + + return result; } fn registerTaggedEnum(self: *CodeGen, ud: ast.EnumDecl) !void { @@ -5181,6 +6015,27 @@ pub const CodeGen = struct { return self.genSliceLiteral(node.data.array_literal, target_ty); } + // closure() with inferred params → provide type context from target + if (target_ty.isClosureType() and node.data == .call) { + const call_d = node.data.call; + if (call_d.callee.data == .identifier and + std.mem.eql(u8, call_d.callee.data.identifier.name, "closure")) + { + if (call_d.args.len == 1 and call_d.args[0].data == .lambda) { + const lam = call_d.args[0].data.lambda; + const has_inferred = for (lam.params) |p| { + if (p.type_expr.data == .inferred_type) break true; + } else false; + if (has_inferred) { + const saved_cet = self.closure_expected_type; + self.closure_expected_type = target_ty.closure_type; + defer self.closure_expected_type = saved_cet; + return try self.genExpr(node); + } + } + } + } + // Infer source type once for all coercion checks below const src_ty = self.inferType(node); @@ -5316,6 +6171,13 @@ pub const CodeGen = struct { // Same type → return as-is if (src_ty.eql(target_ty)) return val; + // Protocol conversion: concrete type → protocol value via xx + if (target_ty.isStruct()) { + if (self.protocol_decls.get(target_ty.struct_type)) |pd| { + return self.buildProtocolValue(val, src_ty, pd) catch return val; + } + } + // string <-> []u8: identical LLVM type {ptr, i64}, no conversion needed if ((src_ty == .string_type and target_ty.isSlice() and std.mem.eql(u8, target_ty.slice_type.element_name, "u8")) or @@ -6858,12 +7720,13 @@ pub const CodeGen = struct { // Struct field function pointer / closure call: obj.field(args) // Checked before UFCS so that struct fields shadow free functions of the same name. + // SKIP for protocol types — protocol fn-ptr fields are dispatched via wrapper methods. { var obj_ty = self.inferType(fa.object); if (obj_ty.isPointer()) { obj_ty = self.resolveTypeFromName(obj_ty.pointer_type.pointee_name) orelse obj_ty; } - if (obj_ty.isStruct()) { + if (obj_ty.isStruct() and !self.protocol_decls.contains(obj_ty.struct_type)) { if (self.lookupStructInfo(obj_ty.struct_type)) |info| { if (self.findNameIndex(info.field_names, fa.field)) |idx| { const field_ty = info.field_types[idx]; @@ -6886,64 +7749,56 @@ pub const CodeGen = struct { } } - // UFCS: obj.method(args...) → method(obj, args...) - const method_name = fa.field; - const resolved_method = self.ufcs_aliases.get(method_name) orelse method_name; - const method_z = self.allocator.dupeZ(u8, resolved_method) catch resolved_method; - // Also check namespace-qualified name for intra-namespace UFCS - const ns_qualified = if (self.current_namespace) |ns| - (std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ ns, resolved_method }) catch null) - else - null; - const ns_qualified_z = if (ns_qualified) |q| (self.allocator.dupeZ(u8, q) catch null) else null; - if (self.generic_templates.contains(resolved_method) or - c.LLVMGetNamedFunction(self.module, method_z.ptr) != null or - (if (ns_qualified) |q| self.generic_templates.contains(q) else false) or - (if (ns_qualified_z) |qz| c.LLVMGetNamedFunction(self.module, qz.ptr) != null else false)) + // Struct method: obj.method(args) → StructName.method(obj, args) { - // Use namespace-qualified name if that's what exists - const effective_method = if (c.LLVMGetNamedFunction(self.module, method_z.ptr) != null or self.generic_templates.contains(resolved_method)) - resolved_method - else - ns_qualified orelse resolved_method; - // Check if receiver is a tuple — if so, splat its elements as leading args - const receiver_type = self.inferType(fa.object); - const is_tuple = receiver_type == .tuple_type; - - if (is_tuple) { - const tuple_info = receiver_type.tuple_type; - const n_tuple = tuple_info.field_types.len; - var ufcs_args = try self.allocator.alloc(*Node, n_tuple + call_node.args.len); - // Create synthetic field_access nodes for each tuple element - for (0..n_tuple) |i| { - const syn_node = try self.allocator.create(ast.Node); - var idx_buf: [20]u8 = undefined; - const idx_str = std.fmt.bufPrint(&idx_buf, "{d}", .{i}) catch "0"; - const field_name = try self.allocator.dupe(u8, idx_str); - syn_node.* = .{ - .span = fa.object.span, - .data = .{ .field_access = .{ .object = fa.object, .field = field_name } }, - }; - ufcs_args[i] = syn_node; - } - for (call_node.args, 0..) |arg, i| { - ufcs_args[n_tuple + i] = arg; - } - return self.genCallByName(effective_method, .{ - .callee = call_node.callee, - .args = ufcs_args, - }); + var obj_ty2 = self.inferType(fa.object); + if (obj_ty2.isPointer()) { + obj_ty2 = self.resolveTypeFromName(obj_ty2.pointer_type.pointee_name) orelse obj_ty2; } - - var ufcs_args = try self.allocator.alloc(*Node, call_node.args.len + 1); - ufcs_args[0] = fa.object; - for (call_node.args, 0..) |arg, i| { - ufcs_args[i + 1] = arg; + if (obj_ty2.isStruct()) { + const struct_name = obj_ty2.struct_type; + // Try base struct name and template name for generic structs + const template_name = if (self.lookupStructInfo(struct_name)) |si| si.template_name orelse struct_name else struct_name; + const qualified = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ template_name, fa.field }); + const qualified_z = try self.allocator.dupeZ(u8, qualified); + if (self.generic_templates.contains(qualified) or + c.LLVMGetNamedFunction(self.module, qualified_z.ptr) != null) + { + var method_args = try self.allocator.alloc(*Node, call_node.args.len + 1); + method_args[0] = fa.object; + for (call_node.args, 0..) |arg, i| { + method_args[i + 1] = arg; + } + return self.genCallByName(qualified, .{ + .callee = call_node.callee, + .args = method_args, + }); + } + } + // Non-struct impl method: obj.method(args) for primitive/other types with impl blocks + // e.g., s32.eq(other) via `impl Eq for s32` + if (!obj_ty2.isStruct()) { + const type_name = obj_ty2.toName(); + if (type_name) |tn| { + if (self.namespaces.contains(tn)) { + const qualified = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ tn, fa.field }); + const qualified_z = try self.allocator.dupeZ(u8, qualified); + if (self.generic_templates.contains(qualified) or + c.LLVMGetNamedFunction(self.module, qualified_z.ptr) != null) + { + var method_args = try self.allocator.alloc(*Node, call_node.args.len + 1); + method_args[0] = fa.object; + for (call_node.args, 0..) |arg, i| { + method_args[i + 1] = arg; + } + return self.genCallByName(qualified, .{ + .callee = call_node.callee, + .args = method_args, + }); + } + } + } } - return self.genCallByName(effective_method, .{ - .callee = call_node.callee, - .args = ufcs_args, - }); } } @@ -7160,13 +8015,47 @@ pub const CodeGen = struct { stored_param_types.?[i] else self.llvmTypeToSxType(param_llvm_types[i]); + + // Check if this is a Self-erased protocol param BEFORE generating the value + const is_self_erased = blk: { + if (!param_ty.isPointer() or !std.mem.eql(u8, param_ty.pointer_type.pointee_name, "void")) + break :blk false; + const dot_pos = std.mem.indexOfScalar(u8, callee_name, '.') orelse break :blk false; + const proto_name = callee_name[0..dot_pos]; + const pd = self.protocol_decls.get(proto_name) orelse break :blk false; + const method_name = callee_name[dot_pos + 1 ..]; + for (pd.methods) |pm| { + if (std.mem.eql(u8, pm.name, method_name)) { + // stored_param_types = [Proto, param0, param1, ...] + // call_node.args after genCall prepends self, so arg[0]=self, arg[1]=first method param + const method_param_idx = if (i > 0) i - 1 else break; + if (method_param_idx < pm.params.len) { + const pn = pm.params[method_param_idx]; + if (pn.data == .type_expr and std.mem.eql(u8, pn.data.type_expr.name, "Self")) + break :blk true; + } + break; + } + } + break :blk false; + }; + // For #foreign functions, [:0]u8 params have LLVM type ptr const arg_ty = self.inferType(arg); const llvm_param_is_ptr = (i < num_params and c.LLVMGetTypeKind(param_llvm_types[i]) == c.LLVMPointerTypeKind); const ptr_ty = Type{ .pointer_type = .{ .pointee_name = "u8" } }; - var val = if (llvm_param_is_ptr and arg.data == .string_literal) blk: { + var val = if (is_self_erased) blk: { + // Self-erased param: generate as actual type, then take address to pass as *void + const raw_val = try self.genExpr(arg); + if (!arg_ty.isPointer()) { + const tmp = self.buildEntryBlockAlloca(c.LLVMTypeOf(raw_val), "self_tmp"); + _ = c.LLVMBuildStore(self.builder, raw_val, tmp); + break :blk tmp; + } + break :blk raw_val; + } else if (llvm_param_is_ptr and arg.data == .string_literal) blk: { // String literal → pointer: produce raw ptr directly (context-dependent) break :blk try self.genExprAsType(arg, ptr_ty); } else if (llvm_param_is_ptr and arg_ty == .string_type) blk: { @@ -7442,9 +8331,16 @@ pub const CodeGen = struct { else => return self.emitError("closure() argument must be a lambda expression"), }; + // Check if any params need type inference + const has_inferred_params = for (lambda.params) |p| { + if (p.type_expr.data == .inferred_type) break true; + } else false; + // Determine lambda return type const ret_ty = if (lambda.return_type) |rt| self.resolveType(rt) + else if (has_inferred_params) + (if (self.closure_expected_type) |ctx| ctx.return_type.* else Type.void_type) else if (lambda.body.data == .block) Type.void_type else @@ -7471,10 +8367,18 @@ pub const CodeGen = struct { } const capture_list = deduped.items; - // Build param types for the closure type + // Build param types for the closure type (resolve inferred from context) var closure_param_types = try self.allocator.alloc(Type, lambda.params.len); for (lambda.params, 0..) |p, i| { - closure_param_types[i] = self.resolveType(p.type_expr); + if (p.type_expr.data == .inferred_type) { + if (self.closure_expected_type) |ctx| { + if (i < ctx.param_types.len) { + closure_param_types[i] = ctx.param_types[i]; + } else return self.emitError("closure has more parameters than expected type"); + } else return self.emitError("cannot infer closure parameter type without type context"); + } else { + closure_param_types[i] = self.resolveType(p.type_expr); + } } // Generate unique name @@ -7500,8 +8404,7 @@ pub const CodeGen = struct { const total_params = lambda.params.len + 1; var tramp_param_types = try self.allocator.alloc(c.LLVMTypeRef, total_params); tramp_param_types[0] = ptr_ty; // env: *void - for (lambda.params, 0..) |p, i| { - const pt = self.resolveType(p.type_expr); + for (closure_param_types, 0..) |pt, i| { tramp_param_types[i + 1] = if (pt.isArray()) ptr_ty else self.typeToLLVM(pt); } const ret_llvm = self.typeToLLVM(ret_ty); @@ -7554,7 +8457,7 @@ pub const CodeGen = struct { // Register lambda params (starting from param index 1) for (lambda.params, 0..) |p, i| { - const param_ty = self.resolveType(p.type_expr); + const param_ty = closure_param_types[i]; const llvm_param = c.LLVMGetParam(tramp_fn, @intCast(i + 1)); const param_llvm_ty = self.typeToLLVM(param_ty); const alloca = self.buildEntryBlockAlloca(param_llvm_ty, "param"); @@ -7593,20 +8496,10 @@ pub const CodeGen = struct { // Now back in the caller's context — allocate env and store captures if (capture_list.len > 0) { - // Allocate env via malloc + // Allocate env via context.allocator const env_size = c.LLVMSizeOf(env_struct_ty); const env_size_i64 = c.LLVMBuildIntCast2(self.builder, env_size, self.i64Type(), 0, "env_size"); - const malloc_fn = self.getOrDeclareMalloc(); - var malloc_arg_types = [_]c.LLVMTypeRef{self.i64Type()}; - var malloc_args = [_]c.LLVMValueRef{env_size_i64}; - const env_raw = c.LLVMBuildCall2( - self.builder, - c.LLVMFunctionType(ptr_ty, &malloc_arg_types, 1, 0), - malloc_fn, - &malloc_args, - 1, - "env_alloc", - ); + const env_raw = try self.emitContextAlloc(env_size_i64); // Store captured values into env struct for (capture_list, 0..) |cap, i| { @@ -7776,7 +8669,7 @@ pub const CodeGen = struct { }, // Leaf nodes: nothing to capture .int_literal, .float_literal, .bool_literal, .string_literal, - .null_literal, .undef_literal, .builtin_expr, .break_expr, + .null_literal, .undef_literal, .inferred_type, .builtin_expr, .break_expr, .continue_expr, .type_expr, .enum_literal, .foreign_expr, .library_decl, .array_type_expr, .slice_type_expr, .pointer_type_expr, .many_pointer_type_expr, .optional_type_expr, @@ -7811,10 +8704,146 @@ pub const CodeGen = struct { .root, .fn_decl, .param, .match_arm, .enum_decl, .struct_decl, .union_decl, .namespace_decl, .import_decl, .c_import_decl, .ufcs_alias, .parameterized_type_expr, + .protocol_decl, .impl_block, => {}, } } + /// Allocate memory via context.allocator. Panics at runtime if no allocator is set. + /// Emits: context.allocator.alloc_fn(context.allocator.ctx, size) + fn emitContextAlloc(self: *CodeGen, size_val: c.LLVMValueRef) !c.LLVMValueRef { + // Look up the 'context' global + const ctx_entry = self.global_mutable_vars.get("context") orelse + return self.emitError("closure() requires a global 'context' variable (from std.sx) with an allocator"); + + // Look up struct layout for Context and Allocator + const ctx_info = self.lookupStructInfo("Context") orelse + return self.emitError("closure() requires 'Context' struct type"); + const alloc_info = self.lookupStructInfo("Allocator") orelse + return self.emitError("closure() requires 'Allocator' struct type"); + + // Find field indices + const alloc_field_idx: c_uint = for (ctx_info.field_names, 0..) |fname, i| { + if (std.mem.eql(u8, fname, "allocator")) break @intCast(i); + } else return self.emitError("Context struct missing 'allocator' field"); + + const ctx_field_idx: c_uint = for (alloc_info.field_names, 0..) |fname, i| { + if (std.mem.eql(u8, fname, "ctx")) break @intCast(i); + } else return self.emitError("Allocator struct missing 'ctx' field"); + + const alloc_fn_field_idx: c_uint = for (alloc_info.field_names, 0..) |fname, i| { + if (std.mem.eql(u8, fname, "alloc") or std.mem.eql(u8, fname, "alloc_fn")) break @intCast(i); + } else return self.emitError("Allocator struct missing 'alloc' field"); + + // GEP to context.allocator + const alloc_ptr = self.structGEP(ctx_info.llvm_type, ctx_entry.ptr, alloc_field_idx, "ctx_alloc_ptr"); + + // Load allocator.ctx and allocator.alloc_fn + const alloc_ctx_ptr = self.structGEP(alloc_info.llvm_type, alloc_ptr, ctx_field_idx, "alloc_ctx_ptr"); + const alloc_ctx = c.LLVMBuildLoad2(self.builder, self.ptrType(), alloc_ctx_ptr, "alloc_ctx"); + const alloc_fn_ptr = self.structGEP(alloc_info.llvm_type, alloc_ptr, alloc_fn_field_idx, "alloc_fn_ptr"); + const alloc_fn = c.LLVMBuildLoad2(self.builder, self.ptrType(), alloc_fn_ptr, "alloc_fn"); + + // Check allocator.ctx != null — panic if no allocator set + const is_null = c.LLVMBuildICmp(self.builder, c.LLVMIntEQ, alloc_ctx, c.LLVMConstNull(self.ptrType()), "alloc_null"); + const cur_fn = self.current_function; + const then_bb = c.LLVMAppendBasicBlockInContext(self.context, cur_fn, "alloc_panic"); + const cont_bb = c.LLVMAppendBasicBlockInContext(self.context, cur_fn, "alloc_ok"); + _ = c.LLVMBuildCondBr(self.builder, is_null, then_bb, cont_bb); + + // Panic block: call trap + c.LLVMPositionBuilderAtEnd(self.builder, then_bb); + const trap_fn = self.getOrDeclareIntrinsic("llvm.trap"); + _ = c.LLVMBuildCall2(self.builder, c.LLVMFunctionType(self.voidType(), null, 0, 0), trap_fn, null, 0, ""); + _ = c.LLVMBuildUnreachable(self.builder); + + // Continue: call alloc_fn(ctx, size) + c.LLVMPositionBuilderAtEnd(self.builder, cont_bb); + var fn_param_types = [_]c.LLVMTypeRef{ self.ptrType(), self.i64Type() }; + const fn_type = c.LLVMFunctionType(self.ptrType(), &fn_param_types, 2, 0); + var call_args = [_]c.LLVMValueRef{ alloc_ctx, size_val }; + return c.LLVMBuildCall2(self.builder, fn_type, alloc_fn, &call_args, 2, "env_alloc"); + } + + fn getOrDeclareIntrinsic(self: *CodeGen, name: [*c]const u8) c.LLVMValueRef { + if (c.LLVMGetNamedFunction(self.module, name)) |f| return f; + const fn_type = c.LLVMFunctionType(self.voidType(), null, 0, 0); + return c.LLVMAddFunction(self.module, name, fn_type); + } + + /// Find an LLVM function by name, trying bare name first then namespaced variants. + fn findFunction(self: *CodeGen, name: []const u8) ?c.LLVMValueRef { + // Try bare name first + var nbuf: [256]u8 = undefined; + const name_z = std.fmt.bufPrintZ(&nbuf, "{s}", .{name}) catch return null; + if (c.LLVMGetNamedFunction(self.module, name_z.ptr)) |f| return f; + // Try each registered namespace prefix + var ns_it = self.namespaces.iterator(); + while (ns_it.next()) |entry| { + var qbuf: [256]u8 = undefined; + const qualified = std.fmt.bufPrintZ(&qbuf, "{s}.{s}", .{ entry.key_ptr.*, name }) catch continue; + if (c.LLVMGetNamedFunction(self.module, qualified.ptr)) |f| return f; + } + return null; + } + + /// Auto-initialize the global `context` with a default GPA allocator. + /// Called at the start of main(). No-op if std.sx types are not imported. + fn emitDefaultContextInit(self: *CodeGen) !void { + // Look up context global — bail if not present (no std.sx) + const ctx_entry = self.global_mutable_vars.get("context") orelse return; + + // Look up struct layouts — bail if missing + const ctx_info = self.lookupStructInfo("Context") orelse return; + const alloc_info = self.lookupStructInfo("Allocator") orelse return; + const gpa_info = self.lookupStructInfo("GPA") orelse return; + + // Look up GPA→Allocator thunk functions (protocol-generated) + const gpa_alloc_fn = self.findFunction("__GPA_Allocator_alloc") orelse + self.findFunction("gpa_alloc") orelse return; + const gpa_free_fn = self.findFunction("__GPA_Allocator_dealloc") orelse + self.findFunction("gpa_free") orelse return; + + // Find field indices in Context + const alloc_field_idx: c_uint = for (ctx_info.field_names, 0..) |fname, i| { + if (std.mem.eql(u8, fname, "allocator")) break @intCast(i); + } else return; + const data_field_idx: c_uint = for (ctx_info.field_names, 0..) |fname, i| { + if (std.mem.eql(u8, fname, "data")) break @intCast(i); + } else return; + + // Find field indices in Allocator (protocol-generated: ctx, alloc, dealloc) + const ctx_field_idx: c_uint = for (alloc_info.field_names, 0..) |fname, i| { + if (std.mem.eql(u8, fname, "ctx")) break @intCast(i); + } else return; + const alloc_fn_field_idx: c_uint = for (alloc_info.field_names, 0..) |fname, i| { + if (std.mem.eql(u8, fname, "alloc") or std.mem.eql(u8, fname, "alloc_fn")) break @intCast(i); + } else return; + const free_fn_field_idx: c_uint = for (alloc_info.field_names, 0..) |fname, i| { + if (std.mem.eql(u8, fname, "dealloc") or std.mem.eql(u8, fname, "free_fn")) break @intCast(i); + } else return; + + // 1. Stack-allocate GPA struct and zero alloc_count + const gpa_alloca = self.buildEntryBlockAlloca(gpa_info.llvm_type, "__default_gpa"); + self.storeStructField(gpa_info.llvm_type, gpa_alloca, 0, c.LLVMConstInt(self.i64Type(), 0, 0)); + + // 2. GEP to context.allocator + const alloc_ptr = self.structGEP(ctx_info.llvm_type, ctx_entry.ptr, alloc_field_idx, "ctx_alloc"); + + // 3. Store allocator.ctx = &gpa + self.storeStructField(alloc_info.llvm_type, alloc_ptr, ctx_field_idx, gpa_alloca); + + // 4. Store allocator.alloc_fn = @gpa_alloc + self.storeStructField(alloc_info.llvm_type, alloc_ptr, alloc_fn_field_idx, gpa_alloc_fn); + + // 5. Store allocator.free_fn = @gpa_free + self.storeStructField(alloc_info.llvm_type, alloc_ptr, free_fn_field_idx, gpa_free_fn); + + // 6. Store context.data = null + const data_ptr = self.structGEP(ctx_info.llvm_type, ctx_entry.ptr, data_field_idx, "ctx_data"); + _ = c.LLVMBuildStore(self.builder, c.LLVMConstNull(self.ptrType()), data_ptr); + } + fn getOrDeclareMalloc(self: *CodeGen) c.LLVMValueRef { var nbuf: [256]u8 = undefined; if (c.LLVMGetNamedFunction(self.module, self.nameToCStr("malloc", &nbuf))) |f| return f; @@ -7983,6 +9012,21 @@ pub const CodeGen = struct { } } + // Check protocol constraints: $T/Eq/Hashable → verify T implements Eq and Hashable + for (fd.type_params) |tp| { + if (tp.protocol_constraints.len > 0) { + if (bindings.get(tp.name)) |bound_ty| { + const type_name = bound_ty.toName() orelse "unknown"; + for (tp.protocol_constraints) |proto_name| { + const key = try std.fmt.allocPrint(self.allocator, "{s}\x00{s}", .{ proto_name, type_name }); + if (!self.impl_blocks.contains(key)) { + return self.emitErrorFmt("{s} does not implement {s}", .{ type_name, proto_name }); + } + } + } + } + } + if (has_comptime_values) { return self.genComptimeCall(qualified_name, fd, call_node, bindings, comptime_nodes); } @@ -8001,18 +9045,28 @@ pub const CodeGen = struct { const saved_call_bindings = self.type_param_bindings; self.type_param_bindings = bindings; var arg_vals = std.ArrayList(c.LLVMValueRef).empty; - for (call_node.args, 0..) |arg, i| { - // Skip $T: Type params — the arg is a type expression, not a runtime value - if (i < fd.params.len) { - const p = fd.params[i]; - if (isTypeParamDecl(p)) continue; + // Use separate indices for call args and fn params, since $T: Type params + // may or may not have a corresponding arg (explicit type vs inferred) + var arg_idx: usize = 0; + for (fd.params) |p| { + if (isTypeParamDecl(p)) { + // If the arg at this position is a type expression, skip it (explicitly passed type) + if (arg_idx < call_node.args.len and call_node.args[arg_idx].data == .type_expr) { + arg_idx += 1; + } + // Otherwise, T was inferred — no arg to consume + continue; } - if (i < fd.params.len) { - const param_ty = self.resolveType(fd.params[i].type_expr); - try arg_vals.append(self.allocator, try self.genExprAsType(arg, param_ty)); - } else { - try arg_vals.append(self.allocator, try self.genExpr(arg)); + if (p.is_comptime) { + // Comptime value param — skip (handled in genComptimeCall) + if (arg_idx < call_node.args.len) arg_idx += 1; + continue; } + if (arg_idx < call_node.args.len) { + const param_ty = self.resolveType(p.type_expr); + try arg_vals.append(self.allocator, try self.genExprAsType(call_node.args[arg_idx], param_ty)); + } + arg_idx += 1; } self.type_param_bindings = saved_call_bindings; const args_slice = try arg_vals.toOwnedSlice(self.allocator); @@ -9425,24 +10479,37 @@ pub const CodeGen = struct { return std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ fa.object.data.identifier.name, fa.field }) catch return null; } } - // UFCS: obj.method(args) → method is the callee name - const resolved = self.ufcs_aliases.get(fa.field) orelse fa.field; - const method_z = self.allocator.dupeZ(u8, resolved) catch return null; - if (self.generic_templates.contains(resolved) or - c.LLVMGetNamedFunction(self.module, method_z.ptr) != null) - { - return resolved; - } - // Intra-namespace fallback - if (self.current_namespace) |ns| { - const qualified = std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ ns, resolved }) catch return null; - const qz = self.allocator.dupeZ(u8, qualified) catch return null; + // Struct method: obj.method(args) → StructName.method + const obj_ty_raw = self.inferType(fa.object); + const obj_ty = if (obj_ty_raw.isPointer()) + (self.resolveTypeFromName(obj_ty_raw.pointer_type.pointee_name) orelse obj_ty_raw) + else + obj_ty_raw; + if (obj_ty.isStruct()) { + const struct_name = obj_ty.struct_type; + const template_name = if (self.lookupStructInfo(struct_name)) |si| si.template_name orelse struct_name else struct_name; + const qualified = std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ template_name, fa.field }) catch return null; + const qualified_z = self.allocator.dupeZ(u8, qualified) catch return null; if (self.generic_templates.contains(qualified) or - c.LLVMGetNamedFunction(self.module, qz.ptr) != null) + c.LLVMGetNamedFunction(self.module, qualified_z.ptr) != null) { return qualified; } } + // Non-struct impl method: e.g., s64.eq via `impl Eq for s64` + if (!obj_ty.isStruct()) { + if (obj_ty.toName()) |tn| { + if (self.namespaces.contains(tn)) { + const qualified = std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ tn, fa.field }) catch return null; + const qualified_z = self.allocator.dupeZ(u8, qualified) catch return null; + if (self.generic_templates.contains(qualified) or + c.LLVMGetNamedFunction(self.module, qualified_z.ptr) != null) + { + return qualified; + } + } + } + } } return null; } @@ -9663,7 +10730,7 @@ pub const CodeGen = struct { } } // Closure display name: "Closure(T1, T2) -> R" or "Closure(T1, T2)" - if (name.len > 9 and std.mem.startsWith(u8, name, "Closure(")) { + if (name.len >= 9 and std.mem.startsWith(u8, name, "Closure(")) { // Find matching closing paren if (std.mem.indexOfScalar(u8, name[8..], ')')) |close_rel| { const params_str = name[8 .. 8 + close_rel]; @@ -9864,6 +10931,16 @@ pub const CodeGen = struct { if (std.mem.eql(u8, base_name, "closure")) { if (call_node.args.len == 1 and call_node.args[0].data == .lambda) { const lam = call_node.args[0].data.lambda; + // If any param has inferred type, use context type directly + const has_inferred = for (lam.params) |p| { + if (p.type_expr.data == .inferred_type) break true; + } else false; + if (has_inferred) { + if (self.closure_expected_type) |ctx| { + return .{ .closure_type = ctx }; + } + return Type.s(64); // fallback — will error in genClosureIntrinsic + } var param_types = std.ArrayList(Type).empty; for (lam.params) |p| { param_types.append(self.allocator, self.resolveType(p.type_expr)) catch {}; diff --git a/src/comptime.zig b/src/comptime.zig index 41482fb..0de9aa7 100644 --- a/src/comptime.zig +++ b/src/comptime.zig @@ -289,7 +289,7 @@ pub const UnionFieldType = enum { int, float, bool_k, pointer, string }; pub const ValueKind = enum { int, float, f32_k, bool_k, string }; -pub const BuiltinId = enum { print, out, sqrt, size_of, cast, malloc, free, memcpy, memset, type_of }; +pub const BuiltinId = enum { print, out, sqrt, size_of, cast, malloc, free, memcpy, memset, type_of, alloc, dealloc }; /// A compiled function or expression — a flat sequence of instructions. pub const Chunk = struct { @@ -1864,6 +1864,56 @@ pub const VM = struct { } } + // Resolve UFCS aliases first (comptime builtins like allocator_alloc need priority) + for (self.root_decls) |decl| { + switch (decl.data) { + .ufcs_alias => |ua| { + if (std.mem.eql(u8, ua.name, name)) { + // Check if target is a builtin + if (std.meta.stringToEnum(BuiltinId, ua.target)) |id| + return self.callBuiltin(id, arg_count); + return self.callFunction(ua.target, arg_count); + } + }, + .namespace_decl => |ns| { + for (ns.decls) |d| { + if (d.data == .ufcs_alias) { + const ua = d.data.ufcs_alias; + if (std.mem.eql(u8, ua.name, name)) { + if (std.meta.stringToEnum(BuiltinId, ua.target)) |id| + return self.callBuiltin(id, arg_count); + return self.callFunction(ua.target, arg_count); + } + } + } + }, + else => {}, + } + } + + // Search struct methods (after UFCS aliases, so builtins take priority) + for (self.root_decls) |decl| { + switch (decl.data) { + .struct_decl => |sd| { + for (sd.methods) |m| { + if (m.data == .fn_decl and std.mem.eql(u8, m.data.fn_decl.name, name)) + return self.compileFunctionAndInvoke(name, m.data.fn_decl, arg_count); + } + }, + .namespace_decl => |ns| { + for (ns.decls) |d| { + if (d.data == .struct_decl) { + for (d.data.struct_decl.methods) |m| { + if (m.data == .fn_decl and std.mem.eql(u8, m.data.fn_decl.name, name)) + return self.compileFunctionAndInvoke(name, m.data.fn_decl, arg_count); + } + } + } + }, + else => {}, + } + } + return error.UndefinedFunction; } @@ -2038,6 +2088,31 @@ pub const VM = struct { } try self.push(.{ .void_val = {} }); }, + .alloc => { + // alloc(size) or alloc(allocator, size) — at comptime, equivalent to malloc + if (arg_count >= 2) { + const size_val = try self.pop(); + _ = try self.pop(); // discard allocator struct + const size: usize = if (size_val.asInt()) |v| @intCast(@max(0, v)) else 0; + const buf = try self.allocator.alloc(u8, size); + @memset(buf, 0); + try self.push(.{ .byte_ptr_val = .{ .data = buf, .offset = 0 } }); + } else if (arg_count == 1) { + const val = try self.pop(); + const size: usize = if (val.asInt()) |v| @intCast(@max(0, v)) else 0; + const buf = try self.allocator.alloc(u8, size); + @memset(buf, 0); + try self.push(.{ .byte_ptr_val = .{ .data = buf, .offset = 0 } }); + } else { + try self.push(.{ .byte_ptr_val = .{ .data = &.{}, .offset = 0 } }); + } + }, + .dealloc => { + // dealloc(ptr) — at comptime, no-op + var i: u8 = 0; + while (i < arg_count) : (i += 1) _ = try self.pop(); + try self.push(.{ .void_val = {} }); + }, .type_of => { // type_of(val) — return the type tag (matching ANY_TAG_* constants) if (arg_count >= 1) { diff --git a/src/lexer.zig b/src/lexer.zig index e91d7cc..4fcde1f 100644 --- a/src/lexer.zig +++ b/src/lexer.zig @@ -76,6 +76,7 @@ pub const Lexer = struct { .{ "#source", Tag.hash_source }, .{ "#define", Tag.hash_define }, .{ "#flags", Tag.hash_flags }, + .{ "#inline", Tag.hash_inline }, }; inline for (directives) |d| { const keyword = d[0]; diff --git a/src/lsp/server.zig b/src/lsp/server.zig index b4589e2..b8d9af9 100644 --- a/src/lsp/server.zig +++ b/src/lsp/server.zig @@ -399,6 +399,7 @@ pub const Server = struct { .constant => @intFromEnum(lsp.SymbolKindLsp.Constant), .enum_type => @intFromEnum(lsp.SymbolKindLsp.Enum), .struct_type => @intFromEnum(lsp.SymbolKindLsp.Struct), + .protocol_type => @intFromEnum(lsp.SymbolKindLsp.Interface), .type_alias => @intFromEnum(lsp.SymbolKindLsp.Class), .param => @intFromEnum(lsp.SymbolKindLsp.Variable), .namespace => @intFromEnum(lsp.SymbolKindLsp.Namespace), @@ -455,6 +456,7 @@ pub const Server = struct { .constant => @intFromEnum(lsp.CompletionItemKind.Constant), .enum_type => @intFromEnum(lsp.CompletionItemKind.Enum), .struct_type => @intFromEnum(lsp.CompletionItemKind.Struct), + .protocol_type => @intFromEnum(lsp.CompletionItemKind.Interface), .type_alias => @intFromEnum(lsp.CompletionItemKind.Class), .param => @intFromEnum(lsp.CompletionItemKind.Variable), .namespace => @intFromEnum(lsp.CompletionItemKind.Module), @@ -790,11 +792,13 @@ pub const Server = struct { for (fd.params, 0..) |param, pi| { if (pi > 0) try detail_buf.appendSlice(allocator, ", "); try detail_buf.appendSlice(allocator, param.name); - try detail_buf.appendSlice(allocator, ": "); - if (param.type_expr.data == .type_expr) { - try detail_buf.appendSlice(allocator, param.type_expr.data.type_expr.name); - } else { - try detail_buf.appendSlice(allocator, "?"); + if (param.type_expr.data != .inferred_type) { + try detail_buf.appendSlice(allocator, ": "); + if (param.type_expr.data == .type_expr) { + try detail_buf.appendSlice(allocator, param.type_expr.data.type_expr.name); + } else { + try detail_buf.appendSlice(allocator, "?"); + } } } try detail_buf.append(allocator, ')'); @@ -838,6 +842,12 @@ pub const Server = struct { .kind = @intFromEnum(lsp.CompletionItemKind.Struct), }); }, + .protocol_decl => |pd| { + try items.append(allocator, .{ + .label = pd.name, + .kind = @intFromEnum(lsp.CompletionItemKind.Interface), + }); + }, .var_decl => |vd| { try items.append(allocator, .{ .label = vd.name, @@ -892,6 +902,43 @@ pub const Server = struct { } } } + } else if (sym.kind == .protocol_type) { + const lookup_root = if (sym.origin) |origin_path| + if (self.documents.get(origin_path)) |od| od.root orelse root else root + else + root; + if (sx.sema.findNodeAtOffset(lookup_root, sym.def_span.start)) |node| { + if (node.data == .protocol_decl) { + const pd = node.data.protocol_decl; + for (pd.methods) |method| { + // Build detail string: (params) -> ret + var detail_buf = std.ArrayList(u8).empty; + try detail_buf.append(self.allocator, '('); + for (method.param_names, 0..) |pname, pi| { + if (pi > 0) try detail_buf.appendSlice(self.allocator, ", "); + try detail_buf.appendSlice(self.allocator, pname); + if (pi < method.params.len) { + try detail_buf.appendSlice(self.allocator, ": "); + if (method.params[pi].data == .type_expr) { + try detail_buf.appendSlice(self.allocator, method.params[pi].data.type_expr.name); + } + } + } + try detail_buf.append(self.allocator, ')'); + if (method.return_type) |rt| { + try detail_buf.appendSlice(self.allocator, " -> "); + if (rt.data == .type_expr) { + try detail_buf.appendSlice(self.allocator, rt.data.type_expr.name); + } + } + try items.append(self.allocator, .{ + .label = method.name, + .kind = @intFromEnum(lsp.CompletionItemKind.Method), + .detail = detail_buf.items, + }); + } + } + } } break; } @@ -956,11 +1003,13 @@ pub const Server = struct { if (pi > 0) try label_buf.appendSlice(self.allocator, ", "); const param_start = label_buf.items.len; try label_buf.appendSlice(self.allocator, param.name); - try label_buf.appendSlice(self.allocator, ": "); - if (param.type_expr.data == .type_expr) { - try label_buf.appendSlice(self.allocator, param.type_expr.data.type_expr.name); - } else { - try label_buf.appendSlice(self.allocator, "?"); + if (param.type_expr.data != .inferred_type) { + try label_buf.appendSlice(self.allocator, ": "); + if (param.type_expr.data == .type_expr) { + try label_buf.appendSlice(self.allocator, param.type_expr.data.type_expr.name); + } else { + try label_buf.appendSlice(self.allocator, "?"); + } } const param_label = try self.allocator.dupe(u8, label_buf.items[param_start..]); try param_labels.append(self.allocator, param_label); @@ -1361,6 +1410,9 @@ pub const Server = struct { .kw_push, .kw_ufcs, .kw_in, + .kw_closure, + .kw_protocol, + .kw_impl, .hash_run, .hash_import, .hash_insert, @@ -1372,9 +1424,10 @@ pub const Server = struct { .hash_source, .hash_define, .hash_flags, + .hash_inline, => ST.keyword, - .kw_f32, .kw_f64, .kw_Type => ST.type_, + .kw_f32, .kw_f64, .kw_Type, .kw_Self => ST.type_, .int_literal, .float_literal => ST.number, .string_literal, .raw_string_literal => null, @@ -1480,6 +1533,7 @@ pub const Server = struct { .param => ST.parameter, .enum_type => ST.enum_, .struct_type => ST.struct_, + .protocol_type => ST.interface, .type_alias => ST.type_, .namespace => ST.namespace, }; @@ -1817,8 +1871,10 @@ pub const Server = struct { for (params, 0..) |param, pi| { if (pi > 0) try buf.appendSlice(self.allocator, ", "); try buf.appendSlice(self.allocator, param.name); - try buf.appendSlice(self.allocator, ": "); - if (param.type_expr.data == .type_expr) { + if (param.type_expr.data == .inferred_type) { + // Inferred type — show name only + } else if (param.type_expr.data == .type_expr) { + try buf.appendSlice(self.allocator, ": "); try buf.appendSlice(self.allocator, param.type_expr.data.type_expr.name); } else { try buf.appendSlice(self.allocator, "?"); @@ -2305,11 +2361,13 @@ pub const Server = struct { for (fd.params, 0..) |param, pi| { if (pi > 0) try buf.appendSlice(allocator, ", "); try buf.appendSlice(allocator, param.name); - try buf.appendSlice(allocator, ": "); - if (param.type_expr.data == .type_expr) { - try buf.appendSlice(allocator, param.type_expr.data.type_expr.name); - } else { - try buf.appendSlice(allocator, "?"); + if (param.type_expr.data != .inferred_type) { + try buf.appendSlice(allocator, ": "); + if (param.type_expr.data == .type_expr) { + try buf.appendSlice(allocator, param.type_expr.data.type_expr.name); + } else { + try buf.appendSlice(allocator, "?"); + } } } try buf.append(allocator, ')'); @@ -2407,6 +2465,44 @@ pub const Server = struct { } try buf.appendSlice(allocator, " }"); }, + .protocol_decl => |pd| { + try buf.appendSlice(allocator, pd.name); + try buf.appendSlice(allocator, " :: protocol"); + if (pd.is_inline) try buf.appendSlice(allocator, " #inline"); + try buf.appendSlice(allocator, " { "); + for (pd.methods, 0..) |method, mi| { + if (mi > 0) try buf.appendSlice(allocator, " "); + try buf.appendSlice(allocator, method.name); + try buf.appendSlice(allocator, " :: ("); + for (method.param_names, 0..) |pname, pi| { + if (pi > 0) try buf.appendSlice(allocator, ", "); + try buf.appendSlice(allocator, pname); + if (pi < method.params.len) { + try buf.appendSlice(allocator, ": "); + if (method.params[pi].data == .type_expr) { + try buf.appendSlice(allocator, method.params[pi].data.type_expr.name); + } else { + try buf.appendSlice(allocator, "?"); + } + } + } + try buf.append(allocator, ')'); + if (method.return_type) |rt| { + try buf.appendSlice(allocator, " -> "); + if (rt.data == .type_expr) { + try buf.appendSlice(allocator, rt.data.type_expr.name); + } + } + try buf.appendSlice(allocator, ";"); + } + try buf.appendSlice(allocator, " }"); + }, + .impl_block => |ib| { + try buf.appendSlice(allocator, "impl "); + try buf.appendSlice(allocator, ib.protocol_name); + try buf.appendSlice(allocator, " for "); + try buf.appendSlice(allocator, ib.target_type); + }, .const_decl => |cd| { try buf.appendSlice(allocator, cd.name); try buf.appendSlice(allocator, " :: "); @@ -2507,6 +2603,10 @@ pub const Server = struct { try buf.appendSlice(allocator, sym.name); try buf.appendSlice(allocator, " :: struct { ... }"); }, + .protocol_type => { + try buf.appendSlice(allocator, sym.name); + try buf.appendSlice(allocator, " :: protocol { ... }"); + }, .type_alias => { try buf.appendSlice(allocator, sym.name); try buf.appendSlice(allocator, " :: (type)"); diff --git a/src/lsp/types.zig b/src/lsp/types.zig index 4418331..8919337 100644 --- a/src/lsp/types.zig +++ b/src/lsp/types.zig @@ -106,7 +106,7 @@ pub fn initializeResultJson(allocator: std.mem.Allocator) ![]const u8 { "\"completionProvider\":{{\"triggerCharacters\":[\".\"]}}," ++ "\"signatureHelpProvider\":{{\"triggerCharacters\":[\"(\",\",\"]}}," ++ "\"semanticTokensProvider\":{{\"legend\":{{" ++ - "\"tokenTypes\":[\"namespace\",\"type\",\"enum\",\"struct\",\"parameter\",\"variable\",\"enumMember\",\"function\",\"keyword\",\"number\",\"string\",\"operator\"]," ++ + "\"tokenTypes\":[\"namespace\",\"type\",\"enum\",\"struct\",\"parameter\",\"variable\",\"enumMember\",\"function\",\"keyword\",\"number\",\"string\",\"operator\",\"interface\"]," ++ "\"tokenModifiers\":[\"declaration\",\"readonly\"]" ++ "}},\"full\":true}}," ++ "\"inlayHintProvider\":true}}}}", @@ -321,6 +321,7 @@ pub const SemanticTokenType = struct { pub const number: u32 = 9; pub const string_: u32 = 10; pub const operator_: u32 = 11; + pub const interface: u32 = 12; }; /// Build a SemanticTokens JSON response. diff --git a/src/parser.zig b/src/parser.zig index eb9a091..b283fbb 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -16,6 +16,8 @@ pub const Parser = struct { err_offset: ?u32 = null, prev_end: u32 = 0, diagnostics: ?*errors.DiagnosticList = null, + /// Type param names from enclosing generic struct (set while parsing methods) + struct_type_params: []const []const u8 = &.{}, pub fn init(allocator: std.mem.Allocator, source: [:0]const u8) Parser { var lexer = Lexer.init(source); @@ -75,8 +77,13 @@ pub const Parser = struct { return try self.createNode(start, .{ .comptime_expr = .{ .expr = expr } }); } + // impl Protocol for Type { methods } + if (self.current.tag == .kw_impl) { + return self.parseImplBlock(start); + } + // All top-level declarations start with an identifier - if (self.current.tag != .identifier) { + if (self.current.tag != .identifier and self.current.tag != .kw_Self) { return self.fail("expected identifier at top level"); } const name = self.tokenSlice(self.current); @@ -170,6 +177,11 @@ pub const Parser = struct { return self.parseStructDecl(name, start_pos); } + // Protocol declaration + if (self.current.tag == .kw_protocol) { + return self.parseProtocolDecl(name, start_pos); + } + // C-style union declaration if (self.current.tag == .kw_union) { return self.parseUnionDecl(name, start_pos); @@ -378,7 +390,7 @@ pub const Parser = struct { return try self.createNode(start, .{ .array_type_expr = .{ .length = len_node, .element_type = elem_type } }); } - // Generic type parameter introduction: $T + // Generic type parameter introduction: $T or $T/Protocol1/Protocol2 if (self.current.tag == .dollar) { self.advance(); if (self.current.tag != .identifier) { @@ -386,7 +398,18 @@ pub const Parser = struct { } const name = self.tokenSlice(self.current); self.advance(); - return try self.createNode(start, .{ .type_expr = .{ .name = name, .is_generic = true } }); + // Parse optional protocol constraints: $T/Eq/Hashable + var constraints = std.ArrayList([]const u8).empty; + while (self.current.tag == .slash) { + self.advance(); // skip '/' + if (self.current.tag != .identifier) { + return self.fail("expected protocol name after '/'"); + } + try constraints.append(self.allocator, self.tokenSlice(self.current)); + self.advance(); + } + const pc = try constraints.toOwnedSlice(self.allocator); + return try self.createNode(start, .{ .type_expr = .{ .name = name, .is_generic = true, .protocol_constraints = pc } }); } // Function type: (ParamTypes) -> ReturnType // Tuple type: (T1, T2) or (T1) — no '->' after ')' @@ -525,7 +548,12 @@ pub const Parser = struct { } }); } - return try self.createNode(start, .{ .type_expr = .{ .name = name } }); + // Mark as generic if name matches an enclosing struct's type param + var is_struct_generic = false; + for (self.struct_type_params) |tp| { + if (std.mem.eql(u8, tp, name)) { is_struct_generic = true; break; } + } + return try self.createNode(start, .{ .type_expr = .{ .name = name, .is_generic = is_struct_generic } }); } // Inline struct type in type position: struct { ... } if (self.current.tag == .kw_struct) { @@ -682,17 +710,38 @@ pub const Parser = struct { self.advance(); try self.expect(.colon); const constraint = try self.parseTypeExpr(); - try type_params.append(self.allocator, .{ .name = param_name, .constraint = constraint }); + // Parse optional protocol constraints: $T: Type/Eq/Hashable + var pc_list = std.ArrayList([]const u8).empty; + if (constraint.data == .type_expr and std.mem.eql(u8, constraint.data.type_expr.name, "Type")) { + while (self.current.tag == .slash) { + self.advance(); // skip '/' + if (self.current.tag != .identifier) { + return self.fail("expected protocol name after '/'"); + } + try pc_list.append(self.allocator, self.tokenSlice(self.current)); + self.advance(); + } + } + const pc = try pc_list.toOwnedSlice(self.allocator); + try type_params.append(self.allocator, .{ .name = param_name, .constraint = constraint, .protocol_constraints = pc }); } try self.expect(.r_paren); } try self.expect(.l_brace); + // Set struct type params context so method params can reference T without $ + var tp_names = std.ArrayList([]const u8).empty; + for (type_params.items) |tp| try tp_names.append(self.allocator, tp.name); + const saved_struct_type_params = self.struct_type_params; + self.struct_type_params = tp_names.items; + defer self.struct_type_params = saved_struct_type_params; + var field_names = std.ArrayList([]const u8).empty; var field_types = std.ArrayList(*Node).empty; var field_defaults = std.ArrayList(?*Node).empty; var using_entries = std.ArrayList(ast.UsingEntry).empty; + var methods = std.ArrayList(*Node).empty; while (self.current.tag != .r_brace and self.current.tag != .eof) { // Check for #using directive @@ -711,6 +760,20 @@ pub const Parser = struct { continue; } + // Method declaration: name :: (params) -> type { body } + if (self.current.tag == .identifier and self.peekNext() == .colon_colon) { + const method_start = self.current.loc.start; + const method_name = self.tokenSlice(self.current); + self.advance(); // skip name + self.advance(); // skip :: + if (self.current.tag == .l_paren and self.isFunctionDef()) { + try methods.append(self.allocator, try self.parseFnDecl(method_name, method_start)); + } else { + return self.fail("only function declarations are allowed inside struct bodies"); + } + continue; + } + // Parse field group: name1, name2, ...: type (= default)?; var group_names = std.ArrayList([]const u8).empty; @@ -764,6 +827,166 @@ pub const Parser = struct { .field_defaults = try field_defaults.toOwnedSlice(self.allocator), .type_params = try type_params.toOwnedSlice(self.allocator), .using_entries = try using_entries.toOwnedSlice(self.allocator), + .methods = try methods.toOwnedSlice(self.allocator), + } }); + } + + fn parseProtocolDecl(self: *Parser, name: []const u8, start_pos: u32) anyerror!*Node { + self.advance(); // skip 'protocol' + + // Check for #inline + var is_inline = false; + if (self.current.tag == .hash_inline) { + is_inline = true; + self.advance(); + } + + try self.expect(.l_brace); + + var methods = std.ArrayList(ast.ProtocolMethodDecl).empty; + + while (self.current.tag != .r_brace and self.current.tag != .eof) { + // Method: name :: (params) -> type; or name :: (params) -> type { body } + if (self.current.tag != .identifier) { + return self.fail("expected method name in protocol body"); + } + const method_name = self.tokenSlice(self.current); + self.advance(); + try self.expect(.colon_colon); + try self.expect(.l_paren); + + var param_types = std.ArrayList(*Node).empty; + var param_names = std.ArrayList([]const u8).empty; + + while (self.current.tag != .r_paren and self.current.tag != .eof) { + if (param_types.items.len > 0) { + try self.expect(.comma); + } + // Parse: name: type + if (self.current.tag != .identifier and self.current.tag != .kw_Self) { + return self.fail("expected parameter name in protocol method"); + } + const pname = self.tokenSlice(self.current); + self.advance(); + try self.expect(.colon); + const ptype = try self.parseTypeExpr(); + try param_names.append(self.allocator, pname); + try param_types.append(self.allocator, ptype); + } + try self.expect(.r_paren); + + // Optional return type + var return_type: ?*Node = null; + if (self.current.tag == .arrow) { + self.advance(); + return_type = try self.parseTypeExpr(); + } + + // Optional body (default method) or semicolon + var default_body: ?*Node = null; + if (self.current.tag == .l_brace) { + default_body = try self.parseBlock(); + } else { + if (self.current.tag == .semicolon) self.advance(); + } + + try methods.append(self.allocator, .{ + .name = method_name, + .params = try param_types.toOwnedSlice(self.allocator), + .param_names = try param_names.toOwnedSlice(self.allocator), + .return_type = return_type, + .default_body = default_body, + }); + } + + try self.expect(.r_brace); + + return try self.createNode(start_pos, .{ .protocol_decl = .{ + .name = name, + .methods = try methods.toOwnedSlice(self.allocator), + .is_inline = is_inline, + } }); + } + + fn parseImplBlock(self: *Parser, start_pos: u32) anyerror!*Node { + self.advance(); // skip 'impl' + + // Protocol name + if (self.current.tag != .identifier) { + return self.fail("expected protocol name after 'impl'"); + } + const protocol_name = self.tokenSlice(self.current); + self.advance(); + + // 'for' — note: 'for' is a keyword (kw_for), not an identifier + if (self.current.tag != .kw_for) { + return self.fail("expected 'for' after protocol name in impl block"); + } + self.advance(); + + // Target type name + if (self.current.tag != .identifier) { + return self.fail("expected type name after 'for'"); + } + const target_type = self.tokenSlice(self.current); + self.advance(); + + // Optional type params: impl Protocol for List($T) + var target_type_params = std.ArrayList(ast.StructTypeParam).empty; + if (self.current.tag == .l_paren) { + self.advance(); // skip '(' + while (self.current.tag != .r_paren and self.current.tag != .eof) { + if (target_type_params.items.len > 0) { + try self.expect(.comma); + } + try self.expect(.dollar); + if (self.current.tag != .identifier) { + return self.fail("expected type parameter name after '$'"); + } + const param_name = self.tokenSlice(self.current); + self.advance(); + // Optional constraint — for now just use Type + const constraint = try self.createNode(self.current.loc.start, .{ .type_expr = .{ .name = "Type" } }); + try target_type_params.append(self.allocator, .{ .name = param_name, .constraint = constraint }); + } + try self.expect(.r_paren); + } + + try self.expect(.l_brace); + + // Set struct type params context so method params can reference T without $ + var tp_names = std.ArrayList([]const u8).empty; + for (target_type_params.items) |tp| try tp_names.append(self.allocator, tp.name); + const saved_struct_type_params = self.struct_type_params; + self.struct_type_params = tp_names.items; + defer self.struct_type_params = saved_struct_type_params; + + var methods = std.ArrayList(*Node).empty; + + while (self.current.tag != .r_brace and self.current.tag != .eof) { + // Method: name :: (params) -> type { body } + if (self.current.tag != .identifier) { + return self.fail("expected method name in impl block"); + } + const method_start = self.current.loc.start; + const method_name = self.tokenSlice(self.current); + self.advance(); + try self.expect(.colon_colon); + + if (self.current.tag == .l_paren and self.isFunctionDef()) { + try methods.append(self.allocator, try self.parseFnDecl(method_name, method_start)); + } else { + return self.fail("expected function declaration in impl block"); + } + } + + try self.expect(.r_brace); + + return try self.createNode(start_pos, .{ .impl_block = .{ + .protocol_name = protocol_name, + .target_type = target_type, + .target_type_params = try target_type_params.toOwnedSlice(self.allocator), + .methods = try methods.toOwnedSlice(self.allocator), } }); } @@ -848,7 +1071,13 @@ pub const Parser = struct { const param_name = self.tokenSlice(self.current); const param_name_span = ast.Span{ .start = self.current.loc.start, .end = self.current.loc.end }; self.advance(); - try self.expect(.colon); + // Optional type annotation: if no ':', infer type from context + if (self.current.tag != .colon) { + const inferred_node = try self.createNode(param_name_span.start, .{ .inferred_type = {} }); + try params.append(self.allocator, .{ .name = param_name, .name_span = param_name_span, .type_expr = inferred_node }); + continue; + } + self.advance(); // consume ':' const is_variadic = self.current.tag == .dot_dot; if (is_variadic) self.advance(); const param_type = try self.parseTypeExpr(); @@ -856,7 +1085,18 @@ pub const Parser = struct { if (is_ct_param and param_type.data == .type_expr) { const constraint_name = param_type.data.type_expr.name; if (std.mem.eql(u8, constraint_name, "Type")) { - param_type.data = .{ .type_expr = .{ .name = param_name, .is_generic = true } }; + // Parse optional protocol constraints: $T: Type/Eq/Hashable + var constraints = std.ArrayList([]const u8).empty; + while (self.current.tag == .slash) { + self.advance(); // skip '/' + if (self.current.tag != .identifier) { + return self.fail("expected protocol name after '/'"); + } + try constraints.append(self.allocator, self.tokenSlice(self.current)); + self.advance(); + } + const pc = try constraints.toOwnedSlice(self.allocator); + param_type.data = .{ .type_expr = .{ .name = param_name, .is_generic = true, .protocol_constraints = pc } }; } else { is_comptime_param = true; } @@ -906,8 +1146,13 @@ pub const Parser = struct { for (generic_names.items) |gen_name| { if (!seen.contains(gen_name)) { try seen.put(gen_name, {}); + // Propagate protocol constraints from the TypeExpr if present + const pc = if (param.type_expr.data == .type_expr) + param.type_expr.data.type_expr.protocol_constraints + else + &[_][]const u8{}; const type_constraint = self.createNode(param.type_expr.span.start, .{ .type_expr = .{ .name = "Type" } }) catch continue; - type_params.append(self.allocator, .{ .name = gen_name, .constraint = type_constraint }) catch {}; + type_params.append(self.allocator, .{ .name = gen_name, .constraint = type_constraint, .protocol_constraints = pc }) catch {}; } } } @@ -1476,6 +1721,11 @@ pub const Parser = struct { self.advance(); return try self.createNode(start, .{ .identifier = .{ .name = name } }); }, + .kw_closure => { + // `closure` keyword used as identifier in expressions (closure intrinsic call) + self.advance(); + return try self.createNode(start, .{ .identifier = .{ .name = "closure" } }); + }, .dot => { self.advance(); // Anonymous struct literal: .{ ... } @@ -1946,7 +2196,9 @@ pub const Parser = struct { if (self.current.tag == .r_paren) break :blk true; // empty parens if (self.current.tag != .identifier) break :blk false; self.advance(); - break :blk self.current.tag == .colon; + break :blk self.current.tag == .colon or + self.current.tag == .comma or + self.current.tag == .r_paren; }; // Restore to '(' and scan past parens inline (not via peekPastParens which restores state) diff --git a/src/sema.zig b/src/sema.zig index fb3ae8b..2a0c9a0 100644 --- a/src/sema.zig +++ b/src/sema.zig @@ -16,6 +16,7 @@ pub const SymbolKind = enum { function, enum_type, struct_type, + protocol_type, type_alias, param, namespace, @@ -650,9 +651,10 @@ pub const Analyzer = struct { } // Built-in names that aren't declared in source - if (std.mem.eql(u8, name, "io")) return; - if (std.mem.eql(u8, name, "true") or std.mem.eql(u8, name, "false")) return; - if (std.mem.eql(u8, name, "cast")) return; + const builtins = [_][]const u8{ "io", "true", "false", "cast", "closure", "out", "size_of", "malloc", "free", "memcpy", "memset" }; + for (builtins) |b| { + if (std.mem.eql(u8, name, b)) return; + } try self.diagnostics.append(self.allocator, .{ .level = .warn, @@ -844,6 +846,7 @@ pub const Analyzer = struct { .param, .match_arm, .undef_literal, + .inferred_type, .builtin_expr, .foreign_expr, .library_decl, @@ -863,6 +866,30 @@ pub const Analyzer = struct { .slice_expr, .tuple_type_expr, => {}, + .protocol_decl => |pd| { + try self.addSymbol(pd.name, .protocol_type, null, node.span); + // Recurse into default method bodies + for (pd.methods) |method| { + if (method.default_body) |body| { + try self.pushScope(); + // `self` is implicit in protocol default methods + try self.addSymbol("self", .param, null, node.span); + for (method.param_names) |pname| { + try self.addSymbol(pname, .param, null, node.span); + } + try self.analyzeNode(body); + self.popScope(); + } + } + }, + .impl_block => |ib| { + // Each impl block gets its own scope so methods don't conflict across impls + try self.pushScope(); + for (ib.methods) |method_node| { + try self.analyzeNode(method_node); + } + self.popScope(); + }, .ufcs_alias => |ua| { // Register the alias name as a function and resolve the target try self.addSymbol(ua.name, .function, null, node.span); @@ -1205,6 +1232,7 @@ pub fn findNodeAtOffset(node: *Node, offset: u32) ?*Node { .param, .match_arm, .undef_literal, + .inferred_type, .builtin_expr, .foreign_expr, .library_decl, @@ -1228,6 +1256,21 @@ pub fn findNodeAtOffset(node: *Node, offset: u32) ?*Node { .ufcs_alias, .closure_type_expr, => {}, + .protocol_decl => |pd| { + for (pd.methods) |method| { + if (method.default_body) |body| { + if (findNodeAtOffset(body, offset)) |found| return found; + } + for (method.params) |param| { + if (findNodeAtOffset(param, offset)) |found| return found; + } + } + }, + .impl_block => |ib| { + for (ib.methods) |method_node| { + if (findNodeAtOffset(method_node, offset)) |found| return found; + } + }, .tuple_literal => |tl| { for (tl.elements) |elem| { if (findNodeAtOffset(elem.value, offset)) |found| return found; diff --git a/src/token.zig b/src/token.zig index e05878a..048be2c 100644 --- a/src/token.zig +++ b/src/token.zig @@ -32,6 +32,10 @@ pub const Tag = enum { kw_push, // push kw_ufcs, // ufcs kw_in, // in + kw_closure, // closure + kw_protocol, // protocol + kw_impl, // impl + kw_Self, // Self (in protocol declarations) // Symbols colon, // : @@ -103,6 +107,7 @@ pub const Tag = enum { hash_source, // #source (inside #import c { ... }) hash_define, // #define (inside #import c { ... }) hash_flags, // #flags (inside #import c { ... }) + hash_inline, // #inline (protocol layout modifier) triple_minus, // --- // Special @@ -169,7 +174,7 @@ pub const Tag = enum { pub fn isTypeKeyword(tag: Tag) bool { return switch (tag) { - .kw_f32, .kw_f64, .kw_Type => true, + .kw_f32, .kw_f64, .kw_Type, .kw_Self => true, else => false, }; } @@ -215,6 +220,10 @@ pub const keywords = std.StaticStringMap(Tag).initComptime(.{ .{ "push", .kw_push }, .{ "ufcs", .kw_ufcs }, .{ "in", .kw_in }, + .{ "closure", .kw_closure }, + .{ "protocol", .kw_protocol }, + .{ "impl", .kw_impl }, + .{ "Self", .kw_Self }, }); pub fn getKeyword(bytes: []const u8) ?Tag { diff --git a/src/types.zig b/src/types.zig index 3bf7f05..74ce309 100644 --- a/src/types.zig +++ b/src/types.zig @@ -200,6 +200,36 @@ pub const Type = union(enum) { }; } + /// Returns the canonical type name for this type, or null for complex types. + /// Used for looking up impl methods on non-struct types (e.g., s32.eq). + pub fn toName(self: Type) ?[]const u8 { + return switch (self) { + .signed => |w| switch (w) { + 8 => "s8", + 16 => "s16", + 32 => "s32", + 64 => "s64", + else => null, + }, + .unsigned => |w| switch (w) { + 8 => "u8", + 16 => "u16", + 32 => "u32", + 64 => "u64", + else => null, + }, + .f32 => "f32", + .f64 => "f64", + .boolean => "bool", + .string_type => "string", + .void_type => "void", + .struct_type => |n| n, + .enum_type => |n| n, + .union_type => |n| n, + else => null, + }; + } + pub fn fromTypeExpr(node: *Node) ?Type { if (node.data != .type_expr) return null; return fromName(node.data.type_expr.name); diff --git a/tests/expected/50-smoke.txt b/tests/expected/50-smoke.txt index f762c58..8643315 100644 --- a/tests/expected/50-smoke.txt +++ b/tests/expected/50-smoke.txt @@ -462,4 +462,102 @@ opt-closure: none opt-closure: 15 opt-closure-btn: 1 99 opt-closure-btn: null +closure-ptr: 3 +closure-enum: 2 +closure-rstr: [INFO] ok +closure-rstruct: 11 22 +closure-linear: 37 +closure-clamp: 0 100 255 +closure-compose2: 12 +closure-chain: 22 +closure-map: 3 6 9 12 15 +closure-filter: 3 [3 4 5] +closure-sort: 5 4 3 2 1 +closure-fe: item 0=10 +closure-fe: item 1=20 +closure-fe: item 2=30 +closure-find: 2 +closure-any: false true +closure-struct-field: -5 +closure-btn: 1 99 +closure-counter: 1 2 3 +closure-acc: 105 115 +closure-loop: 115 +closure-reassign: 11 +closure-reassign: 20 +closure-snapstruct: 15 +closure-cap-promoted: 11 +closure-iife: 15 +closure-toggle: none +closure-toggle: true +closure-panel: main 800x600 +closure-chain-call: true +closure-loop-0: 1 +closure-loop-1: 11 +closure-loop-4: 41 +closure-cond: 10 +closure-form: submitted +closure-form: no cancel +closure-null-env: true +closure-slice: 10 20 30 +closure-arena: 15 +closure-gpa: 17 allocs=0 +closure-opt: 42 +closure-ropt: 50 +closure-ropt: none +closure-mixed: 10 +closure-mixed: 15 +closure-mixed: 25 +closure-factory-indep: 20 30 40 +closure-deep-chain: 122 +closure-8cap: 36 +closure-4param: 10 +closure-shared-ptr: 7 +closure-f64: true +closure-zerocap: 49 true +closure-struct-method: 7 +closure-multi-factory: 10 +closure-multi-factory: 20 +closure-multi-factory: 30 +closure-bool-cap: true false +closure-as-arg: 142 +closure-strfmt: hello world +closure-ptr-before: 10 +closure-ptr-after: 42 +closure-neg: -70 +closure-proto-cap: true +closure-chain-factory: 37 +closure-while-cond: 3 +closure-infer: 7 +closure-infer-arg: 15 +closure-infer-block: 12 +closure-infer-cap: 105 +closure-infer-factory: 35 +closure-infer-compose: 11 +closure-infer-void: 42 +=== Protocols === +P1.1: 3 +P1.2: 30 +P2.1: 42 +P2.2: 150 +P2.3: 5 10 +P3.1: 5 +P3.2: 12 +hi hi +P4.1: 2 +yo yo +P4.2: 2 +P4.3: 6 2 +P5.1: true false +P5.2: 10 20 +P5.5: true false +P5.3: true false +P6.1: true false +P6.2: true false +P6.3: true false +P6.4: 40 +P6.5: 20 +P7.1: 30 +P7.2: 10 300 +P2.6: 5 10 === DONE ===