This commit is contained in:
agra
2026-03-05 16:20:36 +02:00
parent 22bc2439ce
commit f9dda972d2
36 changed files with 1063 additions and 7 deletions

31
examples/issue-0002.sx Normal file
View File

@@ -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";

83
examples/issue-0003.sx Normal file
View File

@@ -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);
}

43
examples/issue-0004.sx Normal file
View File

@@ -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);
}

59
examples/issue-0005.sx Normal file
View File

@@ -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);
}

21
examples/issue-0006.sx Normal file
View File

@@ -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);
}

55
examples/issue-0007.sx Normal file
View File

@@ -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)
}

View File

@@ -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");
}

52
examples/issue-0008.sx Normal file
View File

@@ -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");
}

23
examples/issue-0009.sx Normal file
View File

@@ -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");
}

34
examples/issue-0010.sx Normal file
View File

@@ -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");
}

51
examples/issue-0011.sx Normal file
View File

@@ -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");
}
}

87
examples/issue-0012.sx Normal file
View File

@@ -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);
}

36
examples/issue-0013.sx Normal file
View File

@@ -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);
}

15
examples/issue-0014.sx Normal file
View File

@@ -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 <script> tag. Add a {{{ CONTENT_HASH }}} variable
// that is a short hash (e.g. 8-char hex) of the final build outputs
// (.js + .wasm + .data), so the shell can use it for cache busting:
//
// <script>
// Module.locateFile=function(path){return path+'?v={{{ CONTENT_HASH }}}'};
// </script>
// <script async src="index.js?v={{{ CONTENT_HASH }}}"></script>
//
// 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.

28
examples/issue-0015.sx Normal file
View File

@@ -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");
}
}

17
examples/issue-0016.sx Normal file
View File

@@ -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);
}

53
examples/issue-0017.sx Normal file
View File

@@ -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);
}

89
examples/issue-0018.sx Normal file
View File

@@ -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");
}

42
examples/issue-0019.sx Normal file
View File

@@ -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();
// }

View File

@@ -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");

View File

@@ -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 {