From f9dda972d214e856b066302b22d08ba28ed0591f Mon Sep 17 00:00:00 2001 From: agra Date: Thu, 5 Mar 2026 16:20:36 +0200 Subject: [PATCH] fixes --- examples/issue-0002.sx | 31 ++++++ examples/issue-0003.sx | 83 ++++++++++++++++ examples/issue-0004.sx | 43 ++++++++ examples/issue-0005.sx | 59 +++++++++++ examples/issue-0006.sx | 21 ++++ examples/issue-0007.sx | 55 +++++++++++ examples/issue-0008-bare.sx | 42 ++++++++ examples/issue-0008.sx | 52 ++++++++++ examples/issue-0009.sx | 23 +++++ examples/issue-0010.sx | 34 +++++++ examples/issue-0011.sx | 51 ++++++++++ examples/issue-0012.sx | 87 ++++++++++++++++ examples/issue-0013.sx | 36 +++++++ examples/issue-0014.sx | 15 +++ examples/issue-0015.sx | 28 ++++++ examples/issue-0016.sx | 17 ++++ examples/issue-0017.sx | 53 ++++++++++ examples/issue-0018.sx | 89 +++++++++++++++++ examples/issue-0019.sx | 42 ++++++++ examples/modules/opengl.sx | 2 + examples/modules/sdl3.sx | 2 + src/ast.zig | 4 + src/ir/emit_llvm.zig | 30 +++++- src/ir/inst.zig | 6 ++ src/ir/lower.zig | 99 ++++++++++++++++++- src/lsp/server.zig | 1 + src/parser.zig | 40 +++++++- src/token.zig | 2 + tests/expected/51-compound-assign-global.exit | 1 + tests/expected/51-compound-assign-global.txt | 10 ++ tests/expected/issue-0012.exit | 1 + tests/expected/issue-0012.txt | 6 ++ tests/expected/issue-0016.exit | 1 + tests/expected/issue-0016.txt | 1 + tests/expected/issue-0017.exit | 1 + tests/expected/issue-0017.txt | 2 + 36 files changed, 1063 insertions(+), 7 deletions(-) create mode 100644 examples/issue-0002.sx create mode 100644 examples/issue-0003.sx create mode 100644 examples/issue-0004.sx create mode 100644 examples/issue-0005.sx create mode 100644 examples/issue-0006.sx create mode 100644 examples/issue-0007.sx create mode 100644 examples/issue-0008-bare.sx create mode 100644 examples/issue-0008.sx create mode 100644 examples/issue-0009.sx create mode 100644 examples/issue-0010.sx create mode 100644 examples/issue-0011.sx create mode 100644 examples/issue-0012.sx create mode 100644 examples/issue-0013.sx create mode 100644 examples/issue-0014.sx create mode 100644 examples/issue-0015.sx create mode 100644 examples/issue-0016.sx create mode 100644 examples/issue-0017.sx create mode 100644 examples/issue-0018.sx create mode 100644 examples/issue-0019.sx create mode 100644 tests/expected/51-compound-assign-global.exit create mode 100644 tests/expected/51-compound-assign-global.txt create mode 100644 tests/expected/issue-0012.exit create mode 100644 tests/expected/issue-0012.txt create mode 100644 tests/expected/issue-0016.exit create mode 100644 tests/expected/issue-0016.txt create mode 100644 tests/expected/issue-0017.exit create mode 100644 tests/expected/issue-0017.txt diff --git a/examples/issue-0002.sx b/examples/issue-0002.sx new file mode 100644 index 0000000..fa38e9e --- /dev/null +++ b/examples/issue-0002.sx @@ -0,0 +1,31 @@ +// issue-0002: impl for built-in types fails with "expected type name after 'for'" +// +// `impl Protocol for f32` should work the same as `impl Protocol for MyStruct`. +// Currently the parser rejects built-in type names (f32, s64, bool, etc.) after `for`. + +Lerpable :: protocol #inline { + lerp :: (b: Self, t: f32) -> Self; +} + +impl Lerpable for f32 { + lerp :: (self: f32, b: f32, t: f32) -> f32 { self + (b - self) * t; } +} + +do_lerp :: (a: Lerpable, b: f32, t: f32) -> f32 { + a.lerp(b, t); +} + +main :: () -> void { + // Static call through impl + result := f32.lerp(0.0, 10.0, 0.5); + print("lerp(0, 10, 0.5) = {}\n", result); + + // Protocol dispatch through #inline erasure + val : f32 = 0.0; + p : *f32 = @val; + l : Lerpable = xx p; + result2 := do_lerp(l, 10.0, 0.25); + print("lerp(0, 10, 0.25) = {}\n", result2); +} + +#import "modules/std.sx"; diff --git a/examples/issue-0003.sx b/examples/issue-0003.sx new file mode 100644 index 0000000..430075d --- /dev/null +++ b/examples/issue-0003.sx @@ -0,0 +1,83 @@ +// issue-0003: Generic struct with protocol #inline constraint generates wrong LLVM types +// +// When `Animated($T: Lerpable)` is monomorphized with a struct type like `Size`, +// the LLVM IR generates `{}` (empty type) instead of the actual struct layout +// for the `T` parameter in methods like `set_immediate`, `animate_to`, and `lerp`. +// +// Error: "Call parameter type does not match function signature!" +// call void @Animated__0.set_immediate(ptr ..., { float, float } ...) +// expected {} but got { float, float } + +#import "modules/std.sx"; +#import "modules/math"; + +Lerpable :: protocol #inline { + lerp :: (b: Self, t: f32) -> Self; +} + +Size :: struct { + width, height: f32; + zero :: () -> Size => .{ width = 0.0, height = 0.0 }; +} + +impl Lerpable for Size { + lerp :: (self: Size, b: Size, t: f32) -> Size { + Size.{ width = self.width + (b.width - self.width) * t, + height = self.height + (b.height - self.height) * t }; + } +} + +Animated :: struct ($T: Lerpable) { + current: T; + from: T; + to: T; + elapsed: f32; + duration: f32; + active: bool; + + make :: (value: T) -> Animated(T) { + Animated(T).{ + current = value, from = value, to = value, + elapsed = 0.0, duration = 0.0, active = false + }; + } + + set_immediate :: (self: *Animated(T), value: T) { + self.current = value; + self.from = value; + self.to = value; + self.active = false; + } + + animate_to :: (self: *Animated(T), target: T, dur: f32) { + self.from = self.current; + self.to = target; + self.elapsed = 0.0; + self.duration = dur; + self.active = true; + } + + tick :: (self: *Animated(T), dt: f32) { + if !self.active { return; } + self.elapsed += dt; + t := clamp(self.elapsed / self.duration, 0.0, 1.0); + self.current = self.from.lerp(self.to, t); + if t >= 1.0 { + self.current = self.to; + self.active = false; + } + } +} + +main :: () -> void { + anim := Animated(Size).make(Size.zero()); + anim.set_immediate(Size.{ width = 100.0, height = 50.0 }); + print("after set: {}x{}\n", anim.current.width, anim.current.height); + + anim.animate_to(Size.{ width = 200.0, height = 100.0 }, 1.0); + anim.tick(0.5); + print("mid anim: {}x{}\n", anim.current.width, anim.current.height); + + anim.tick(0.5); + print("end anim: {}x{}\n", anim.current.width, anim.current.height); +} diff --git a/examples/issue-0004.sx b/examples/issue-0004.sx new file mode 100644 index 0000000..df6bad2 --- /dev/null +++ b/examples/issue-0004.sx @@ -0,0 +1,43 @@ +// issue-0004: scalar-to-vector conversion when all optional struct fields are null +// +// When a struct has multiple ?f32 fields and ALL are set to null simultaneously, +// passing that struct to a virtual function call triggers: +// "error: scalar-to-vector conversion failed" +// +// Setting at least one field to a concrete value works fine. + +#import "modules/std.sx"; + +ProposedSize :: struct { + width: ?f32; + height: ?f32; +} + +Sizable :: protocol { + size :: (proposal: ProposedSize) -> f32; +} + +Widget :: struct {} + +impl Sizable for Widget { + size :: (self: *Widget, proposal: ProposedSize) -> f32 { + w := if pw := proposal.width { pw; } else { 100.0; }; + h := if ph := proposal.height { ph; } else { 100.0; }; + w + h; + } +} + +main :: () -> void { + w := Widget.{}; + s : Sizable = w; + + // These work: + r1 := s.size(ProposedSize.{ width = 50.0, height = null }); + print("r1 = {}\n", r1); + r2 := s.size(ProposedSize.{ width = null, height = 50.0 }); + print("r2 = {}\n", r2); + + // This fails with "scalar-to-vector conversion failed": + r3 := s.size(ProposedSize.{ width = null, height = null }); + print("r3 = {}\n", r3); +} diff --git a/examples/issue-0005.sx b/examples/issue-0005.sx new file mode 100644 index 0000000..3c68ebd --- /dev/null +++ b/examples/issue-0005.sx @@ -0,0 +1,59 @@ +// issue-0005: optional f32 field in struct loses value when earlier optional is null +// +// FIXED: null_literal was double-wrapped as Some in struct literal coercion. +// Root cause: inferExprType(null) returns .void, coerceToType(.void, ?f32) +// tried to wrap the already-null value as Some, corrupting the struct. + +#import "modules/std.sx"; + +ProposedSize :: struct { + width: ?f32; + height: ?f32; +} + +// Direct function — does it work? +direct_size :: (proposal: ProposedSize) -> f32 { + w := if pw := proposal.width { pw; } else { 100.0; }; + h := if ph := proposal.height { ph; } else { 100.0; }; + w + h; +} + +Sizable :: protocol { + size :: (proposal: ProposedSize) -> f32; +} + +Widget :: struct {} + +impl Sizable for Widget { + size :: (self: *Widget, proposal: ProposedSize) -> f32 { + w := if pw := proposal.width { pw; } else { 100.0; }; + h := if ph := proposal.height { ph; } else { 100.0; }; + w + h; + } +} + +main :: () -> void { + // Test 1: Direct call + print("=== Direct calls ===\n"); + d1 := direct_size(ProposedSize.{ width = 50.0, height = null }); + print("d1 = {}\n", d1); + d2 := direct_size(ProposedSize.{ width = null, height = 50.0 }); + print("d2 = {}\n", d2); + d3 := direct_size(ProposedSize.{ width = null, height = null }); + print("d3 = {}\n", d3); + d4 := direct_size(ProposedSize.{ width = 50.0, height = 60.0 }); + print("d4 = {}\n", d4); + + // Test 2: Protocol dispatch + print("=== Protocol dispatch ===\n"); + w := Widget.{}; + s : Sizable = w; + r1 := s.size(ProposedSize.{ width = 50.0, height = null }); + print("r1 = {}\n", r1); + r2 := s.size(ProposedSize.{ width = null, height = 50.0 }); + print("r2 = {}\n", r2); + r3 := s.size(ProposedSize.{ width = null, height = null }); + print("r3 = {}\n", r3); + r4 := s.size(ProposedSize.{ width = 50.0, height = 60.0 }); + print("r4 = {}\n", r4); +} diff --git a/examples/issue-0006.sx b/examples/issue-0006.sx new file mode 100644 index 0000000..426184b --- /dev/null +++ b/examples/issue-0006.sx @@ -0,0 +1,21 @@ +// issue-0006: literal `0` in integer comparison inferred as float inside f32 ternary +// +// When `s64 != 0` is used as the condition of a ternary whose result type is f32, +// the literal `0` in the comparison leaks the ternary's f32 type instead of matching +// the LHS s64 type. This generates invalid LLVM IR: +// %icmp = icmp ne i64 %load, float 0.000000e+00 +// +// The same comparison works fine in a regular if-statement. + +#import "modules/std.sx"; + +main :: () -> void { + x : s64 = 42; + + // OK: comparison in statement context + if x != 0 { out("ok\n"); } + + // BUG: comparison as condition of f32 ternary — `0` inferred as f32 + result : f32 = if x != 0 then 1.0 else 2.0; + print("result = {}\n", result); +} diff --git a/examples/issue-0007.sx b/examples/issue-0007.sx new file mode 100644 index 0000000..e37bb9b --- /dev/null +++ b/examples/issue-0007.sx @@ -0,0 +1,55 @@ +// issue-0007: protocol value stores dangling pointer to stack local +// +// When a concrete value is converted to a protocol value inside a function, +// and the protocol value is stored in a List (via a wrapper struct), the +// protocol value's data pointer points to the stack-local variable rather +// than a heap-allocated copy. After the function returns, the pointer is +// dangling and method dispatch crashes (SIGSEGV/SIGBUS). +// +// Inside the function: dispatch works (stack local still alive) +// After the function returns: dispatch crashes (stack local gone) + +#import "modules/std.sx"; + +Sizable :: protocol { + size :: () -> s64; +} + +Widget :: struct { value: s64; } +impl Sizable for Widget { + size :: (self: *Widget) -> s64 { self.value; } +} + +// Wrapper struct with a protocol field (like ViewChild) +Item :: struct { + view: Sizable; +} + +Container :: struct { + items: List(Item); + + add :: (self: *Container, w: Widget) { + p := w; // local copy + self.items.append(Item.{ view = p }); // protocol created from stack local `p` + + // Works here: stack local `p` is still alive + out("inside add: "); + print("{}\n", self.items.items[self.items.len - 1].view.size()); + } +} + +main :: () -> void { + c : Container = .{}; + c.add(Widget.{ value = 42 }); + c.add(Widget.{ value = 99 }); + + // BUG: items[0] should return 42, but returns 99 (reads items[1]'s stack slot) + // Both protocol values point to the same stack address (the `p` local in add()) + r0 := c.items.items[0].view.size(); + r1 := c.items.items[1].view.size(); + print("items[0] = {} (expected 42)\n", r0); + print("items[1] = {} (expected 99)\n", r1); + + // With more stack activity between add() and the reads, this crashes + // (stack memory overwritten by other function calls) +} diff --git a/examples/issue-0008-bare.sx b/examples/issue-0008-bare.sx new file mode 100644 index 0000000..881bd9a --- /dev/null +++ b/examples/issue-0008-bare.sx @@ -0,0 +1,42 @@ +// Minimal: protocol dispatch on List(Protocol) items from a function + +#import "modules/std.sx"; + +Sizable :: protocol { + size :: () -> s64; +} + +Leaf :: struct { value: s64; } +impl Sizable for Leaf { + size :: (self: *Leaf) -> s64 { self.value; } +} + +add :: (items: *List(Sizable), w: Leaf) { + p := w; + items.append(p); +} + +dispatch_fn :: (items: *List(Sizable)) { + out("dispatch_fn: about to dispatch\n"); + s := items.items[0].size(); + print("dispatch_fn: result = {}\n", s); +} + +main :: () -> void { + items : List(Sizable) = .{}; + add(@items, Leaf.{ value = 42 }); + + // Direct dispatch twice + out("=== Direct 1 ===\n"); + r1 := items.items[0].size(); + print("r1 = {}\n", r1); + + out("=== Direct 2 ===\n"); + r2 := items.items[0].size(); + print("r2 = {}\n", r2); + + // Then from function + out("=== From function ===\n"); + dispatch_fn(@items); + out("=== OK ===\n"); +} diff --git a/examples/issue-0008.sx b/examples/issue-0008.sx new file mode 100644 index 0000000..681de28 --- /dev/null +++ b/examples/issue-0008.sx @@ -0,0 +1,52 @@ +// issue-0008: protocol value created in a function and appended to a list +// still stores a dangling stack pointer (issue-0007 fix incomplete) +// +// When a concrete value is converted to a protocol value inside a function +// (either implicitly via append or explicitly) and stored in a List, the +// protocol data pointer targets the function's stack frame instead of a +// heap-allocated copy. +// +// After the function returns, the first dispatch may succeed (stack not yet +// overwritten), but subsequent dispatches crash because the stack memory has +// been reused by other calls. +// +// STATUS: open — issue-0007 fix only covers some cases + +#import "modules/std.sx"; + +Sizable :: protocol { + size :: () -> s64; +} + +Leaf :: struct { value: s64; } +impl Sizable for Leaf { + size :: (self: *Leaf) -> s64 { self.value; } +} + +add :: (items: *List(Sizable), w: Leaf) { + p := w; + items.append(p); // protocol value created from stack local `p` +} + +main :: () -> void { + // Works: protocol value created in main, appended to list + out("=== Created in main ===\n"); + list_a : List(Sizable) = .{}; + s : Sizable = Leaf.{ value = 42 }; + list_a.append(s); + r1 := list_a.items[0].size(); + print("first: {} (expected 42)\n", r1); + r2 := list_a.items[0].size(); + print("second: {} (expected 42)\n", r2); + + // BUG: protocol value created in add(), first dispatch works, second crashes + out("=== Created in add() ===\n"); + list_b : List(Sizable) = .{}; + add(@list_b, Leaf.{ value = 99 }); + r3 := list_b.items[0].size(); + print("first: {} (expected 99)\n", r3); // works (stack not yet clobbered) + r4 := list_b.items[0].size(); + print("second: {} (expected 99)\n", r4); // CRASH: stack memory reused + + out("=== OK ===\n"); +} diff --git a/examples/issue-0009.sx b/examples/issue-0009.sx new file mode 100644 index 0000000..f46a541 --- /dev/null +++ b/examples/issue-0009.sx @@ -0,0 +1,23 @@ +// issue-0009: `push` with Context containing inline protocol triggers LLVM verification error +// +// LLVM verification failed: Invalid InsertValueInst operands! +// %si24 = insertvalue { ptr, ptr, ptr } undef, { ptr, ptr, ptr } %si21, 0 +// +// Context contains `allocator: Allocator` where `Allocator :: protocol #inline`. +// push saves/restores context as a value, but LLVM lowering mishandles the struct +// when the first field is an inline protocol cast from a different impl (Arena). + +#import "modules/std.sx"; +#import "modules/allocators.sx"; + +main :: () -> void { + arena : Arena = ---; + arena.create(context.allocator, 4096); + + new_ctx := Context.{ allocator = xx @arena, data = context.data }; + push new_ctx { + ptr := context.allocator.alloc(128); + out("inside push\n"); + } + out("after push\n"); +} diff --git a/examples/issue-0010.sx b/examples/issue-0010.sx new file mode 100644 index 0000000..831082a --- /dev/null +++ b/examples/issue-0010.sx @@ -0,0 +1,34 @@ +// issue-0010: Closure returning a protocol value generates invalid LLVM IR +// +// LLVM verification failed: Called function must be a pointer! +// %icall = call addrspace(64) i64 %load56() +// +// A Closure() -> MyProtocol where MyProtocol is a protocol (not #inline) +// fails at codegen. Calling the function directly works fine; only the +// closure dispatch path is broken. + +#import "modules/std.sx"; + +MyProtocol :: protocol { + get_value :: () -> s64; +} + +MyImpl :: struct { value: s64; } +impl MyProtocol for MyImpl { + get_value :: (self: *MyImpl) -> s64 { self.value; } +} + +make_thing :: () -> MyProtocol { + MyImpl.{ value = 42 }; +} + +main :: () -> void { + // Direct call works: + v := make_thing(); + out("direct call works\n"); + + // Closure call crashes: + c := closure(make_thing); + result := c(); + out("closure call works\n"); +} diff --git a/examples/issue-0011.sx b/examples/issue-0011.sx new file mode 100644 index 0000000..ae1a57e --- /dev/null +++ b/examples/issue-0011.sx @@ -0,0 +1,51 @@ +// issue-0011: Assigning to List(T).items corrupts adjacent memory when T is large +// +// Writing `list.items = xx 0` overwrites memory beyond the 8-byte items pointer +// when the List's element type T is larger than 32 bytes. The corruption spills +// into the struct field that follows the List in memory. +// +// Works correctly when size_of(T) <= 32. +// Fails when size_of(T) > 32 (e.g., 40 bytes). +// +// Likely cause: codegen confuses size_of(T) with size_of([*]T) when generating +// the store instruction for the items field. + +#import "modules/std.sx"; + +// 40-byte struct — triggers the bug. +// Shrink to [4]s64 (32 bytes) and the bug goes away. +BigNode :: struct { + data: [5]s64; // 40 bytes +} + +Tree :: struct { + nodes: List(BigNode); // items(8) + len(8) + cap(8) = 24 bytes + generation: s64; // 8 bytes — total 32 bytes +} + +Container :: struct { + tree: Tree; + sentinel: s64; + + do_work :: (self: *Container) { + self.tree.nodes.items = xx 0; // BUG: corrupts self.sentinel + } +} + +main :: () -> void { + obj : *Container = xx context.allocator.alloc(size_of(Container)); + memset(obj, 0, size_of(Container)); + obj.sentinel = 0xDEADBEEF; + + print("size_of BigNode = {}\n", size_of(BigNode)); + print("before: sentinel = {}\n", obj.sentinel); + + obj.do_work(); + + print("after: sentinel = {}\n", obj.sentinel); + if obj.sentinel != 0xDEADBEEF { + print("BUG: sentinel was corrupted!\n"); + } else { + out("OK\n"); + } +} diff --git a/examples/issue-0012.sx b/examples/issue-0012.sx new file mode 100644 index 0000000..f594a62 --- /dev/null +++ b/examples/issue-0012.sx @@ -0,0 +1,87 @@ +// issue-0012: Pattern match expression with mixed null/concrete arms +// +// A match expression with both `null` arms and concrete struct value arms +// should produce an optional type (?T) and correctly wrap non-null values. + +#import "modules/std.sx"; + +HAlignment :: enum { leading; center; trailing; } +VAlignment :: enum { top; center; bottom; } +Alignment :: struct { h: HAlignment; v: VAlignment; } + +ALIGN_CENTER :: Alignment.{ h = .center, v = .center }; +ALIGN_TOP :: Alignment.{ h = .center, v = .top }; +ALIGN_BOTTOM :: Alignment.{ h = .center, v = .bottom }; +ALIGN_LEADING :: Alignment.{ h = .leading, v = .center }; +ALIGN_TRAILING :: Alignment.{ h = .trailing, v = .center }; +ALIGN_TOP_LEADING :: Alignment.{ h = .leading, v = .top }; +ALIGN_TOP_TRAILING :: Alignment.{ h = .trailing, v = .top }; +ALIGN_BOTTOM_LEADING :: Alignment.{ h = .leading, v = .bottom }; +ALIGN_BOTTOM_TRAILING :: Alignment.{ h = .trailing, v = .bottom }; + +Zone :: enum { + floating; + fill; + center; + top; + bottom; + left; + right; + top_left; + top_right; + bottom_left; + bottom_right; +} + +// Match expression as implicit return with mixed null/concrete arms +zone_to_alignment :: (zone: Zone) -> ?Alignment { + if zone == { + case .floating: null; + case .fill: ALIGN_CENTER; + case .center: ALIGN_CENTER; + case .top: ALIGN_TOP; + case .bottom: ALIGN_BOTTOM; + case .left: ALIGN_LEADING; + case .right: ALIGN_TRAILING; + case .top_left: ALIGN_TOP_LEADING; + case .top_right: ALIGN_TOP_TRAILING; + case .bottom_left: ALIGN_BOTTOM_LEADING; + case .bottom_right: ALIGN_BOTTOM_TRAILING; + } +} + +// Side-effect match inside a function returning bool — must NOT be +// affected by optional inference (no null arms here) +NodeType :: enum { rect; text; image; } +process_node :: (t: NodeType) -> bool { + if t == { + case .rect: { out("rect\n"); } + case .text: { out("text\n"); } + case .image: { out("image\n"); } + } + true; +} + +main :: () -> void { + // Test null arm + r0 := zone_to_alignment(.floating); + if a := r0 { print("BUG: floating should be null, got h={}\n", xx a.h); } + else { out("ok: floating is null\n"); } + + // Test concrete arms + r1 := zone_to_alignment(.left); + if a := r1 { print("ok: left h={}\n", xx a.h); } + else { out("BUG: left returned null\n"); } + + r2 := zone_to_alignment(.center); + if a := r2 { print("ok: center h={}\n", xx a.h); } + else { out("BUG: center returned null\n"); } + + r3 := zone_to_alignment(.top_right); + if a := r3 { print("ok: top_right h={} v={}\n", xx a.h, xx a.v); } + else { out("BUG: top_right returned null\n"); } + + // Test side-effect match (no null arms) still works + process_node(.rect); + process_node(.text); +} diff --git a/examples/issue-0013.sx b/examples/issue-0013.sx new file mode 100644 index 0000000..641c48f --- /dev/null +++ b/examples/issue-0013.sx @@ -0,0 +1,36 @@ +// issue-0013: += on global variables reads initial value instead of current value +// +// `g_counter += 1` compiles as `store(initial_value + 1)` instead of +// `store(load(g_counter) + 1)`. So it always produces the same result. +// +// Workaround: use `g_counter = g_counter + 1` instead of `g_counter += 1` + +#import "modules/std.sx"; + +g_counter : s64 = 0; + +tick :: () { + g_counter += 1; + print("counter={}\n", g_counter); +} + +main :: () -> void { + // Test 1: += always produces 1 (BUG) + out("--- Test 1: += (broken) ---\n"); + out("Expected: 1, 2, 3\n"); + i : s64 = 0; + while i < 3 { + tick(); + i += 1; + } + + // Test 2: manual read-modify-write works correctly + out("--- Test 2: = x + 1 (works) ---\n"); + out("Expected: 2, 3, 4\n"); + g_counter = g_counter + 1; + print("counter={}\n", g_counter); + g_counter = g_counter + 1; + print("counter={}\n", g_counter); + g_counter = g_counter + 1; + print("counter={}\n", g_counter); +} diff --git a/examples/issue-0014.sx b/examples/issue-0014.sx new file mode 100644 index 0000000..29b7208 --- /dev/null +++ b/examples/issue-0014.sx @@ -0,0 +1,15 @@ +// issue-0014: Feature request — {{{ CONTENT_HASH }}} template variable for wasm shell +// +// When targeting wasm, the compiler processes shell.html and substitutes +// {{{ SCRIPT }}} with the +// +// +// This lets browsers cache until the next build, then bust automatically. +// No changes needed to build.sx or modules/compiler.sx — just the compiler +// recognizing the new placeholder during shell template substitution. diff --git a/examples/issue-0015.sx b/examples/issue-0015.sx new file mode 100644 index 0000000..1bcde2b --- /dev/null +++ b/examples/issue-0015.sx @@ -0,0 +1,28 @@ +// issue-0015: Global array variables with initializers contain all zeros at runtime. +// +// Expected: VALS[0]=-2, VALS[1]=-1, VALS[2]=42 +// Actual: VALS[0]=0, VALS[1]=0, VALS[2]=0 +// +// Global arrays declared with `: [N]T = .[...]` syntax get zero-initialized +// instead of receiving their specified values. + +#import "modules/std.sx"; + +VALS : [4]s32 = .[-2, -1, 42, 99]; + +main :: () { + out("VALS: "); + i := 0; + while i < 4 { + out(int_to_string(xx VALS[i])); + out(" "); + i = i + 1; + } + out("\n"); + + if VALS[0] == -2 and VALS[1] == -1 and VALS[2] == 42 and VALS[3] == 99 { + out("PASS\n"); + } else { + out("FAIL: global array not initialized\n"); + } +} diff --git a/examples/issue-0016.sx b/examples/issue-0016.sx new file mode 100644 index 0000000..674ab03 --- /dev/null +++ b/examples/issue-0016.sx @@ -0,0 +1,17 @@ +// issue-0016: C calling convention for function pointers passed to foreign callbacks. +// +// `callconv(.c)` ensures the function uses C ABI, so it can be safely +// passed as a callback to #foreign functions like SDL_AddEventWatch. + +#import "modules/std.sx"; + +// A function with C calling convention +add_c :: (a: s64, b: s64) -> s64 callconv(.c) { + a + b; +} + +main :: () { + // Call it directly — should work like any other function + result := add_c(10, 32); + print("callconv(.c): {}\n", result); +} diff --git a/examples/issue-0017.sx b/examples/issue-0017.sx new file mode 100644 index 0000000..9d59940 --- /dev/null +++ b/examples/issue-0017.sx @@ -0,0 +1,53 @@ +// issue-0017: Investigate data corruption in callconv(.c) callbacks +// when accessing struct methods on global pointers. + +#import "modules/std.sx"; + +Pipe :: struct { + pw: s32; + ph: s32; + frame: s32; + + resize :: (self: *Pipe, nw: s32, nh: s32) { + self.pw = nw; + self.ph = nh; + } + + tick :: (self: *Pipe) { + self.frame = self.frame + 1; + } +} + +g_pipe : *Pipe = ---; +g_width : s32 = 800; +g_height : s32 = 600; + +do_render :: () { + g_pipe.resize(g_width, g_height); + g_pipe.tick(); + print("wrapper: pw={}, ph={}, frame={}\n", g_pipe.pw, g_pipe.ph, g_pipe.frame); +} + +callback_inline :: (userdata: *void, code: s64) -> bool callconv(.c) { + g_width = xx code; + g_height = xx (code + 1); + g_pipe.resize(xx g_width, xx g_height); + g_pipe.tick(); + print("inline: pw={}, ph={}, frame={}\n", g_pipe.pw, g_pipe.ph, g_pipe.frame); + true; +} + +callback_wrapper :: (userdata: *void, code: s64) -> bool callconv(.c) { + g_width = xx code; + g_height = xx (code + 1); + do_render(); + true; +} + +main :: () { + pipe := Pipe.{ pw = 0, ph = 0, frame = 0 }; + g_pipe = @pipe; + + callback_inline(xx 0, 320); + callback_wrapper(xx 0, 640); +} diff --git a/examples/issue-0018.sx b/examples/issue-0018.sx new file mode 100644 index 0000000..105ccb7 --- /dev/null +++ b/examples/issue-0018.sx @@ -0,0 +1,89 @@ +// issue-0018: Dot-shorthand `.{...}` for struct with protocol field causes +// LLVM verification error when used in List(T).append from 2+ different +// struct methods. +// +// Trigger: TWO or more structs each with `List(Container)` calling +// `.append(.{ child = d })` — using dot-shorthand. +// +// Works: Only 1 struct doing it, or using explicit `Container.{ child = d }`. +// Fails: 2+ structs → `Invalid InsertValueInst operands!` +// `%si = insertvalue i64 undef, { ptr, ptr } %load, 0` +// +// Likely a monomorphization issue in `List(T).append` when the dot-shorthand +// type inference is resolved from multiple call sites. + +#import "modules/std.sx"; + +Drawable :: protocol { + draw :: () -> s32; + name :: () -> string; + layout :: (x: s32) -> s32; + handle :: (event: s32) -> bool; +} + +Circle :: struct { radius: s32; } +impl Drawable for Circle { + draw :: (self: *Circle) -> s32 { self.radius; } + name :: (self: *Circle) -> string { "circle"; } + layout :: (self: *Circle, x: s32) -> s32 { x + self.radius; } + handle :: (self: *Circle, event: s32) -> bool { event > 0; } +} + +Square :: struct { side: s32; } +impl Drawable for Square { + draw :: (self: *Square) -> s32 { self.side * self.side; } + name :: (self: *Square) -> string { "square"; } + layout :: (self: *Square, x: s32) -> s32 { x + self.side; } + handle :: (self: *Square, event: s32) -> bool { event > 1; } +} + +Rect :: struct { + x: f32; + y: f32; + w: f32; + h: f32; + zero :: () -> Rect { Rect.{ x = 0.0, y = 0.0, w = 0.0, h = 0.0 }; } +} + +Container :: struct { + child: Drawable; + computed_frame: Rect = .zero(); +} + +// Two different structs, each with List(Container), both calling .append(.{...}) +// This mirrors VStack/HStack in the game. + +StackA :: struct { + children: List(Container); + + add :: (self: *StackA, d: Drawable) { + // BUG: `.{ child = d }` causes LLVM error when 2+ structs do this + self.children.append(.{ child = d }); + } +} + +StackB :: struct { + children: List(Container); + + add :: (self: *StackB, d: Drawable) { + // BUG: second struct doing `.{ child = d }` triggers the error + self.children.append(.{ child = d }); + // FIX: explicit `Container.{ child = d }` works + // self.children.append(Container.{ child = d }); + } +} + +main :: () -> void { + c := Circle.{ radius = 42 }; + s := Square.{ side = 5 }; + + a : StackA = .{}; + a.add(c); + print("StackA: draw={}\n", a.children.items[0].child.draw()); + + b : StackB = .{}; + b.add(s); + print("StackB: draw={}\n", b.children.items[0].child.draw()); + + print("OK\n"); +} diff --git a/examples/issue-0019.sx b/examples/issue-0019.sx new file mode 100644 index 0000000..2308ebf --- /dev/null +++ b/examples/issue-0019.sx @@ -0,0 +1,42 @@ +// issue-0019: #import c symbols are globally visible instead of scoped to the importing file +// +// When a file uses `#import c { #include "foo.h"; #source "foo.c"; }`, +// the C symbols become available to ALL files in the compilation unit, +// not just the file that imported them. +// +// This means a file can call C functions without importing the module +// that declares them, as long as some other file in the project does. +// +// Expected: C symbols from `#import c` should only be visible in files +// that directly (or transitively via SX #import) import the module. +// +// Repro: +// - a.sx: `#import c { #include "some_lib.h"; #source "some_lib.c"; };` +// - b.sx: does NOT import a.sx, but calls some_lib_function() — compiles successfully +// +// In the game project: +// - main.sx imports modules/stb_truetype.sx (which has #import c for kbts/stbtt) +// - ui/glyph_cache.sx does NOT import modules/stb_truetype.sx +// - ui/glyph_cache.sx calls kbts_ShapeRun, stbtt_InitFont, etc. — compiles fine +// - If main.sx removed the import, glyph_cache.sx would break + +// Minimal repro structure (two files): + +// --- module_with_c.sx --- +// #import c { +// #include "vendors/some_lib.h"; +// #source "vendors/some_lib.c"; +// }; +// +// uses_c :: () -> s32 { +// some_lib_function(); +// } + +// --- main.sx (this file) --- +// #import "module_with_c.sx"; +// +// main :: () -> s32 { +// // This should fail because we never imported the C module directly, +// // but currently it compiles: +// some_lib_function(); +// } diff --git a/examples/modules/opengl.sx b/examples/modules/opengl.sx index a70b11a..c1b3888 100644 --- a/examples/modules/opengl.sx +++ b/examples/modules/opengl.sx @@ -30,6 +30,7 @@ glClear : (u32) -> void = ---; glEnable : (u32) -> void = ---; glDisable : (u32) -> void = ---; glViewport : (s32, s32, s32, s32) -> void = ---; +glFlush : () -> void = ---; glDrawArrays : (u32, s32, s32) -> void = ---; glPolygonMode : (u32, u32) -> void = ---; glLineWidth : (f32) -> void = ---; @@ -101,6 +102,7 @@ load_gl :: (get_proc: ([*]u8) -> *void) { glEnable = xx get_proc("glEnable"); glDisable = xx get_proc("glDisable"); glViewport = xx get_proc("glViewport"); + glFlush = xx get_proc("glFlush"); glDrawArrays = xx get_proc("glDrawArrays"); glPolygonMode = xx get_proc("glPolygonMode"); glLineWidth = xx get_proc("glLineWidth"); diff --git a/examples/modules/sdl3.sx b/examples/modules/sdl3.sx index 249b085..a4c9b1e 100644 --- a/examples/modules/sdl3.sx +++ b/examples/modules/sdl3.sx @@ -332,12 +332,14 @@ SDL_GL_SwapWindow :: (window: *void) -> bool #foreign sdl3; SDL_GL_SetSwapInterval :: (interval: s32) -> bool #foreign sdl3; SDL_GL_GetProcAddress :: (proc: [:0]u8) -> *void #foreign sdl3; SDL_PollEvent :: (event: *SDL_Event) -> bool #foreign sdl3; +SDL_AddEventWatch :: (filter: *void, userdata: *void) -> bool #foreign sdl3; SDL_GetTicks :: () -> u64 #foreign sdl3; SDL_GetPerformanceCounter :: () -> u64 #foreign sdl3; SDL_GetPerformanceFrequency :: () -> u64 #foreign sdl3; SDL_Delay :: (ms: u32) -> void #foreign sdl3; SDL_GetWindowDisplayScale :: (window: *void) -> f32 #foreign sdl3; SDL_GetWindowSize :: (window: *void, w: *s32, h: *s32) -> bool #foreign sdl3; +SDL_SetWindowSize :: (window: *void, w: s32, h: s32) -> bool #foreign sdl3; SDL_GetWindowSizeInPixels :: (window: *void, w: *s32, h: *s32) -> bool #foreign sdl3; SDL_Rect :: struct { diff --git a/src/ast.zig b/src/ast.zig index 38c2818..2fdc4dc 100644 --- a/src/ast.zig +++ b/src/ast.zig @@ -102,6 +102,8 @@ pub const Root = struct { decls: []const *Node, }; +pub const CallingConvention = enum { default, c }; + pub const FnDecl = struct { name: []const u8, params: []const Param, @@ -109,6 +111,7 @@ pub const FnDecl = struct { body: *Node, type_params: []const StructTypeParam = &.{}, is_arrow: bool = false, + call_conv: CallingConvention = .default, }; pub const Param = struct { @@ -323,6 +326,7 @@ pub const Lambda = struct { return_type: ?*Node, body: *Node, type_params: []const StructTypeParam = &.{}, + call_conv: CallingConvention = .default, }; pub const TypeExpr = struct { diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index ed28856..4a5dedd 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -282,6 +282,7 @@ pub const LLVMEmitter = struct { .float => |v| c.LLVMConstReal(llvm_ty, v), .boolean => |v| c.LLVMConstInt(llvm_ty, @intFromBool(v), 0), .string => |sid| self.emitConstStringGlobal(self.ir_mod.types.getString(sid)), + .aggregate => |agg| self.emitConstAggregate(agg, llvm_ty), .vtable => c.LLVMConstNull(llvm_ty), // placeholder — initialized in initVtableGlobals after function declarations else => c.LLVMConstNull(llvm_ty), }; @@ -354,15 +355,16 @@ pub const LLVMEmitter = struct { // main always returns i32 at the LLVM level (JIT expects it) const raw_ret_ty = self.toLLVMType(func.ret); - const ret_ty = if (is_main) self.cached_i32 else if (func.is_extern) self.abiCoerceParamType(func.ret, raw_ret_ty) else raw_ret_ty; + const needs_c_abi = func.is_extern or func.call_conv == .c; + const ret_ty = if (is_main) self.cached_i32 else if (needs_c_abi) self.abiCoerceParamType(func.ret, raw_ret_ty) else raw_ret_ty; - // Build parameter types — apply C ABI coercion for foreign (extern) functions + // Build parameter types — apply C ABI coercion for foreign/callconv(.c) functions const param_count: c_uint = @intCast(func.params.len); const param_types = self.alloc.alloc(c.LLVMTypeRef, func.params.len) catch unreachable; defer self.alloc.free(param_types); for (func.params, 0..) |param, j| { const llvm_ty = self.toLLVMType(param.ty); - param_types[j] = if (func.is_extern) self.abiCoerceParamType(param.ty, llvm_ty) else llvm_ty; + param_types[j] = if (needs_c_abi) self.abiCoerceParamType(param.ty, llvm_ty) else llvm_ty; } const fn_type = c.LLVMFunctionType(ret_ty, param_types.ptr, param_count, 0); @@ -378,6 +380,11 @@ pub const LLVMEmitter = struct { .private => c.LLVMSetLinkage(llvm_func, c.LLVMPrivateLinkage), } + // Set calling convention + if (func.call_conv == .c) { + c.LLVMSetFunctionCallConv(llvm_func, c.LLVMCCallConv); + } + // Add frame-pointer and nounwind attributes for correct ARM64 codegen { const fp_kind = "frame-pointer"; @@ -2840,6 +2847,23 @@ pub const LLVMEmitter = struct { return c.LLVMConstStructInContext(self.context, &fields, 2, 0); } + fn emitConstAggregate(self: *LLVMEmitter, agg: []const ir_inst.ConstantValue, llvm_ty: c.LLVMTypeRef) c.LLVMValueRef { + const elem_ty = c.LLVMGetElementType(llvm_ty); + const n: c_uint = @intCast(agg.len); + const vals = self.alloc.alloc(c.LLVMValueRef, agg.len) catch return c.LLVMConstNull(llvm_ty); + defer self.alloc.free(vals); + for (agg, 0..) |cv, i| { + vals[i] = switch (cv) { + .int => |v| c.LLVMConstInt(elem_ty, @bitCast(v), 1), + .float => |v| c.LLVMConstReal(elem_ty, v), + .boolean => |v| c.LLVMConstInt(elem_ty, @intFromBool(v), 0), + .aggregate => |inner| self.emitConstAggregate(inner, elem_ty), + else => c.LLVMConstNull(elem_ty), + }; + } + return c.LLVMConstArray(elem_ty, vals.ptr, n); + } + fn emitStringConstant(self: *LLVMEmitter, str: []const u8) c.LLVMValueRef { // LLVMBuildGlobalStringPtr needs a null-terminated C string const str_z = self.alloc.dupeZ(u8, str) catch unreachable; diff --git a/src/ir/inst.zig b/src/ir/inst.zig index 19fd2f7..cde75e3 100644 --- a/src/ir/inst.zig +++ b/src/ir/inst.zig @@ -410,6 +410,7 @@ pub const Function = struct { is_extern: bool = false, is_comptime: bool = false, linkage: Linkage = .internal, + call_conv: CallingConvention = .default, pub const Param = struct { name: StringId, @@ -422,6 +423,11 @@ pub const Function = struct { private, }; + pub const CallingConvention = enum { + default, + c, + }; + pub fn init(name: StringId, params: []const Param, ret: TypeId) Function { return .{ .name = name, diff --git a/src/ir/lower.zig b/src/ir/lower.zig index c3e3a09..79a9640 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -452,6 +452,7 @@ pub const Lowering = struct { .bool_literal => |bl| .{ .boolean = bl.value }, .float_literal => |fl| .{ .float = fl.value }, .string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) }, + .array_literal => |al| self.constArrayLiteral(al.elements), else => null, } else null; const gid = self.module.addGlobal(.{ @@ -467,6 +468,31 @@ pub const Lowering = struct { } } + /// Try to convert an array literal's elements into a compile-time ConstantValue.aggregate. + /// Returns null if any element is not a compile-time constant. + fn constArrayLiteral(self: *Lowering, elements: []const *const Node) ?inst_mod.ConstantValue { + const vals = self.alloc.alloc(inst_mod.ConstantValue, elements.len) catch return null; + for (elements, 0..) |elem, i| { + vals[i] = switch (elem.data) { + .int_literal => |il| .{ .int = il.value }, + .bool_literal => |bl| .{ .boolean = bl.value }, + .float_literal => |fl| .{ .float = fl.value }, + .string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) }, + .unary_op => |uo| switch (uo.op) { + .negate => switch (uo.operand.data) { + .int_literal => |il| .{ .int = -il.value }, + .float_literal => |fl| .{ .float = -fl.value }, + else => return null, + }, + else => return null, + }, + .array_literal => |al| self.constArrayLiteral(al.elements) orelse return null, + else => return null, + }; + } + return .{ .aggregate = vals }; + } + /// Pass 2: Lower main function body and comptime side-effects. fn lowerMainAndComptime(self: *Lowering, decls: []const *const Node) void { for (decls) |decl| { @@ -514,19 +540,23 @@ pub const Lowering = struct { }) catch unreachable; } + const cc: Function.CallingConvention = if (fd.call_conv == .c) .c else .default; + // For #foreign with C name override, declare under C name and map sx name → C name if (fd.body.data == .foreign_expr) { const fe = fd.body.data.foreign_expr; if (fe.c_name) |c_name| { const c_name_id = self.module.types.internString(c_name); - _ = self.builder.declareExtern(c_name_id, params.items, ret_ty); + const fid = self.builder.declareExtern(c_name_id, params.items, ret_ty); + self.module.getFunctionMut(fid).call_conv = cc; self.foreign_name_map.put(name, c_name) catch {}; return; } } const name_id = self.module.types.internString(name); - _ = self.builder.declareExtern(name_id, params.items, ret_ty); + const fid = self.builder.declareExtern(name_id, params.items, ret_ty); + self.module.getFunctionMut(fid).call_conv = cc; } /// Lazily lower a function body on demand. Called when lowerCall can't find @@ -607,6 +637,7 @@ pub const Lowering = struct { } func.is_extern = false; // promote from extern stub to real function func.linkage = if (std.mem.eql(u8, name, "main")) .external else .internal; + if (fd.call_conv == .c) func.call_conv = .c; // Set inst_counter to param count (params occupy refs 0..N-1) std.debug.assert(func.params.len == fd.params.len); // AST and IR param counts must match self.builder.inst_counter = @intCast(func.params.len); @@ -717,6 +748,11 @@ pub const Lowering = struct { self.builder.currentFunc().linkage = .external; } + // Set calling convention + if (fd.call_conv == .c) { + self.builder.currentFunc().call_conv = .c; + } + // Create entry block const entry_name = self.module.types.internString("entry"); const entry = self.builder.appendBlock(entry_name, &.{}); @@ -1070,12 +1106,14 @@ pub const Lowering = struct { // Set target_type from LHS for RHS lowering (enum literals, struct literals, etc.) const old_target = self.target_type; if (asgn.target.data == .identifier) { + var found_local = false; if (self.scope) |scope| { if (scope.lookup(asgn.target.data.identifier.name)) |binding| { self.target_type = binding.ty; + found_local = true; } } - if (self.target_type == null) { + if (!found_local) { if (self.global_names.get(asgn.target.data.identifier.name)) |gi| { self.target_type = gi.ty; } @@ -4387,6 +4425,9 @@ pub const Lowering = struct { }; const name_id = self.module.types.internString(name); const func_id = self.builder.beginFunction(name_id, params.items, ret_ty); + if (lam.call_conv == .c) { + self.module.getFunctionMut(func_id).call_conv = .c; + } // Create entry block const entry_name = self.module.types.internString("entry"); @@ -6436,6 +6477,58 @@ pub const Lowering = struct { /// Resolve parameter types for a call expression (for target_type context). /// Returns empty slice if the function can't be resolved. fn resolveCallParamTypes(self: *Lowering, c: *const ast.Call) []const TypeId { + // Method calls: obj.method(args) — resolve param types from the method signature, + // skipping the first param (self) since it's prepended later. + if (c.callee.data == .field_access) { + const fa = c.callee.data.field_access; + const obj_ty = self.inferExprType(fa.object); + if (self.getStructTypeName(obj_ty)) |sname| { + const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sname, fa.field }) catch return &.{}; + // Try already-lowered functions first + if (self.resolveFuncByName(qualified)) |fid| { + const func = &self.module.functions.items[@intFromEnum(fid)]; + if (func.params.len > 0) { + // Skip self param — caller args don't include self + var types_list = std.ArrayList(TypeId).empty; + for (func.params[1..]) |p| { + types_list.append(self.alloc, p.ty) catch unreachable; + } + return types_list.items; + } + } + // Try AST map (not yet lowered) + if (self.fn_ast_map.get(qualified)) |fd| { + if (fd.params.len > 0) { + var types_list = std.ArrayList(TypeId).empty; + for (fd.params[1..]) |p| { + types_list.append(self.alloc, self.resolveParamType(&p)) catch unreachable; + } + return types_list.items; + } + } + // Try generic struct template method: List__Container.append → List.append + // with type bindings from the struct instantiation + if (self.struct_instance_template.get(sname)) |tmpl_name| { + const tmpl_qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ tmpl_name, fa.field }) catch return &.{}; + if (self.fn_ast_map.get(tmpl_qualified)) |fd| { + if (fd.params.len > 0) { + // Temporarily set type_bindings so resolveParamType can substitute T → concrete type + const saved_bindings = self.type_bindings; + if (self.struct_instance_bindings.getPtr(sname)) |bindings| { + self.type_bindings = bindings.*; + } + var types_list = std.ArrayList(TypeId).empty; + for (fd.params[1..]) |p| { + types_list.append(self.alloc, self.resolveParamType(&p)) catch unreachable; + } + self.type_bindings = saved_bindings; + return types_list.items; + } + } + } + } + return &.{}; + } if (c.callee.data != .identifier) return &.{}; const bare_name = c.callee.data.identifier.name; const name = blk: { diff --git a/src/lsp/server.zig b/src/lsp/server.zig index 318225c..e45c78c 100644 --- a/src/lsp/server.zig +++ b/src/lsp/server.zig @@ -1478,6 +1478,7 @@ pub const Server = struct { .kw_protocol, .kw_impl, .kw_inline, + .kw_callconv, .hash_run, .hash_import, .hash_insert, diff --git a/src/parser.zig b/src/parser.zig index c8f932e..654a4fe 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -1220,6 +1220,24 @@ pub const Parser = struct { return_type = try self.parseTypeExpr(); } + // Optional calling convention: callconv(.c) + var call_conv: ast.CallingConvention = .default; + if (self.current.tag == .kw_callconv) { + self.advance(); + try self.expect(.l_paren); + try self.expect(.dot); + if (self.current.tag != .identifier) + return self.fail("expected calling convention name after '.'"); + const cc_name = self.tokenSlice(self.current); + if (std.mem.eql(u8, cc_name, "c")) { + call_conv = .c; + } else { + return self.fail("unknown calling convention"); + } + self.advance(); + try self.expect(.r_paren); + } + // Body: block `{ ... }`, arrow `=> expr;`, #builtin, #compiler, or #foreign marker var is_arrow = false; const body = if (self.current.tag == .hash_builtin) blk: { @@ -1273,6 +1291,7 @@ pub const Parser = struct { .body = body, .type_params = type_params, .is_arrow = is_arrow, + .call_conv = call_conv, } }); } @@ -2333,6 +2352,24 @@ pub const Parser = struct { return_type = try self.parseTypeExpr(); } + // Optional calling convention: callconv(.c) + var call_conv: ast.CallingConvention = .default; + if (self.current.tag == .kw_callconv) { + self.advance(); + try self.expect(.l_paren); + try self.expect(.dot); + if (self.current.tag != .identifier) + return self.fail("expected calling convention name after '.'"); + const cc_name = self.tokenSlice(self.current); + if (std.mem.eql(u8, cc_name, "c")) { + call_conv = .c; + } else { + return self.fail("unknown calling convention"); + } + self.advance(); + try self.expect(.r_paren); + } + // Two body forms: // (params) => expr — expression lambda // (params) { stmts } — block-body lambda @@ -2348,6 +2385,7 @@ pub const Parser = struct { .return_type = return_type, .body = body, .type_params = type_params, + .call_conv = call_conv, } }); } @@ -2365,7 +2403,7 @@ pub const Parser = struct { fn isFunctionDef(self: *Parser) bool { const tag = self.peekPastParens() orelse return false; - return tag == .l_brace or tag == .arrow or tag == .hash_builtin or tag == .hash_compiler or tag == .hash_foreign or tag == .fat_arrow; + return tag == .l_brace or tag == .arrow or tag == .hash_builtin or tag == .hash_compiler or tag == .hash_foreign or tag == .fat_arrow or tag == .kw_callconv; } fn isAssignOp(self: *const Parser) bool { diff --git a/src/token.zig b/src/token.zig index b3d832e..909cefd 100644 --- a/src/token.zig +++ b/src/token.zig @@ -37,6 +37,7 @@ pub const Tag = enum { kw_impl, // impl kw_Self, // Self (in protocol declarations) kw_inline, // inline (compile-time if/for/while) + kw_callconv, // callconv (calling convention annotation) // Symbols colon, // : @@ -227,6 +228,7 @@ pub const keywords = std.StaticStringMap(Tag).initComptime(.{ .{ "impl", .kw_impl }, .{ "Self", .kw_Self }, .{ "inline", .kw_inline }, + .{ "callconv", .kw_callconv }, }); pub fn getKeyword(bytes: []const u8) ?Tag { diff --git a/tests/expected/51-compound-assign-global.exit b/tests/expected/51-compound-assign-global.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/51-compound-assign-global.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/51-compound-assign-global.txt b/tests/expected/51-compound-assign-global.txt new file mode 100644 index 0000000..c8efda1 --- /dev/null +++ b/tests/expected/51-compound-assign-global.txt @@ -0,0 +1,10 @@ +add: 13 +sub: 5 +mul: 60 +div: 5 +mod: 1 +and: 15 +or: 255 +xor: 240 +shl: 64 +shr: 8 diff --git a/tests/expected/issue-0012.exit b/tests/expected/issue-0012.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/issue-0012.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/issue-0012.txt b/tests/expected/issue-0012.txt new file mode 100644 index 0000000..ff6c111 --- /dev/null +++ b/tests/expected/issue-0012.txt @@ -0,0 +1,6 @@ +ok: floating is null +ok: left h=0 +ok: center h=1 +ok: top_right h=2 v=0 +rect +text diff --git a/tests/expected/issue-0016.exit b/tests/expected/issue-0016.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/issue-0016.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/issue-0016.txt b/tests/expected/issue-0016.txt new file mode 100644 index 0000000..e1a9e66 --- /dev/null +++ b/tests/expected/issue-0016.txt @@ -0,0 +1 @@ +callconv(.c): 42 diff --git a/tests/expected/issue-0017.exit b/tests/expected/issue-0017.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/issue-0017.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/issue-0017.txt b/tests/expected/issue-0017.txt new file mode 100644 index 0000000..07339f5 --- /dev/null +++ b/tests/expected/issue-0017.txt @@ -0,0 +1,2 @@ +inline: pw=320, ph=321, frame=1 +wrapper: pw=640, ph=641, frame=2