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,9 @@
#import "modules/std.sx";
main :: () {
fx :: (s:i3) -> i3 {
s
}
print("{}\n", fx(-3));
}

View File

@@ -0,0 +1,28 @@
#import "modules/std.sx";
add :: (a: i32, b: i32) -> i32 { a + b }
mul :: (a: i32, b: i32) -> i32 { a * b }
apply :: (f: (i32, i32) -> i32, x: i32, y: i32) -> i32 {
f(x, y)
}
main :: () {
// Store function in variable
fp : (i32, i32) -> i32 = add;
print("fp(3,4) = {}\n", fp(3, 4));
// Reassign to different function
fp = mul;
print("fp(3,4) = {}\n", fp(3, 4));
// Pass function pointer as argument
print("apply(add,5,6) = {}\n", apply(add, 5, 6));
print("apply(mul,5,6) = {}\n", apply(mul, 5, 6));
}
// ** stdout **
//fp(3,4) = 7
//fp(3,4) = 12
//apply(add,5,6) = 11
//apply(mul,5,6) = 30

View File

@@ -0,0 +1,96 @@
#import "modules/std.sx";
// --- Closure Basics ---
// Factory: returns a new closure each time
make_adder :: (n: i64) -> Closure(i64) -> i64 {
return closure((x: i64) -> i64 => x + n);
}
// Higher-order function: accepts any Closure(i64) -> i64
apply :: (f: Closure(i64) -> i64, x: i64) -> i64 { return f(x); }
// Reduce: fold over a slice with a closure
reduce :: (arr: []i64, f: Closure(i64, i64) -> i64, init: i64) -> i64 {
acc := init;
i : i64 = 0;
while i < arr.len { acc = f(acc, arr[i]); i += 1; }
return acc;
}
// Auto-promoted bare function
triple :: (x: i64) -> i64 { return x * 3; }
// Struct with optional closure callback
Widget :: struct {
name: string;
on_update: ?Closure(i64) -> void;
}
main :: () {
// 1. Basic closure with capture
offset := 100;
add_offset := closure((x: i64) -> i64 => x + offset);
print("basic: {}\n", add_offset(42));
// 2. Capture by value (snapshot semantics)
n := 10;
snap := closure((x: i64) -> i64 => x + n);
n = 999;
print("snapshot: {}\n", snap(5));
// 3. Block-body closure with control flow
clamp := closure((x: i64) -> i64 {
if x < 0 { return 0; }
if x > 100 { return 100; }
return x;
});
print("clamp: {} {} {}\n", clamp(50), clamp(0 - 10), clamp(200));
// 4. Void closure with string capture
tag := "INFO";
logger := closure((msg: string) {
print("[{}] {}\n", tag, msg);
});
logger("system ready");
// 5. Factory pattern
add5 := make_adder(5);
add10 := make_adder(10);
print("factory: {} {}\n", add5(100), add10(100));
// 6. Auto-promotion: bare fn passed where Closure expected
print("auto-promote: {}\n", apply(triple, 7));
// 7. Closure passed to higher-order function
factor := 4;
print("hof: {}\n", apply(closure((x: i64) -> i64 => x * factor), 10));
// 8. Reduce with closure
nums : []i64 = .[1, 2, 3, 4, 5];
total := reduce(nums, closure((acc: i64, x: i64) -> i64 => acc + x), 0);
print("reduce: {}\n", total);
// 9. Closure captures closure
inner := closure((x: i64) -> i64 => x + 10);
outer := closure((x: i64) -> i64 => inner(x) * 2);
print("compose: {}\n", outer(5));
// 10. Multiple closures from same scope
base := 100;
cl_add := closure((x: i64) -> i64 => x + base);
cl_mul := closure((x: i64) -> i64 => x * base);
print("multi: {} {}\n", cl_add(5), cl_mul(5));
// 11. Optional closures
w1 := Widget.{ name = "slider", on_update = closure((val: i64) {
print("widget: {} = {}\n", "slider", val);
}) };
w2 := Widget.{ name = "label", on_update = null };
if h := w1.on_update { h(42); }
if h := w2.on_update { h(0); } else { print("widget: no handler\n"); }
print("=== DONE ===\n");
}

View File

@@ -0,0 +1,28 @@
// Closure whose return type is a (non-`#inline`) protocol value — exercises
// the indirect-call path where the result is a boxed protocol.
#import "modules/std.sx";
MyProtocol :: protocol {
get_value :: (self: *Self) -> i64;
}
MyImpl :: struct { value: i64; }
impl MyProtocol for MyImpl {
get_value :: (self: *MyImpl) -> i64 { 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");
}

View File

@@ -0,0 +1,20 @@
// A closure stored in a struct field receives sub-32-bit enum args
// with the right tag, same as direct or protocol-dispatched calls.
#import "modules/std.sx";
Fmt :: enum { a; b; }
Ctx :: struct {
on: Closure(Fmt) -> void;
}
main :: () -> i32 {
c : Ctx = .{ on = (f: Fmt) => {
n : i64 = xx f;
print("cl f = {}\n", n);
}};
c.on(.b);
c.on(.a);
0
}

View File

@@ -0,0 +1,40 @@
// Invoking a Closure-typed struct field as `self.field()` from a
// method whose receiver is `*Self`. The field access must auto-deref
// the pointer before extracting the closure value.
#import "modules/std.sx";
Holder :: struct {
cb: Closure() = ---;
has: bool = false;
set :: (self: *Holder, fn: Closure()) {
self.cb = fn;
self.has = true;
}
// Direct invocation through *self.
call_direct :: (self: *Holder) {
if self.has == false { return; }
self.cb();
}
// Hoist-then-call form — must agree with the direct form.
call_hoisted :: (self: *Holder) {
if self.has == false { return; }
fn := self.cb;
fn();
}
}
ticks : i32 = 0;
main :: () -> i32 {
h : Holder = .{};
h.set(() => { ticks += 1; });
h.call_direct();
h.call_hoisted();
return ticks;
}

View File

@@ -0,0 +1,47 @@
// Phase 1.3 — the closure env-buffer heap-copy in `lowerLambda` must
// dispatch through `context.allocator`, not `.heap_alloc` directly.
// So when a `push Context.{ allocator = tracer }` block is active, a
// capturing closure created inside it MUST allocate its env through
// the tracker.
//
// Mirrors the shape of `130-xx-value-routes-through-context-allocator.sx`
// for the protocol-erasure heap path — same Tracer, same install via
// `push Context`, same `Tracer.count = 1` assertion. Different
// allocation site (closure env vs xx-value heap copy).
#import "modules/std.sx";
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);
}
}
main :: () -> i32 {
tracer := Tracer.init();
push Context.{ allocator = xx tracer, data = null } {
// Capturing closure. lowerLambda allocates an env struct on the
// stack, copies the captures in, then heap-copies the env via
// `allocViaContext` — which dispatches through the installed
// tracer's `alloc`.
captured : i64 = 100;
add_capture := closure((y: i64) -> i64 => y + captured);
_ = add_capture(1);
}
print("Tracer.count = {}\n", tracer.count);
0
}

View File

@@ -0,0 +1,28 @@
// Step 2.5 — contextual typing for closure literals with N (heterogeneous)
// params. An untyped lambda `(a, b, c) => ...` takes each param's type
// positionally from the expected `Closure(T0, T1, T2) -> R` signature, in both
// assignment and argument position. (Previously only the first param — or
// all-same-typed params — resolved; trailing params silently defaulted to i64.)
#import "modules/std.sx";
// argument-position: lambda typed from the parameter's closure type.
apply2 :: (f: Closure(i64, string) -> i64, x: i64, s: string) -> i64 {
return f(x, s);
}
apply3 :: (f: Closure(i64, i64, string) -> i64, a: i64, b: i64, c: string) -> i64 {
return f(a, b, c);
}
main :: () -> i32 {
// assignment-position, mixed (i64, string) params — `b` is string.
cb : Closure(i64, string) -> i64 = (a, b) => a + b.len;
print("cb={}\n", cb(10, "hello")); // 10 + 5 = 15
// argument-position, 2 params.
print("r={}\n", apply2((a, b) => a + b.len, 10, "hello")); // 15
// argument-position, 3 params (i64, i64, string).
print("q={}\n", apply3((a, b, c) => a + b + c.len, 1, 2, "xyz")); // 1+2+3 = 6
0
}

View File

@@ -0,0 +1,24 @@
// Regression (issue 0059): a function with NO explicit return type infers it
// from the body, which references the function's own parameters. The inference
// must see those params — before the fix they weren't in scope during
// return-type resolution, so the inferred type came out `.unresolved` and tripped
// the LLVM-emission guard ("unresolved type reached LLVM emission"). Whether it
// slipped through used to depend on a same-named binding lingering from earlier
// lowering. Covers the arrow (`=>`) and inferred-via-`return` forms, at top level
// and as locals.
#import "modules/std.sx";
dbl :: (x: i32) => x * 2; // top-level arrow, inferred return
inc :: (x: i32) { return x + 1; } // top-level block, inferred via `return`
main :: () {
print("{}\n", dbl(7)); // 14
print("{}\n", inc(41)); // 42
tripl :: (x: i32) => x * 3; // local arrow, inferred return
print("{}\n", tripl(4)); // 12
half :: (x: f32) => x / 2.0; // inferred float return
print("{}\n", half(9.0)); // 4.500000
}

View File

@@ -0,0 +1,17 @@
// Regression (issue 0060): a closure LITERAL passed directly as a bare
// function-type argument `(T) -> U` and then called inside the callee. The
// closure's underlying function takes a hidden env arg that a bare fn-ptr slot
// doesn't pass, so the compiler bridges a capture-free closure to the bare ABI
// with a generated adapter. Both block and arrow bodies. (The working contrast
// where the param is a `Closure(...)` type is examples/0302.)
#import "modules/std.sx";
apply :: (f: (i64) -> i64) -> i64 { return f(5); }
twice :: (f: (i64) -> i64, x: i64) -> i64 { return f(f(x)); }
main :: () {
print("block={}\n", apply(closure((x: i64) -> i64 { return x * 2; }))); // 10
print("arrow={}\n", apply(closure((x: i64) -> i64 => x * 2))); // 10
print("twice={}\n", twice(closure((x: i64) -> i64 => x + 3), 1)); // ((1+3)+3) = 7
}

View File

@@ -0,0 +1,22 @@
// Closure literal declared (and used) inside a `defer` body.
//
// Regression (issue 0073): this used to segfault lowering. A lambda inherited
// the enclosing function's `func_defer_base`, so the lambda's `return` re-drained
// the enclosing function's defers — and when the defer body itself declared the
// lambda, that re-lowered the lambda forever (infinite recursion). A lambda now
// opens its own defer window (like every other function-lowering entry).
#import "modules/std.sx";
run :: () {
defer {
cb := (n: i32) -> i32 { return n * 2; };
print("defer closure: {}\n", cb(21)); // 42, at scope exit
}
print("body\n");
}
main :: () -> i32 {
run();
return 0;
}

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
-3

View File

@@ -0,0 +1 @@
0

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,4 @@
fp(3,4) = 7
fp(3,4) = 12
apply(add,5,6) = 11
apply(mul,5,6) = 30

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,13 @@
basic: 142
snapshot: 15
clamp: 50 0 100
[INFO] system ready
factory: 105 110
auto-promote: 21
hof: 40
reduce: 15
compose: 30
multi: 105 500
widget: slider = 42
widget: no handler
=== DONE ===

View File

@@ -0,0 +1,2 @@
direct call works
closure call works

View File

@@ -0,0 +1,2 @@
cl f = 1
cl f = 0

View File

@@ -0,0 +1,3 @@
cb=15
r=15
q=6

View File

@@ -0,0 +1,4 @@
14
42
12
4.500000

View File

@@ -0,0 +1,3 @@
block=10
arrow=10
twice=7

View File

@@ -0,0 +1,2 @@
body
defer closure: 42