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,17 @@
#import "modules/std.sx";
sum :: (a:$T, b:T) -> T {
return a + b;
}
main :: () {
x:=sum(2,3);
print("sum: {}\n", x);
print("sum: {}\n", sum(40,2));
print("sum: {}\n", sum(40,2.5));
}
// ** stdout **
// sum: 42
// sum: 42.500000
//

View File

@@ -0,0 +1,87 @@
#import "modules/std.sx";
#import "modules/math";
Vec :: struct($N: u32, $T:Type) {
// <N x T> (LLVM Vector)
// Vector is a Builtin Type
data: Vector(N,T);
}
Complex :: ($T:Type) -> Type {
return struct {
value: T;
//..inject
count: u32;
};
}
Vec3 :: Vec(3, f32);
vec3 :: (x:f32, y:f32, z:f32) -> Vector(3,f32) {
.[x, y, z]
}
Foo :: Complex(u32);
main :: () {
v1 := Vec3.{data = .[1,3,2]};
print("v1: {}\n", v1);
//stdout: Vec(3,f32){data: [1.0, 3.0, 2.0]}
//
v2 := vec3(1,3,2);
print("v2: {}\n", v2);
//stdout: [1.0, 3.0, 2.0]
//
// [N x T] (LLVM Array)
buffer : [5]f32 = .[0, 2, 3.5, 4, 0];
print("buff: {}\n", buffer);
//stdout: [0.0, 2.0, 3.5, 4.0, 0.0]
//
comp : Foo = .{value = 42, count = 1};
print("comp: {}\n", comp);
//stdout: Foo{value: 42, count: 1}
//
// Vector arithmetic
v3 := vec3(3,2,1);
add := v2 + v3;
print("add: {}\n", add);
// Element access
v2x := v2.x;
print("v2.x: {}\n", v2x);
// Index access
v2i := v2[1];
print("v2[1]: {}\n", v2i);
// Scalar broadcast
scaled := v2 * 2.0;
print("scaled: {}\n", scaled);
// Negation
neg := -v2;
print("neg: {}\n", neg);
// sqrt
s := sqrt(9.0);
print("sqrt(9): {}\n", s);
// inline generic type
Sx :: (user: $T) -> Type {
return enum {
counter: i32;
user: T;
};
}
sx := Sx(f32).user(0.5);
print("{}\n", sx);
print("{}\n", size_of(f32));
print("{}\n", size_of(Sx(f32)));
print("{}\n", size_of(Foo));
}

View File

@@ -0,0 +1,11 @@
#import "modules/std.sx";
main :: () {
i := 0;
while i < 10 {
i+=1;
if i == 2 then continue;
if i == 5 then break;
}
print("{}\n", i);
}

View File

@@ -0,0 +1,16 @@
// Functions without an explicit return type infer the type from the first
// `return <value>;` statement in the body, so `foo :: () { return 42; }` is
// usable from a typed context. No explicit-value return → `.void` default.
foo :: () { return 42; }
bar :: () { return foo() * 2; }
nested :: () {
if true {
return foo() + 10;
}
return 0;
}
main :: () -> i32 {
return xx (foo() + bar() + nested());
}

View File

@@ -0,0 +1,76 @@
// Generic struct `Animated($T: Lerpable)` monomorphized with a struct type — the
// `#inline` protocol constraint participates in method dispatch via `self.from.lerp(...)`.
#import "modules/std.sx";
#import "modules/math";
Lerpable :: protocol #inline {
lerp :: (self: *Self, 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);
}

View File

@@ -0,0 +1,28 @@
// Dot-call dispatch for generic struct methods.
//
// Covers three shapes:
// 1. non-generic method: h.plain()
// 2. generic method, explicit type arg: h.sized(i32)
// 3. generic method, inferred from val: h.taking(99)
#import "modules/std.sx";
Holder :: struct {
n: i64;
plain :: (self: *Holder) -> i64 { self.n }
sized :: (self: *Holder, $T: Type) -> i64 { size_of(T) }
taking :: (self: *Holder, $T: Type, v: T) -> T { v }
}
main :: () -> i32 {
h : *Holder = xx libc_malloc(size_of(Holder));
h.n = 7;
print("plain: {}\n", h.plain());
print("sized i32: {}\n", h.sized(i32));
print("sized i64: {}\n", h.sized(i64));
print("taking explicit: {}\n", h.taking(i32, 42));
print("taking inferred: {}\n", h.taking(99));
0
}

View File

@@ -0,0 +1,37 @@
// FFI plan step 5.2 — generic `Into(Block) for Closure(..$args) ->
// $R` impl. One impl in stdlib covers every closure shape; the
// compiler monomorphises the impl body per call shape and emits a
// dedicated `__invoke` `abi(.c)` trampoline + Block literal
// (via `#insert build_block_convert($args, $R);`).
//
// This test exercises a closure shape (`Closure(i64, i64) -> void`)
// that has NO hand-rolled `Into(Block)` impl in
// `library/modules/ffi/objc_block.sx`. Before step 5.2 lands,
// `xx cl : Block` errors out with the "no Into(Block) for
// cl_i64_i64__void" focused diagnostic. After the generic impl
// lands, the same call resolves through the pack-shaped impl and
// the per-shape trampoline ferries control back to the sx closure.
//
// The block is invoked directly through `b.invoke` (a typed
// `abi(.c)` fn-pointer) — the same shape the Apple Block
// runtime calls when a UIKit/Foundation API hands the block back
// to its registered invoke.
#import "modules/std.sx";
#import "modules/ffi/objc_block.sx";
g_a: i64 = 0;
g_b: i64 = 0;
main :: () -> i32 {
cl := (a: i64, b: i64) => { g_a = a; g_b = b; };
blk : Block = xx cl;
invoke_fn : (*Block, i64, i64) -> void abi(.c) = xx blk.invoke;
invoke_fn(@blk, 10, 20);
if g_a != 10 { print("FAIL: g_a={}\n", g_a); return 1; }
if g_b != 20 { print("FAIL: g_b={}\n", g_b); return 1; }
print("generic-into-block ok: a={} b={}\n", g_a, g_b);
0
}

View File

@@ -0,0 +1,29 @@
// A generic value parameter (`$K: u32`) bound from a named const or a
// constant-foldable expression resolves to the SAME monomorphised instantiation
// as the literal form: `Vec(N, f32)` (N a module const) and `Vec(M + 1, f32)`
// (a const expression) are both `Vec(3, f32)`. The struct-copy assignment is the
// proof — it type-checks only because the two spellings name one instantiation.
//
// Regression (issue 0083): the value-param binder hand-rolled an `else => 0`
// switch, so a named-const value arg either fabricated a 0 binding under a wrong
// mangled name or was rejected outright as "unknown type 'N'". It now folds
// through the shared const-int evaluator (`program_index.evalConstIntExpr`).
#import "modules/std.sx";
N :: 3;
M :: 2;
Vec :: struct ($K: u32, $T: Type) { data: [K]T; }
main :: () {
a : Vec(N, f32) = ---; // named-const value param
a.data[0] = 10.0; a.data[1] = 20.0; a.data[2] = 30.0;
print("named: len={} a0={} a2={}\n", a.data.len, a.data[0], a.data[2]);
e : Vec(M + 1, f32) = ---; // const-expr value param (M + 1 == 3)
e.data[0] = 1.0; e.data[2] = 9.0;
print("expr: len={} e2={}\n", e.data.len, e.data[2]);
b : Vec(3, f32) = a; // same instantiation → struct copy type-checks
print("copy: len={} b2={}\n", b.data.len, b.data[2]);
}

View File

@@ -0,0 +1,32 @@
// A type-RETURNING function with a value parameter (`$K: u32`) used as a TYPE
// annotation: `b : Make(N, i64)` where `Make :: ($K, $T) -> Type { return [K]T; }`.
// A named-const value arg (`Make(N, i64)`), a const-expression value arg
// (`Make(M + 1, i64)`), and the literal form (`Make(3, i64)`) all instantiate to
// the SAME type — the array copy `b : Make(3, i64) = a` type-checks only because
// the three spellings name one `[3]i64`.
//
// Regression (issue 0083 / F0.4 attempt 6): the unknown-type checker walked the
// value-param position as a type name ("unknown type 'N'"), and the
// parameterized-type-annotation path never routed to `instantiateTypeFunction`,
// nor did that binder resolve a non-struct/union return shape (`return [K]T`).
// The value arg now folds through the shared const-int evaluator and the type
// function resolves its general return-type expression with bindings active.
#import "modules/std.sx";
N :: 3;
M :: 2;
Make :: ($K: u32, $T: Type) -> Type { return [K]T; }
main :: () {
a : Make(N, i64) = ---; // named-const value param
a[0] = 10; a[1] = 20; a[2] = 30;
print("named: len={} a0={} a2={}\n", a.len, a[0], a[2]);
e : Make(M + 1, i64) = ---; // const-expr value param (M + 1 == 3)
e[0] = 1; e[2] = 9;
print("expr: len={} e2={}\n", e.len, e[2]);
b : Make(3, i64) = a; // same instantiation → array copy type-checks
print("copy: len={} b2={}\n", b.len, b[2]);
}

View File

@@ -0,0 +1,19 @@
// A generic value parameter (`$K: u32`) binds a literal (`Vec(3, i64)`) and an
// integral-float named const (`Vec(L, i64)` with `L : f64 : 4.0`) to the same
// integer a plain `4` would — the value-param arg folds through the shared
// const-int evaluator, so the integral-float rule (F0.4 attempt 8, Agra ruling)
// reaches value params too. The folded value is the array length `[K]i64`.
//
// The bind is range-checked against the declared `u32` (an out-of-range arg is a
// clean compile error — see 1134); a valid in-range value binds normally.
#import "modules/std.sx";
Vec :: struct ($K: u32, $T: Type) { data: [K]T; }
L : f64 : 4.0;
main :: () {
a : Vec(3, i64) = ---; // literal value param
b : Vec(L, i64) = ---; // integral-float named-const value param → 4
print("a.len={} b.len={}\n", a.data.len, b.data.len); // 3 and 4
}

View File

@@ -0,0 +1,31 @@
// Resolver E1 lock: the bare-type-leaf cutover to the source-aware
// `selectNominalLeaf` must NOT touch the NON-leaf type heads. A generic-struct
// instantiation (`Box(i32)`), a `Vector(N, T)` builtin, and a type-returning
// function (`Make(3, i64)`) are all resolved by `resolveTypeWithBindings`
// ABOVE the bare-name leaf switch (`resolveParameterizedWithBindings` /
// `resolveTypeCallWithBindings` / the `Vector` builtin path), so they stay on
// the legacy resolution and never reach `selectNominalLeaf`. Parameterized
// protocols share the same `resolveParameterizedWithBindings` pre-leaf path
// (covered by 0204/0206). This example pins that all three still resolve
// identically after the cutover.
#import "modules/std.sx";
Box :: struct($T: Type) {
value: T;
}
Make :: ($K: u32, $T: Type) -> Type { return [K]T; }
N :: 3;
main :: () {
b : Box(i32) = .{ value = 42 };
print("box: {}\n", b.value);
v : Vector(4, f32) = .[1, 2, 3, 4];
print("vec: {} {}\n", v.x, v.w);
a : Make(N, i64) = ---;
a[0] = 10; a[2] = 30;
print("typefn: len={} a0={} a2={}\n", a.len, a[0], a[2]);
}

View File

@@ -0,0 +1,9 @@
// Companion of 0211: the re-export facade — own alias decls over another
// module's members. Flat importers of THIS file see the aliases bare.
#import "modules/std.sx";
r :: #import "0211-generics-struct-alias-head-rich.sx";
helper :: r.helper;
Thing :: r.Thing;
Box :: r.Box;

View File

@@ -0,0 +1,15 @@
// Companion of 0211: the authoring module — a plain fn, a plain struct,
// and a generic struct, all re-exported by -facade.sx via alias decls.
#import "modules/std.sx";
helper :: () -> i64 { 7 }
Thing :: struct {
v: i64;
init :: () -> Thing { Thing.{ v = 42 } }
}
Box :: struct ($T: Type) {
item: T;
get :: (b: *Box(T)) -> T { b.item }
}

View File

@@ -0,0 +1,43 @@
// Generic-struct head aliases: `BoxAlias :: Box;` binds the alias to the
// SAME template — instantiation, methods, annotations, and alias chains all
// resolve through it. Cross-module, a facade's `Box :: r.Box;` re-export is
// the facade's OWN declaration, so it carries one flat-import level exactly
// like a plain-struct alias (companion files: -rich.sx authors the decls,
// -facade.sx re-exports them through a namespace alias).
// Regression (issue 0120): the alias head used to lower silently to an
// unresolved type and panic in the LLVM backend at instantiation.
#import "modules/std.sx";
#import "0211-generics-struct-alias-head-facade.sx";
LocalBox :: struct ($T: Type) {
item: T;
get :: (b: *LocalBox(T)) -> T { b.item }
}
LocalAlias :: LocalBox;
ChainAlias :: LocalAlias;
main :: () {
// Same-file alias: instantiation + field + method.
b := LocalAlias(i64).{ item = 3 };
print("field: {}\n", b.item);
print("method: {}\n", b.get());
// Alias chain terminates at the template.
c := ChainAlias(i64).{ item = 11 };
print("chain: {}\n", c.item);
// Alias as a type annotation head.
a : LocalAlias(string) = .{ item = "ann" };
print("annot: {}\n", a.item);
// Cross-module re-exports carried one flat hop from the facade:
// plain fn, plain struct (static method), and the generic head.
print("helper: {}\n", helper());
t := Thing.init();
print("thing: {}\n", t.v);
f := Box(i64).{ item = 7 };
print("facade: {}\n", f.get());
x : Box(string) = .{ item = "qq" };
print("facade-annot: {}\n", x.item);
}

View File

@@ -0,0 +1,41 @@
// An ARRAY argument at a generic slice param (`xs: []$T`) binds T from
// the array's element type — the same array→slice promotion concrete
// slice params perform — for scalar and struct elements, with the
// return-position `T` resolving to the element type. The slice
// spelling keeps working unchanged.
//
// Regression (issue 0126): the binding extractor only accepted slice
// args, so `first(a)` left T unbound and the monomorphized body
// reached LLVM emission with the `.unresolved` sentinel (panic).
#import "modules/std.sx";
P :: struct { x: i64; y: i64; }
first :: (xs: []$T) -> T {
return xs[0];
}
last :: (xs: []$T) -> T {
return xs[xs.len - 1];
}
main :: () -> i32 {
a : [3]i64 = ---;
a[0] = 7; a[1] = 8; a[2] = 9;
print("{}\n", first(a));
print("{}\n", last(a));
bs : [4]u8 = ---;
bs[0] = 5; bs[1] = 6; bs[2] = 7; bs[3] = 8;
print("{}\n", last(bs));
ps : [2]P = ---;
ps[0] = .{ x = 1, y = 2 };
ps[1] = .{ x = 3, y = 4 };
print("{}\n", first(ps).y);
s : []i64 = a;
print("{}\n", first(s));
return 0;
}

View File

@@ -0,0 +1,13 @@
// A NAMESPACED call to a generic free function types its result from the
// call's inferred bindings — not the unbound `T` stub (issue 0127:
// `m.pick(3, 9)` boxed as `T{}` while the flat spelling printed `9`).
#import "modules/std.sx";
m :: #import "0213-generics-namespaced-call-result/m.sx";
main :: () {
print("{}\n", m.pick(3, 9)); // i64 binding
print("{}\n", m.pick(1.5, 0.25)); // f64 binding
v := m.double(21);
w : i64 = v + 0; // the concrete type flows onward
print("{}\n", w);
}

View File

@@ -0,0 +1,9 @@
#import "modules/std.sx";
pick :: (a: $T, b: T) -> T {
if a > b then a else b
}
double :: (x: $T) -> T {
x + x
}

View File

@@ -0,0 +1,26 @@
// Generic inference where `$R` comes from a worker closure's RETURN type
// through a variadic `..$args` pack — both the DIRECT spelling
// `mymk(bx, worker, 40, 2)` and the UFCS dot-call `bx.mymk(worker, 40, 2)`
// resolve `$R = i64` identically and build `Wrap($R)` correctly.
// Regression (issue 0151): the UFCS path used to splice the receiver as
// arg 0 without running the direct path's pack/closure-return binding, so
// `$R` stayed `.unresolved` and SIGTRAPped at LLVM emission.
#import "modules/std.sx";
Box :: struct { n: i64; }
Wrap :: struct ($R: Type) { value: R; }
mymk :: ufcs (b: Box, worker: Closure(..$args) -> $R, ..$args) -> Wrap($R) {
f : Wrap($R) = ---;
f.value = worker(..args);
return f;
}
main :: () -> i32 {
bx : Box = .{ n = 1 };
direct := mymk(bx, (a: i64, b: i64) -> i64 => a + b, 40, 2);
ufcs := bx.mymk((a: i64, b: i64) -> i64 => a + b, 40, 2);
print("direct={}\n", direct.value);
print("ufcs={}\n", ufcs.value);
return 0;
}

View File

@@ -0,0 +1,29 @@
// Generic `$T` inferred through a generic-struct argument head — both
// by-value (`Box($T)`) and pointer-wrapped (`*Box($T)`), the latter also
// via a UFCS dot-call (auto-address-of receiver). Multi-param heads
// (`Pair($A, $B)`) and nested heads (`Box(Box($T))`) bind positionally.
// Regression (issue 0151, widened): `extractTypeParam` had no
// `parameterized_type_expr` arm, so `$T` never bound from a generic-struct
// param — the call failed with "cannot infer generic type parameter 'T'".
#import "modules/std.sx";
Box :: struct ($T: Type) { v: T; }
Pair :: struct ($A: Type, $B: Type) { a: A; b: B; }
unbox :: (b: *Box($T)) -> $T { return b.v; } // infer through `*`
byval :: (b: Box($T)) -> $T { return b.v; } // infer through head
second :: (p: Pair($A, $B)) -> $B { return p.b; } // 2nd of two params
nested :: (b: Box(Box($T))) -> $T { return b.v.v; } // nested head
get :: ufcs (b: *Box($T)) -> $T { return b.v; } // UFCS auto-ref
main :: () -> i32 {
b : Box(i64) = .{ v = 42 };
p : Pair(i64, f64) = .{ a = 1, b = 2.5 };
nb : Box(Box(i64)) = .{ v = .{ v = 9 } };
print("unbox={}\n", unbox(@b));
print("byval={}\n", byval(b));
print("second={}\n", second(p));
print("nested={}\n", nested(nb));
print("ufcs={}\n", b.get());
return 0;
}

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,3 @@
sum: 5
sum: 42
sum: 42.500000

View File

@@ -0,0 +1 @@
0

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,14 @@
v1: Vec3{data: [1.000000, 3.000000, 2.000000]}
v2: [1.000000, 3.000000, 2.000000]
buff: [0.000000, 2.000000, 3.500000, 4.000000, 0.000000]
comp: Foo{value: 42, count: 1}
add: [4.000000, 5.000000, 3.000000]
v2.x: 1.000000
v2[1]: 3.000000
scaled: [2.000000, 6.000000, 4.000000]
neg: [-1.000000, -3.000000, -2.000000]
sqrt(9): 3.000000
.user(0.500000)
4
16
8

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
5

View File

@@ -0,0 +1 @@
178

View File

@@ -0,0 +1,3 @@
after set: 100.000000x50.000000
mid anim: 150.000000x75.000000
end anim: 200.000000x100.000000

View File

@@ -0,0 +1,5 @@
plain: 7
sized i32: 4
sized i64: 8
taking explicit: 42
taking inferred: 99

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@
generic-into-block ok: a=10 b=20

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,3 @@
named: len=3 a0=10.000000 a2=30.000000
expr: len=3 e2=9.000000
copy: len=3 b2=30.000000

View File

@@ -0,0 +1,3 @@
named: len=3 a0=10 a2=30
expr: len=3 e2=9
copy: len=3 b2=30

View File

@@ -0,0 +1 @@
a.len=3 b.len=4

View File

@@ -0,0 +1,3 @@
box: 42
vec: 1.000000 4.000000
typefn: len=3 a0=10 a2=30

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,8 @@
field: 3
method: 3
chain: 11
annot: ann
helper: 7
thing: 42
facade: 7
facade-annot: qq

View File

@@ -0,0 +1,5 @@
7
9
8
2
7

View File

@@ -0,0 +1,3 @@
9
1.500000
42

View File

@@ -0,0 +1,2 @@
direct=42
ufcs=42

View File

@@ -0,0 +1,5 @@
unbox=42
byval=42
second=2.500000
nested=9
ufcs=42