test: group examples into per-category folders

Move examples/*.sx and their expected/ snapshots into per-category
subfolders (examples/<category>/...). Folder = leading filename token,
with ffi-objc/ffi-jni kept whole; filenames are unchanged. The corpus
runner and LSP sweep now discover each category's expected/ dir, while
issues/ stays flat. Example 1058's repo-root-relative companion import
is made file-relative. Path strings embedded in 164 snapshots were
regenerated (path-only changes). Test-layout docs in CLAUDE.md updated.
This commit is contained in:
agra
2026-06-21 14:41:34 +03:00
parent 6d1409bc1f
commit 66bdc70bf1
3357 changed files with 456 additions and 363 deletions

View File

@@ -0,0 +1,14 @@
#import "modules/std.sx";
main :: () {
list : List(i32) = .{};
list.append(1);
list.append(3);
list.append(4);
list.append(1);
list.append(5);
print("{}\n", list);
}

View File

@@ -0,0 +1,31 @@
#import "modules/std.sx";
main :: () {
arr : [5]i32 = .[3, 1, 4, 1, 5];
print("arr.len = {}\n", arr.len);
// subslice array
sub := arr[1..4];
print("arr[1..4] = {}\n", sub);
print("sub.len = {}\n", sub.len);
// open-ended
head := arr[..3];
tail := arr[2..];
print("arr[..3] = {}\n", head);
print("arr[2..] = {}\n", tail);
// slice of slice
sl : []i32 = .[10, 20, 30, 40, 50];
mid := sl[1..4];
print("sl[1..4] = {}\n", mid);
rest := mid[1..];
print("mid[1..] = {}\n", rest);
// string subslicing
msg := "hello world";
word := msg[6..11];
print("msg[6..11] = {}\n", word);
prefix := msg[..5];
print("msg[..5] = {}\n", prefix);
}

View File

@@ -0,0 +1,28 @@
#import "modules/std.sx";
Vec2 :: struct { x, y: f32; }
set_x :: (p: *Vec2, val: f32) {
p.x = val;
}
main :: () {
v := Vec2.{ 1.0, 2.0 };
print("before: {}\n", v);
set_x(@v, 99.0);
print("after: {}\n", v);
ptr := @v;
copy := ptr.*;
print("copy: {}\n", copy);
// null pointer
np : *Vec2 = null;
// many-pointer indexing
arr : [5]i32 = .[10, 20, 30, 40, 50];
mp : [*]i32 = @arr[0];
print("mp[0] = {}\n", mp[0]);
print("mp[2] = {}\n", mp[2]);
}

View File

@@ -0,0 +1,17 @@
// `push <Context>` where Context's first field is an `#inline` protocol
// (`allocator: Allocator`) and the value being pushed is an `Arena` upcast to
// that protocol. Exercises save/restore of the boxed context across the push.
#import "modules/std.sx";
#import "modules/std/mem.sx";
main :: () -> void {
arena := Arena.init(context.allocator, 4096);
new_ctx := Context.{ allocator = xx arena, data = context.data };
push new_ctx {
ptr := context.allocator.alloc_bytes(128);
out("inside push\n");
}
out("after push\n");
}

View File

@@ -0,0 +1,42 @@
// `xx` cast inside an RHS expression assigned to a struct field takes its
// target type from the field, not from the enclosing function's return type.
// Covers if-then-else RHS and binary-op RHS variants.
#import "modules/std.sx";
Foo :: struct {
pixel_w: i32;
dpi: f32;
last_perf: i64;
delta_time: f32;
}
FC :: struct { a: f32; b: f32; c: i32; d: i32; e: f32; f: f32; }
// If-then-else RHS in a function whose return type is not f32.
calc_bool :: (self: *Foo, wf: f32) -> bool {
self.dpi = if wf > 0.0 then xx self.pixel_w / wf else 1.0;
true
}
// Binary-op RHS in a struct-returning function. The xx casts must target f32,
// not the FC return-struct shape.
begin :: (self: *Foo, current: i64, freq: i64) -> FC {
if self.last_perf > 0 {
self.delta_time = xx (current - self.last_perf) / xx freq;
}
FC.{ a = 1.0, b = 2.0, c = 3, d = 4, e = 5.0, f = 6.0 }
}
main :: () -> void {
f : *Foo = xx libc_malloc(size_of(Foo));
f.pixel_w = 2880;
f.dpi = 0.0;
f.last_perf = 1000;
f.delta_time = 0.0;
_ := calc_bool(f, 1440.0);
print("dpi={}\n", f.dpi);
fc := begin(f, 1500, 1000);
print("delta={} fc.a={}\n", f.delta_time, fc.a);
}

View File

@@ -0,0 +1,20 @@
// Phase 3 (xx-via-Into): a user-defined `impl Into(Target) for Source`
// reaches the xx operator through compile-time dispatch. The compiler
// monomorphises `convert` for the (Source, Target) pair and emits a
// direct call — no vtable, identical to a hand-written call.
#import "modules/std.sx";
MyString :: struct { tag: i64 = 0; }
impl Into(MyString) for i64 {
convert :: (self: i64) -> MyString {
.{ tag = self }
}
}
main :: () -> i32 {
x : MyString = xx 42;
print("tag = {}\n", x.tag);
0
}

View File

@@ -0,0 +1,47 @@
// Inline `xx` cast as the first argument to a struct static method must
// flow the leading param's type into the cast — otherwise an `xx ptr`
// targeting a protocol param falls back to i64 and the call frame is
// corrupted, SIGTRAPping when the body dispatches through the field.
//
// Three call shapes that must all succeed:
// 1. Named-variable receiver: `a : Allocator = xx p; T.init(a, ...)`
// 2. Free function with inline xx: `make_t(xx p, ...)`
// 3. Static method with inline xx: `T.init(xx p, ...)` ← used to crash
#import "modules/std.sx";
#import "modules/std/mem.sx";
Box :: struct {
parent: Allocator;
first_ptr: *void;
init :: (parent_alloc: Allocator, size: i64) -> *Box {
self : *Box = xx libc_malloc(size_of(Box));
self.parent = parent_alloc;
self.first_ptr = self.parent.alloc_bytes(size);
self
}
}
make_box :: (parent_alloc: Allocator, size: i64) -> *Box {
self : *Box = xx libc_malloc(size_of(Box));
self.parent = parent_alloc;
self.first_ptr = self.parent.alloc_bytes(size);
self
}
main :: () -> i32 {
g_gpa : GPA = .{ alloc_count = 0 };
a : Allocator = xx @g_gpa;
b1 := Box.init(a, 64);
print("Box.init (named-var): ok\n");
b2 := make_box(xx @g_gpa, 64);
print("make_box (inline-xx): ok\n");
b3 := Box.init(xx @g_gpa, 64);
print("Box.init (inline-xx): ok\n");
0
}

View File

@@ -0,0 +1,27 @@
// `xx allocator` recovers the typed concrete pointer (ctx) from a
// protocol value. The recovery is read-only and must not perturb
// subsequent dispatch through the protocol value, regardless of
// whether the recovery happens BEFORE or AFTER the first dispatch.
#import "modules/std.sx";
#import "modules/std/mem.sx"; // `Allocator` is non-transitive: name it, import it.
main :: () -> i32 {
gpa := GPA.init();
a : Allocator = xx gpa;
// Recover BEFORE first dispatch.
recovered : *GPA = xx a;
print("recovered == gpa? {}\n", recovered == @gpa);
p := a.alloc_bytes(64);
print("alloc count after first alloc: {}\n", gpa.alloc_count);
// Recover AFTER dispatch — still works.
recovered2 : *GPA = xx a;
print("recovered2 == gpa? {}\n", recovered2 == @gpa);
a.dealloc_bytes(p);
print("alloc count after dealloc: {}\n", gpa.alloc_count);
0
}

View File

@@ -0,0 +1,45 @@
// Phase 1.1 — the compiler-internal heap-copy that backs `xx <rvalue>`
// protocol erasure must dispatch through `context.allocator`, not call
// libc malloc directly. So when a `push Context.{ allocator = tracer }`
// block is active, a `xx StructLiteral.{}` inside it MUST be allocated
// by the tracker.
//
// Note: `xx` only heap-copies for RVALUES (struct literals, call results).
// `xx <lvalue>` (an identifier, field access, index, or deref) borrows
// the operand's storage, so it never allocates and never reaches this
// path. See specs.md §3 — Protocol value ownership and lifetime.
#import "modules/std.sx";
#import "modules/std/mem.sx"; // `Allocator` is non-transitive: name it, import it.
Tracer :: struct {
count: i64;
init :: () -> *Tracer {
t : *Tracer = xx libc_malloc(size_of(Tracer));
t.count = 0;
t
}
}
impl Allocator for Tracer {
alloc_bytes :: (self: *Tracer, size: i64) -> *void {
self.count += 1;
return libc_malloc(size);
}
dealloc_bytes :: (self: *Tracer, ptr: *void) {
libc_free(ptr);
}
}
ByValue :: struct { x: i64; y: i64; }
main :: () -> i32 {
tracer := Tracer.init();
push Context.{ allocator = xx tracer, data = null } {
// Struct-literal operand: rvalue → heap-copy through context.allocator.
ignore : Allocator = xx ByValue.{ x = 1, y = 2 };
_ = ignore;
}
print("Tracer.count = {}\n", tracer.count);
0
}

View File

@@ -0,0 +1,27 @@
// Option 3 — `xx <lvalue>` borrows the operand's storage instead of
// heap-copying. The protocol value's `ctx` points directly at the local;
// mutations through the protocol are visible to the original.
//
// The witness is TrackingAllocator: incrementing the parent allocator's
// counter happens through the Allocator protocol value. If `xx tracker`
// heap-copied the Tracker, the parent counter would land in the copy
// and the local would stay at zero. With Option 3 the local sees the
// increments because they ARE the local.
#import "modules/std.sx";
#import "modules/std/mem.sx";
main :: () -> i32 {
gpa := GPA.init();
tracker := TrackingAllocator.init(xx gpa); // value, stack-local
// xx tracker — operand is an identifier (lvalue), so the protocol
// borrows tracker's storage. No heap copy. Mutations propagate.
push Context.{ allocator = xx tracker, data = null } {
p := context.allocator.alloc_bytes(128);
context.allocator.dealloc_bytes(p);
}
print("alloc_count = {}\n", tracker.alloc_count);
print("dealloc_count = {}\n", tracker.dealloc_count);
return 0;
}

View File

@@ -0,0 +1,20 @@
#import "modules/std.sx";
#import "modules/math";
#import "modules/build.sx";
#import "modules/std/test.sx";
pkg :: #import "tests/fixtures/testpkg";
main :: () {
// ========================================================
// 17. SLICE/ARRAY .ptr ACCESS
// ========================================================
print("=== 17. Slice Ptr ===\n");
{
sarr : [5]i32 = .[10, 20, 30, 40, 50];
ssl := sarr[1..4];
sp := ssl.ptr;
print("sl-ptr[0]: {}\n", sp[0]);
print("sl-ptr[1]: {}\n", sp[1]);
}
}

View File

@@ -0,0 +1,68 @@
// Typed allocation helpers over the Allocator protocol (std/mem.sx):
// create/destroy (one T), alloc/free (slices), clone, resize, and the
// bytes-level mem_realloc. Declared `ufcs` — the dot spelling
// (`context.allocator.create(Session)`), the pipe spelling, and the
// direct call all hit the same generic machinery. Contents are
// UNINITIALISED by design
// (Zig-aligned): assign before reading. TrackingAllocator balances to
// zero across every pair.
#import "modules/std.sx";
#import "modules/std/mem.sx";
Session :: struct { id: i64; score: i64; }
main :: () {
gpa := GPA.init();
tracker := TrackingAllocator.init(xx gpa);
a : Allocator = xx tracker;
// create / destroy — direct spelling
s := create(a, Session);
s.id = 7; s.score = 42;
print("create: {} {}\n", s.id, s.score);
destroy(a, s);
// create — fluent pipe spelling
p := a |> create(Session);
p.id = 1;
print("pipe-create: {}\n", p.id);
a |> destroy(p);
// create — canonical dot spelling on context.allocator
q := context.allocator.create(Session);
q.id = 2;
print("dot-create: {}\n", q.id);
context.allocator.destroy(q);
// alloc / free — typed slice
xs := a |> alloc(i64, 4);
xs[0] = 10; xs[1] = 20; xs[2] = 30; xs[3] = 40;
print("alloc: {} {} len={}\n", xs[0], xs[3], xs.len);
// clone — independent copy (canonical dot spelling)
ys := xs.clone(a);
xs[0] = 99;
print("clone: {} (orig {})\n", ys[0], xs[0]);
a.free(ys);
// resize — grow (copies, old backing freed)
zs := xs |> resize(a, 6);
zs[5] = 60;
print("resize: {} {} len={}\n", zs[1], zs[5], zs.len);
// resize — shrink
ws := zs |> resize(a, 2);
print("shrink: {} {} len={}\n", ws[0], ws[1], ws.len);
a |> free(ws);
// mem_realloc — bytes level
raw := a.alloc_bytes(8);
q : *i64 = xx raw;
q.* = 1234;
raw2 := mem_realloc(a, raw, 8, 16, 8);
q2 : *i64 = xx raw2;
print("realloc: {}\n", q2.*);
a.dealloc_bytes(raw2);
tracker.report();
}

View File

@@ -0,0 +1,26 @@
// BufAlloc.init returns the state BY VALUE: the caller's local is the
// allocator state and the FULL buffer is usable. Regression: init used
// to carve its own struct off the buffer's head (returning *BufAlloc
// into the buffer), so a 128-byte buffer could only serve 104 bytes —
// the second 64-byte allocation below failed with null.
#import "modules/std.sx";
#import "modules/std/mem.sx";
main :: () {
stack_buf : [128]u8 = ---;
buf := BufAlloc.init(@stack_buf[0], 128);
a : Allocator = xx buf;
b1 := a.alloc_bytes(64);
b2 := a.alloc_bytes(64); // fills the buffer EXACTLY
b1_ok := b1 != null;
b2_ok := b2 != null;
print("full capacity: {} {} pos={}\n", b1_ok, b2_ok, buf.pos);
b3 := a.alloc_bytes(1); // one byte over — must fail
print("over: {}\n", b3 == null);
buf.reset();
print("reset: {}\n", buf.pos);
}

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
List__i32{items: [*]i32@0xADDR, len: 5, cap: 8}

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,9 @@
arr.len = 5
arr[1..4] = [1, 4, 1]
sub.len = 3
arr[..3] = [3, 1, 4]
arr[2..] = [4, 1, 5]
sl[1..4] = [20, 30, 40]
mid[1..] = [30, 40]
msg[6..11] = world
msg[..5] = hello

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,5 @@
before: Vec2{x: 1.000000, y: 2.000000}
after: Vec2{x: 99.000000, y: 2.000000}
copy: Vec2{x: 99.000000, y: 2.000000}
mp[0] = 10
mp[2] = 30

View File

@@ -0,0 +1,2 @@
inside push
after push

View File

@@ -0,0 +1,2 @@
dpi=2.000000
delta=0.500000 fc.a=1.000000

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
tag = 42

View File

@@ -0,0 +1,3 @@
Box.init (named-var): ok
make_box (inline-xx): ok
Box.init (inline-xx): ok

View File

@@ -0,0 +1,4 @@
recovered == gpa? true
alloc count after first alloc: 1
recovered2 == gpa? true
alloc count after dealloc: 0

View File

@@ -0,0 +1 @@
Tracer.count = 1

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,2 @@
alloc_count = 1
dealloc_count = 1

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,3 @@
=== 17. Slice Ptr ===
sl-ptr[0]: 20
sl-ptr[1]: 30

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,9 @@
create: 7 42
pipe-create: 1
dot-create: 2
alloc: 10 40 len=4
clone: 10 (orig 99)
resize: 20 60 len=6
shrink: 99 20 len=2
realloc: 1234
TrackingAllocator: allocs=8 deallocs=8 outstanding=0 total_alloc_bytes=184

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,3 @@
full capacity: true true pos=128
over: true
reset: 0