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,35 @@
#import "modules/std.sx";
sum :: (..args: []i32) -> i32 {
result := 0;
for args (it) {
result = result + it;
}
result
}
print_all :: (..args: []i32) {
for args (it) {
out(int_to_string(it));
out(" ");
}
out("\n");
}
main :: () -> i32 {
out(int_to_string(sum(10, 20, 30)));
out("\n");
print_all(1, 2, 3, 4, 5);
arr : [3]i32 = .[10, 20, 30];
out(int_to_string(sum(..arr)));
out("\n");
for arr (it) {
out(int_to_string(it));
out(" ");
}
out("\n");
0
}

View File

@@ -0,0 +1,47 @@
#import "modules/std.sx";
Point :: struct {
x: i32;
y: i32;
}
// Print all arguments — accepts any type, dispatches via type-switch
print_any :: (..args: []Any) {
for args (it) {
type := type_of(it);
if type == {
case int: out(int_to_string(cast(i32) it));
case string: out(cast(string) it);
case bool: out(bool_to_string(cast(bool) it));
case float: out(float_to_string(cast(f64) it));
case Point: {
p := cast(Point) it;
out("(");
out(int_to_string(p.x));
out(",");
out(int_to_string(p.y));
out(")");
}
}
out(" ");
}
out("\n");
}
count :: (..args: []Any) -> i32 {
args.len
}
main :: () -> i32 {
print_any(42, "hello", true, 3.14);
// Test with struct
p := Point.{ x=10, y=20 };
print_any("point:", p, 99);
// Test count
out(int_to_string(count(1, 2, 3)));
out("\n");
0
}

View File

@@ -0,0 +1,22 @@
// Variadic heterogeneous type packs — `..$args` — parse smoke.
//
// First positive slice of the pack feature: the parser accepts
// `..$args` (variadic marker + comptime sigil + name) as a
// parameter declaration. No semantic effect yet — the function
// is declared but never instantiated; main exists so the example
// has runnable output.
//
// Next slices: type-system representation of the pack, impl
// matching for `Closure(..$args) -> $R`, runtime indexing
// (`args[$i]`), and the `#insert build_x($args, ...)` pattern.
#import "modules/std.sx";
foo :: (..$args) -> i64 {
return 0;
}
main :: () -> i32 {
print("pack parse ok\n");
return 0;
}

View File

@@ -0,0 +1,23 @@
// Variadic heterogeneous type packs — `..$args` — type-system
// representation lock-in.
//
// Step 1c slice for the pack feature (see
// ~/.claude/plans/lets-see-options-for-merry-dijkstra.md). Exercises
// the parser's acceptance of `..$args` inside a `Closure(...)` type
// expression — the pack-shape spelling used by impl headers like
// `impl Into(Block) for Closure(..$args) -> $R`.
//
// Today's parser only accepts `..$args` in fn parameter lists (1b);
// the same syntax inside a `Closure(...)` type expression hits
// `expected type name` at the `..` token. This file pins that
// rejection. The next commit teaches `parseTypeExpr`'s `Closure(...)`
// arm + the type-table representation to carry the pack.
#import "modules/std.sx";
takes_cb :: (cb: Closure(..$args) -> $R) -> void { }
main :: () -> i32 {
print("pack type rep ok\n");
return 0;
}

View File

@@ -0,0 +1,46 @@
// Variadic heterogeneous type packs — `..$args` — impl matching
// lock-in.
//
// Step 1d slice for the pack feature (see
// ~/.claude/plans/lets-see-options-for-merry-dijkstra.md). Pins
// today's concrete-only impl-matching behaviour: a user-declared
// `impl Into(Block) for Closure(..$args) -> $R` does NOT match
// any concrete closure source type. The xx cast site hits the
// existing "no `Into(Block) for <src>` impl" diagnostic even
// though the pack impl is reachable.
//
// Next commit (1d.B) teaches `tryUserConversion` /
// `registerParamImpl` to walk a second `param_impl_pack_map`
// when the concrete-key miss happens. Pack impls bind their
// `$args` and `$R` to the concrete source closure's tail types
// and return; monomorphisation proceeds against those bindings.
//
// The `Closure(i32, bool) -> bool` shape is not covered by
// stdlib's hand-rolled impls (only `Closure() -> void` and
// `Closure(bool) -> void`) and not covered by 96-block-multi-arg's
// `Closure(i32, *void) -> void` impl, so the pack impl is the
// only candidate.
#import "modules/std.sx";
#import "modules/ffi/objc_block.sx";
impl Into(Block) for Closure(..$args) -> $R {
convert :: (self: Closure(..$args) -> $R) -> Block {
.{
isa = @_NSConcreteStackBlock,
flags = 0,
reserved = 0,
invoke = null,
descriptor = xx @__sx_block_descriptor,
sx_env = self.env,
sx_fn = self.fn_ptr,
}
}
}
main :: () -> i32 {
cl := (a: i32, b: bool) => true;
b : *Block = xx cl;
print("pack impl match ok\n");
return 0;
}

View File

@@ -0,0 +1,32 @@
// Variadic heterogeneous type packs — step 2: typed pack indexing.
//
// `args[$i]` (with `$i` a comptime-known integer) inside a pack-fn
// body should resolve to the i-th call-site argument with its
// CONCRETE type, not the boxed `Any` that today's `[]Any` slice
// path yields. Without typed access, downstream operations on the
// element (field access, typed coercion, passing to a typed slot)
// either fail with "field 'X' not found on type 'Any'" or silently
// box/unbox through Any.
//
// This file pins today's failure: `args[0].x` on a struct-typed
// call arg trips "field 'x' not found on type 'Any'" because the
// AST-level type inference for `args[0]` returns Any.
//
// Next commit teaches `lowerIndexExpr` to detect a pack-name base
// with a comptime-int-literal index and substitute the i-th
// call-site arg's lowered value directly — propagating the call
// arg's real type through field access, typed assignments, and
// further indexing.
#import "modules/std.sx";
Point :: struct { x: i64; y: i64; }
get_x :: (..$args) -> i64 => args[0].x;
main :: () -> i32 {
p := Point.{ x = 7, y = 9 };
n := get_x(p);
print("{}\n", n);
return 0;
}

View File

@@ -0,0 +1,39 @@
// Variadic heterogeneous type packs — control-flow follow-up to
// issue-0045 fix (commit 9e78790).
//
// issue-0045's fix routes inline-comptime-body `return X;` into a
// result slot so the caller's basic block isn't terminated
// mid-flight. But the fix sets `block_terminated = true` after
// the inline return — which leaks PAST the enclosing `if`'s
// merge block. When the body shape is
// if cond { return X; }
// return Y;
// only the then-branch's `return X;` runs; `block_terminated`
// stays true in the merge block, so `lowerBlockValue`'s loop
// exits before the trailing `return Y;` lowers. The trailing
// return never stores into the slot — for the false-condition
// path the load reads uninitialised stack memory.
//
// Pack-fn `..$args` is the shortest repro because `args.len`
// gives a comptime-feeling test for the condition. The bug is
// actually shape-agnostic — any comptime body with `if cond
// { return X; }; return Y;` regresses the same way.
//
// `maybe()` with zero call-args takes the false branch and
// should fall through to `return -1;`. Today it loads garbage
// from the uninitialised slot.
#import "modules/std.sx";
maybe :: (..$args) -> i64 {
if args.len > 0 {
return 42;
}
return -1;
}
main :: () -> i32 {
print("{}\n", maybe()); // expect -1
print("{}\n", maybe(99)); // expect 42
return 0;
}

View File

@@ -0,0 +1,30 @@
// Variadic heterogeneous type packs — step 2b: per-call-shape
// monomorphisation. Each unique call signature gets ONE mono fn;
// repeat calls with the same signature share it. The runtime output
// confirms correct semantics; the IR (visible via `sx ir`) shows
// the distinct mono symbols:
//
// call @count__pack(ctx)
// call @count__pack_i64(ctx, 1)
// call @count__pack_i64(ctx, 2) ← shares with the 1-arg i64 call
// call @count__pack_i64_i64_i64(ctx, 1, 2, 3)
// call @count__pack_string_bool(ctx, ..)
//
// Before step 2b, each call inlined a fresh copy of the body into
// main's basic block — no shared symbols, IR size grew linearly in
// call sites. After 2b, distinct shapes get distinct functions,
// repeats share, IR scales with unique shapes.
#import "modules/std.sx";
count :: (..$args) -> i64 => args.len;
main :: () -> i32 {
a := count();
b := count(1);
c := count(2);
d := count(1, 2, 3);
e := count("x", true);
print("{} {} {} {} {}\n", a, b, c, d, e);
return 0;
}

View File

@@ -0,0 +1,23 @@
// Variadic heterogeneous type packs — follow-up #2 (generic $R
// return type).
//
// A pack-fn's return type can be a generic name (`$R`) — bound at
// the call site to match the body's natural type or the caller's
// target. Today's `monomorphizePackFn` calls `resolveReturnType`
// which treats `$R` as an opaque struct, so the mono's signature
// gets a wrong ret_ty and the value is silently zero / garbage.
//
// `first(42)` should return 42; the lock-in pins today's `0`.
// Next commit infers the ret type from the body's tail expression
// (or first `return X;`) and rebuilds the mono signature.
#import "modules/std.sx";
first :: (..$args) -> $R => args[0];
main :: () -> i32 {
a : i64 = first(42);
b : i64 = first(99);
print("{} {}\n", a, b);
return 0;
}

View File

@@ -0,0 +1,25 @@
// Variadic heterogeneous type packs — generic `$R` with
// heterogeneous element pick. `foo(..$args) -> $R => args[2]`
// returns the THIRD arg's value; the ret type is inferred from
// the third arg's concrete type per call shape.
//
// foo(42, 3.2, "hello") → returns "hello" (string).
//
// Exercises:
// - generic `$R` inference for non-zeroth pack indices.
// - heterogeneous mixed-type call args binding into distinct
// types per position (i64, f64, string).
// - `pack_arg_types` type-only binding for `inferExprType`
// pre-mono-scope: without it, the synthesized-ident detour
// loses the type because the scope isn't set up yet during
// return-type inference.
#import "modules/std.sx";
foo :: (..$args) -> $R => args[2];
main :: () -> i32 {
a := foo(42, 3.2, "hello");
print("{}\n", a);
return 0;
}

View File

@@ -0,0 +1,20 @@
// Variadic heterogeneous type packs — out-of-bounds pack index
// is a compile-time error.
//
// `foo(..$args) -> $R => args[2]` accesses the third pack
// element. When called with fewer than 3 args, the literal index
// 2 is out of bounds for the pack's actual arity. The compiler
// detects this in `diagPackIndexOOB` and emits a focused
// diagnostic at the index span — pre-fix, the fall-through hit
// the standard slice-indexing path and produced "unresolved
// 'args'" which buried the real cause.
#import "modules/std.sx";
foo :: (..$args) -> $R => args[2];
main :: () -> i32 {
n : i64 = foo(99);
print("{}\n", n);
return 0;
}

View File

@@ -0,0 +1,23 @@
// Step 2.7 — forwarding a variadic to a `[]Any` helper.
//
// A comptime pack `..$args` is comptime-only (Decision 1): `args` bare is NOT a
// runtime value, so `log_count(args)` on a pack is an error (see the
// pack-as-value tests). To forward a variadic to a runtime `[]Any` helper,
// declare it as the *slice* variadic `..args: []Any` — then `args` is a real
// `[]Any` slice that passes straight through.
#import "modules/std.sx";
log_count :: (items: []Any) -> i64 {
return items.len;
}
// Slice variadic: `args` is a runtime []Any, forwarded directly.
forward :: (..args: []Any) -> i64 {
return log_count(args);
}
main :: () -> i32 {
print("{}\n", forward(1, "hi", 2.5));
return 0;
}

View File

@@ -0,0 +1,29 @@
// Variadic heterogeneous type packs — Step 2.6: indexing a pack with a
// RUNTIME index is a compile error.
//
// Per locked Decision 1, a pack is comptime-only and has NO runtime
// representation — so `args[i]` is valid only when `i` is a compile-time
// constant (a literal, or an `inline for` cursor). A runtime index (here a
// `while`-loop counter) must produce a clear diagnostic, not the confusing
// "unresolved 'args'" the slice-index fall-through used to give. To walk a
// pack, use `inline for 0..args.len (i) { ... }`, which unrolls so each
// `args[i]` is a comptime index.
#import "modules/std.sx";
count_anys :: (..$args) -> i64 {
total : i64 = 0;
i : i64 = 0;
while i < args.len {
x : Any = args[i]; // ERROR: runtime index into a comptime-only pack
_ = x;
total = total + 1;
i = i + 1;
}
return total;
}
main :: () -> i32 {
print("{}\n", count_anys(10, "hi", 2.5, true));
return 0;
}

View File

@@ -0,0 +1,29 @@
// Variadic heterogeneous type packs — follow-up #1 (mixed
// `$comptime + ..$args` pack-fn signatures).
//
// Today's `isPackFn` rejects pack-fns that mix any other
// comptime param with the trailing pack — they fall through
// to the inline `lowerComptimeCall` path. The inline path
// doesn't bind non-string comptime params as runtime locals,
// so a body that uses both `$tag` (i32) AND `..$args` fails
// at the bare-name lookup of `tag`.
//
// Next commit relaxes `isPackFn` to accept "exactly one
// trailing pack + any number of non-pack comptime params" and
// `monomorphizePackFn` folds the comptime VALUES into the
// mangled name (so distinct calls of `tagged(7, ...)` vs
// `tagged(9, ...)` get distinct monos), then binds the
// comptime values as both comptime substitutions and runtime
// locals (for body code that references them by name).
#import "modules/std.sx";
tagged :: ($tag: i32, ..$args) -> i64 {
return tag * 100 + args.len;
}
main :: () -> i32 {
print("{}\n", tagged(7, 1, 2, 3)); // 7*100 + 3 = 703
print("{}\n", tagged(9)); // 9*100 + 0 = 900
return 0;
}

View File

@@ -0,0 +1,39 @@
// Variadic heterogeneous type packs — step 3: `$args[$i]` in
// type positions.
//
// `$args[$i]` resolves to the i-th element type of the active
// pack binding wherever a type expression is expected:
// - return type: `-> $args[0]`
// - local var annotation: `x : $args[1] = ...`
// - (later: param types, fn-pointer types, struct field types)
//
// Today's parser hits "expected '{'" at the `$args[0]` token in
// the return type position because the `$<ident>` arm only
// accepts plain generic names; `[<int>]` after the name isn't
// recognised. This file pins that rejection. Next commit teaches
// the parser to accept `$<pack>[<int>]` and adds a new
// `PackIndexTypeExpr` AST node; `resolveTypeWithBindings`
// consults the active `pack_arg_types` map.
//
// The body intentionally exercises TWO positions per mono — the
// return type AND a local annotation — so the parser change has
// to cover more than just the trailing return arrow.
#import "modules/std.sx";
swap_take :: (..$args) -> $args[0] {
second : $args[1] = args[1];
// `second` is bound and typed — confirms the local-annotation
// path also resolves. The body returns args[0] (statically
// typed as $args[0]).
return args[0];
}
main :: () -> i32 {
// Heterogeneous call shapes — each picks a different concrete
// pair, gets its own mono.
a : i64 = swap_take(42, "ignored"); // $args[0] = i64, $args[1] = string
b : string = swap_take("first", 99); // $args[0] = string, $args[1] = i64
print("{} {}\n", a, b);
return 0;
}

View File

@@ -0,0 +1,21 @@
// Variadic heterogeneous type packs — step 3 complex smoke.
//
// Three-element pack with `$args[2]` (the third element) used in
// the return-type position. Confirms:
// - Multi-arg packs index past the zeroth element correctly.
// - Three distinct call shapes get three distinct monos.
// - The return-type slot is correctly substituted per-mono so
// the inferred caller type matches what the body actually
// returns (string / i64 / bool here).
#import "modules/std.sx";
third :: (..$args) -> $args[2] => args[2];
main :: () -> i32 {
a := third(1, 2, "third"); // (i64, i64, string) → "third"
b := third(true, 3.14, 99); // (bool, f64, i64) → 99
c := third("a", "b", false); // (string, string, bool) → false
print("{} {} {}\n", a, b, c);
return 0;
}

View File

@@ -0,0 +1,30 @@
// Variadic heterogeneous type packs — step 3: `$args[$i]` in
// fn-pointer type literals.
//
// `(*void, $args[0]) -> $args[1]` is the shape step 5's generic
// `Into(Block) for Closure(..$args) -> $R` body needs for its
// trampoline:
// typed_fn : (*void, $args[0], $args[1], ...) -> $R =
// xx block_self.sx_fn;
// — the trampoline's invoke slot is typed against the pack
// positions to bridge the Block ABI to the sx closure.
//
// This test exercises the same plumbing on a smaller scale: a
// local var with a fn-pointer type whose param + return types
// both interpolate through `$args[$i]`.
#import "modules/std.sx";
// Two concrete handlers that match the per-mono fn-pointer shape.
double_i64 :: (env: *void, x: i64) -> i64 => x * 2;
via_fnptr :: (..$args) -> $args[1] {
fp : (*void, $args[0]) -> $args[1] = double_i64;
return fp(null, args[0]);
}
main :: () -> i32 {
n := via_fnptr(7, 0); // (i64, i64) → fp : (*void, i64) -> i64
print("{}\n", n);
return 0;
}

View File

@@ -0,0 +1,62 @@
// Variadic heterogeneous type packs — step 3: type-reflection
// intrinsics.
//
// Three comptime helpers used by pack-fn bodies to branch on
// type identity / protocol membership:
//
// type_name(T) -> string // display name of T
// type_eq(T1, T2) -> bool // structural TypeId equality
// has_impl(P, T) -> bool // T has a reachable impl for P
//
// All three fold to compile-time constants and are accepted by
// `tryConstBoolCondition`, so `inline if type_eq(...)` /
// `inline if has_impl(...)` collapse to a single branch at lower
// time — no runtime cost.
//
// `has_impl`'s protocol arg accepts both shapes:
// - plain protocol name: `has_impl(Allocator, CAllocator)`.
// - parameterised call: `has_impl(Wrap(i64), i32)` — the args
// match the impl's protocol type-args exactly.
#import "modules/std.sx";
#import "modules/std/mem.sx";
// User-defined parameterised protocol + an impl, so has_impl can
// confirm parameterised matching works with a known-true case.
Wrap :: protocol(Target: Type) {
wrap :: (self: *Self) -> Target;
}
impl Wrap(i64) for i32 {
wrap :: (self: i32) -> i64 => xx self;
}
main :: () -> i32 {
// type_name — display names.
print("{} {} {}\n", type_name(i64), type_name(string), type_name(bool));
// type_eq — structural equality on TypeIds.
print("{} {} {} {}\n",
type_eq(i64, i64),
type_eq(i64, string),
type_eq(*i64, *i64),
type_eq(*i64, *i32));
// inline-if folds type_eq at lower time.
inline if type_eq(i64, i64) {
print("inline-if folded: same\n");
} else {
print("inline-if folded: different\n");
}
// has_impl — plain protocol (Allocator is unary).
print("Allocator/CAllocator: {}\n", has_impl(Allocator, CAllocator));
print("Allocator/i64: {}\n", has_impl(Allocator, i64));
// has_impl — parameterised protocol (Wrap takes a Target type arg).
print("Wrap(i64)/i32: {}\n", has_impl(Wrap(i64), i32));
print("Wrap(i64)/bool: {}\n", has_impl(Wrap(i64), bool));
print("Wrap(bool)/i32: {}\n", has_impl(Wrap(bool), i32));
return 0;
}

View File

@@ -0,0 +1,44 @@
// Variadic heterogeneous type packs — step 4 source-construction
// path: `$args[$i]` in expression position yields a comptime Type
// VALUE (`Value.type_tag(TypeId)` in the interp). Lets builders +
// pack-fn bodies dispatch on the i-th pack type at compile time.
//
// Two usage shapes covered here:
//
// 1. `type_name($args[0])` — the value-form Type goes through the
// reflection intrinsic, picking up the per-mono concrete type.
//
// 2. `inline if type_eq($args[0], i64) { ... }` — compile-time
// branch over the pack element's type. The fold via
// `tryConstBoolCondition` reads $args[0] via `resolveTypeArg`,
// which the step-4 work taught to walk `pack_arg_types`.
//
// Lowering: `$args[$i]` in expression position emits a
// `const_type(arg_types[i])` IR op. The interp materialises a
// `Value.type_tag(TypeId)`. LLVM emit bails (Type is comptime-only).
#import "modules/std.sx";
show :: (..$args) -> string => type_name($args[0]);
describe :: (..$args) -> string {
inline if type_eq($args[0], i64) { return "got i64"; }
inline if type_eq($args[0], string) { return "got string"; }
inline if type_eq($args[0], bool) { return "got bool"; }
return "got other";
}
main :: () -> i32 {
// type_name picks up the per-mono first-arg type.
print("{}\n", show(42)); // i64
print("{}\n", show("hi")); // string
print("{}\n", show(3.14)); // f64
// inline-if + type_eq picks the right branch per mono.
print("{}\n", describe(42)); // got i64
print("{}\n", describe("hello")); // got string
print("{}\n", describe(true)); // got bool
print("{}\n", describe(3.14)); // got other
return 0;
}

View File

@@ -0,0 +1,35 @@
// Variadic heterogeneous type packs — step 4 final slice (4A
// bare): `$args` referenced bare (without `[...]` indexing) in
// expression position should evaluate to a comptime `[]Type`
// slice value — the whole pack passed through as data so
// builder fns can walk it.
//
// Use case (eventual): step 5's generic Into(Block) impl body
// convert :: (self: Closure(..$args) -> $R) -> Block {
// #insert build_block_convert($args, $R);
// }
// where `build_block_convert(args: []Type, ret: Type) -> string`
// is a regular sx fn the interp executes — it walks `args` to
// emit a trampoline fn matching the per-mono signature.
//
// Today the parser-arm I wrote for `$<ident>[<int>]` (commit
// fd03b58, M5.A.next.4.3) REQUIRES the `[` after the pack
// name; bare `$args` hits a focused diagnostic. This file
// pins that rejection. Next commit makes the `[` optional —
// no `[` yields a `comptime_pack_ref` AST node which lowering
// converts to a `[N x Type]` aggregate of `const_type` values.
#import "modules/std.sx";
len_of :: (..$args) -> i64 {
list := $args;
return list.len;
}
main :: () -> i32 {
print("{}\n", len_of());
print("{}\n", len_of(42));
print("{}\n", len_of(1, 2, 3));
print("{}\n", len_of("a", true, 3.14, "b"));
return 0;
}

View File

@@ -0,0 +1,38 @@
// Variadic heterogeneous type packs — step 4A final-slice
// follow-up. `type_name(<dynamic-arg>)` where the argument is
// NOT a static type expression (e.g. `list[i]` indexing into
// a `$args`-derived `[]Type` slice) silently folded to "i64"
// because `resolveTypeArg`'s catch-all `else => .i64` lied —
// the kind of silent unimplemented arm the project's REJECTED
// PATTERNS forbid.
//
// The fix: `tryLowerReflectionCall` now splits static vs
// dynamic args via `isStaticTypeArg(node)`. Static → fold to
// const_string at lower time (today's fast path). Dynamic →
// emit `callBuiltin(.type_name, [arg_ref])` for the interp's
// runtime arm to handle.
//
// Type values are comptime-only — the dynamic path only works
// inside a comptime context (`#run` / `#insert`). The test
// runs `walk(42, "hi")` at `#run` time and prints the result.
#import "modules/std.sx";
#import "modules/build.sx";
walk :: (..$args) -> string {
list := $args;
s := "";
i : i64 = 0;
while i < list.len {
s = concat(s, type_name(list[i]));
i = i + 1;
}
return s;
}
show :: () {
print("{}\n", walk(42, "hi"));
}
#run show();
main :: () { print("rt\n"); }

View File

@@ -0,0 +1,48 @@
// Variadic heterogeneous type packs — step 4A end-to-end
// smoke. Exercises the FULL chain step 5's generic
// `Into(Block)` impl needs:
//
// 1. A pack-fn that passes its bound `$args` to a builder
// via `#run` / `#insert` (here `#run` for simplicity).
// 2. The builder receives a `[]Type` slice and walks it,
// calling `type_name(list[i])` per position.
// 3. `type_name(list[i])` is the DYNAMIC form — the index_expr
// argument can't be statically resolved at lower time, so
// the lowering emits a `callBuiltin(.type_name, ...)` that
// the interp's arm handles by reading the runtime
// `Value.type_tag(TypeId)` and returning the per-position
// name.
//
// The smoke demonstrates that step 4's full surface — bare
// `$args` (whole pack as []Type) + dynamic reflection
// intrinsics + per-position type discovery at interp time —
// hangs together end-to-end. This is the foundation step 5's
// builder-driven generic Into(Block) impl rests on.
#import "modules/std.sx";
#import "modules/build.sx";
// Generic "describe pack" builder. Receives the pack as
// []Type, returns a joined string with each type's name.
describe :: (..$args) -> string {
list := $args;
s := "[";
i : i64 = 0;
while i < list.len {
if i > 0 { s = concat(s, ", "); }
s = concat(s, type_name(list[i]));
i = i + 1;
}
s = concat(s, "]");
return s;
}
run_all :: () {
print("{}\n", describe()); // []
print("{}\n", describe(42)); // [i64]
print("{}\n", describe(42, "hi")); // [i64, string]
print("{}\n", describe(true, 3.14, "x", 99)); // [bool, f64, string, i64]
}
#run run_all();
main :: () { print("rt\n"); }

View File

@@ -0,0 +1,55 @@
// Regression: bare `$args` slice survives crossing a function-call
// boundary — both `.len` AND per-element values come through.
//
// Before the fix landed in `lazyLowerFunction`, the callee's
// `args.len` got constant-folded to the outer pack-fn mono's
// arity. `walk(args: []Any) { return args.len; }` lazily lowered
// inside `probe(..$args)`'s first mono inherited
// `pack_param_count["args"] = N` from the pack — the
// `<pack_name>.len` intercept in `lowerFieldAccess` then baked
// `ret i64 N` into walk's IR. Every subsequent shape's call to
// walk returned the same constant, regardless of the actual slice
// it received.
//
// `describe(args)` walks element-by-element so a silent
// truncation surfaces as a missing tail (or a different type at
// some position) — not just the wrong length.
//
// Walking under `#run` is intentional: the bare-`$args` slice
// carries `const_type` elements that only the interp materialises;
// LLVM emission leaves the per-element slots as undef (4A.bare
// semantics — bare-pack is comptime-only).
//
// The element type is `[]Type` (a bare `$args` is the list of the
// pack args' TYPES, each an 8-byte `.type_value`). Declaring it
// `[]Any` (16-byte boxes) read every element past the first at the
// wrong stride — the legacy interp's loose Value model tolerated it,
// the byte-accurate comptime VM does not.
#import "modules/std.sx";
describe :: (args: []Type) -> string {
s := "[";
i : i64 = 0;
while i < args.len {
if i > 0 { s = concat(s, ", "); }
s = concat(s, type_name(args[i]));
i = i + 1;
}
return concat(s, "]");
}
probe :: (..$args) -> string {
list := $args;
return describe(list);
}
run_all :: () {
print("0: {}\n", probe());
print("1: {}\n", probe(1));
print("3: {}\n", probe(1, "x", true));
print("5: {}\n", probe(1, 2.0, "x", true, 99));
}
#run run_all();
main :: () { print("rt\n"); }

View File

@@ -0,0 +1,28 @@
// Regression: new-form variadic `..name: []T` defined in an imported
// module is callable from another module without crashing LLVM emit.
//
// Before the fix in `resolveParamType` + `packVariadicCallArgs`,
// the new-form variadic's element type went through one extra
// `sliceOf` wrap (the helpers treated `..parts: []string` the same
// as the legacy `parts: ..string` and added a slice level on top
// of the already-declared slice). The double-wrapped `[][]T`
// signature mismatched what the call-site marshalling emitted as
// `[N x T]`, producing null/undef Refs that crashed
// `LLVMBuildExtractValue` inside `emitStrCmp` during emission.
//
// Today's stdlib `path_join` uses the new form
// (`(..parts: []string) -> string`); it lives in `modules/std.sx`
// and is called here from the test module. Two- and three-arg
// shapes must round-trip the slice through the function-call
// boundary and concatenate the parts with '/'. Empty join (no
// args) returns "".
#import "modules/std.sx";
main :: () -> i32 {
print("{}\n", path_join());
print("{}\n", path_join("a"));
print("{}\n", path_join("a", "b"));
print("{}\n", path_join("a", "b", "c", "d"));
return 0;
}

View File

@@ -0,0 +1,30 @@
// Regression: a generic function (with `$T: Type` type params)
// called from inside a pack-fn mono must NOT inherit the outer
// pack maps during its own body lowering. Before the fix landed
// in `monomorphizeFunction`, the cached mono of a generic with
// an `args`-named param had its `args.len` constant-folded to
// the arity of whichever pack shape triggered the first mono;
// every subsequent shape read the same baked-in constant.
//
// Same root cause as issue-0048 (`lazyLowerFunction`), in a
// different lowering path (`monomorphizeFunction`).
#import "modules/std.sx";
build :: (args: []Type, $ret: Type) -> string {
return concat("len=", int_to_string(args.len));
}
probe :: (..$args) -> string {
return build($args, void);
}
run_all :: () {
print("0: {}\n", probe());
print("1: {}\n", probe(true));
print("2: {}\n", probe(42, "hi"));
print("4: {}\n", probe(1, 2.0, "x", true));
}
#run run_all();
main :: () { print("rt\n"); }

View File

@@ -0,0 +1,26 @@
// Regression (issue 0143): a variadic `..$args` pack forwarded as a `[]Type`
// ARGUMENT across a call must read the right element types. The pack-slice
// materialization (`buildPackSliceValue`) built a `[]Any` (16-byte) array while
// `Type` is now `.type_value` (8 bytes) — so a `[]Type` reader (8-byte stride)
// read `[t0, pad, t1, …]` instead of `[t0, t1, …]`. The legacy interp's
// tagged-Value model hid it; the byte-accurate comptime VM exposed it.
#import "modules/std.sx";
inner :: (args: []Type) -> string {
s := "";
i : i64 = 0;
while i < args.len {
s = concat(s, type_name(args[i]));
s = concat(s, " ");
i = i + 1;
}
return s;
}
outer :: (..$args) -> string { return inner($args); }
R :: #run outer(42, "hi", true);
main :: () {
print("[{}]\n", R);
}

View File

@@ -0,0 +1,28 @@
// Pack-fn (or any comptime-param fn) with a block body containing
// an explicit `return X;` lowers to clean IR — the inline-return
// path stores into a dedicated result slot and branches to the
// shared `ret_done` block instead of emitting a `ret` inside the
// caller's basic block. Without that, LLVM's verifier rejected the
// IR with "Terminator found in the middle of a basic block".
//
// Surfaced by the variadic heterogeneous type packs feature (step
// 1 made `..$args` parseable, so the simplest pack-fn smoke test
// exercised the bug). The root cause is broader than packs: ANY
// comptime fn with `is_comptime` params, a non-void return, and a
// block body with `return X;` had the same crash. `format`/`print`
// use arrow form (`=> expr`) or `#insert`-only bodies, so the bug
// was invisible until pack-fn bodies surfaced it.
//
// Once fixed, calling foo() reaches the body's `return 42;`, the
// inliner stores 42 into a result slot, the caller loads it as the
// inline value, and main prints "42".
#import "modules/std.sx";
foo :: (..$args) -> i64 { return 42; }
main :: () -> i32 {
n : i64 = foo(1, "hello");
print("{}\n", n);
return 0;
}

View File

@@ -0,0 +1,39 @@
// Feature 1 — heterogeneous protocol-constrained variadic pack (binding).
//
// `..xs: Show` (no `[]`, no `$`) is a pack, not a slice: each call site
// monomorphizes with the concrete per-position arg types (Decision 1 — a pack
// is a comptime mechanism, no runtime pack value), and `xs.len` is a comptime
// constant. Arity and element types vary per call; each call is a distinct
// specialization.
//
// DESIGN (locked): a pack element is viewed THROUGH the protocol — only the
// protocol's own interface (its methods, and the projections `xs.T` / `xs.value`)
// is accessible, NOT arbitrary concrete members. So `xs[i].get()` / `xs.T` are
// the intended interface; `xs[i].v` (a field on the concrete IntBox, not part
// of `Show`) is an error. Those are still being implemented — this example
// locks in only the binding + comptime `xs.len`, the protocol-agnostic part.
#import "modules/std.sx";
Show :: protocol(T: Type) {
get :: (self: *Self) -> T;
}
IntBox :: struct { v: i64; }
StrBox :: struct { s: string; }
impl Show(i64) for IntBox { get :: (self: *IntBox) -> i64 => self.v; }
impl Show(string) for StrBox { get :: (self: *StrBox) -> string => self.s; }
howmany :: (..xs: Show) -> i64 {
return xs.len;
}
main :: () -> i32 {
a := IntBox.{ v = 42 };
b := IntBox.{ v = 7 };
c := StrBox.{ s = "cool" };
print("n0={}\n", howmany()); // empty pack
print("n2={}\n", howmany(a, b)); // two elements
print("n3={}\n", howmany(a, b, c)); // heterogeneous: IntBox, IntBox, StrBox
0
}

View File

@@ -0,0 +1,24 @@
// Feature 1 — a pack argument that doesn't conform to the constraint protocol
// is a per-position error. `Naked` has no `impl Show`, so passing it to a
// `..xs: Show` pack is rejected (pointing at the offending argument).
#import "modules/std.sx";
Show :: protocol(T: Type) {
get :: (self: *Self) -> T;
}
IntBox :: struct { v: i64; }
impl Show(i64) for IntBox { get :: (self: *IntBox) -> i64 => self.v; }
Naked :: struct { x: i64; } // intentionally NOT `impl Show`
howmany :: (..xs: Show) -> i64 {
return xs.len;
}
main :: () -> i32 {
a := IntBox.{ v = 1 };
n := Naked.{ x = 2 };
print("{}\n", howmany(a, n)); // `n` does not conform to Show
0
}

View File

@@ -0,0 +1,31 @@
// Feature 1 — protocol-interface method calls on heterogeneous pack elements.
//
// `..xs: Greeter` binds per call shape; each `xs[i]` is the concrete element,
// and calling the protocol's own method `greet()` on it dispatches to that
// element's impl. Elements may be DIFFERENT concrete types (Dog, Cat) as long
// as each conforms to Greeter — this is the protocol-interface access the
// pack is for. (Protocol method decls omit the implicit `self`; impls list it.)
#import "modules/std.sx";
Greeter :: protocol {
greet :: (self: *Self) -> i64;
}
Dog :: struct { age: i64; }
Cat :: struct { lives: i64; }
impl Greeter for Dog { greet :: (self: *Dog) -> i64 => self.age; }
impl Greeter for Cat { greet :: (self: *Cat) -> i64 => self.lives * 100; }
pair_sum :: (..xs: Greeter) -> i64 {
return xs[0].greet() + xs[1].greet();
}
main :: () -> i32 {
d := Dog.{ age = 3 };
c := Cat.{ lives = 9 };
print("dog+cat={}\n", pair_sum(d, c)); // 3 + 900 = 903 (heterogeneous)
print("cat+dog={}\n", pair_sum(c, d)); // 900 + 3 = 903 (order swapped)
print("dog+dog={}\n", pair_sum(d, Dog.{ age = 4 })); // 3 + 4 = 7
0
}

View File

@@ -0,0 +1,30 @@
// Feature 1 — method calls on a PARAMETERIZED protocol pack (the canonical
// shape: `..xs: ValueListenable` where each element conforms with its own
// type-arg). Calling the protocol method `get()` on `xs[i]` resolves to the
// concrete element's impl, even though each element binds a different `T`.
//
// (Parameterised-protocol impl methods with a concrete source type are now
// registered as `<Source>.<method>`, so UFCS — and thus `xs[i].get()` —
// resolves them.)
#import "modules/std.sx";
Box :: protocol(T: Type) {
get :: (self: *Self) -> T;
}
IntCell :: struct { v: i64; }
StrCell :: struct { s: string; }
impl Box(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; }
impl Box(string) for StrCell { get :: (self: *StrCell) -> string => self.s; }
describe :: (..xs: Box) -> void {
// xs[0] : Box(i64), xs[1] : Box(string) — different type-args per position.
print("first={} second={}\n", xs[0].get(), xs[1].get());
}
main :: () -> i32 {
describe(IntCell.{ v = 11 }, StrCell.{ s = "hi" });
describe(StrCell.{ s = "x" }, IntCell.{ v = 99 });
0
}

View File

@@ -0,0 +1,22 @@
// Feature 1 — a pack element exposes ONLY the constraint protocol's interface.
// `xs[i].v` reaches a concrete field of IntCell that is not part of `Box`, so
// it's rejected even though IntCell does have `v` — a pack element is viewed
// through the protocol, like a constrained generic. (Protocol methods like
// `get()` ARE callable; see examples 193/194.)
#import "modules/std.sx";
Box :: protocol(T: Type) {
get :: (self: *Self) -> T;
}
IntCell :: struct { v: i64; }
impl Box(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; }
leak :: (..xs: Box) -> i64 {
return xs[0].v; // `v` is not part of Box — error
}
main :: () -> i32 {
print("{}\n", leak(IntCell.{ v = 5 }));
0
}

View File

@@ -0,0 +1,28 @@
// Feature 1 — value-position pack projection: `xs.<method>` projects a
// (zero-arg) protocol method over every element into a TUPLE of the per-element
// results. For a parameterised `Box(T)`, each element's method returns its own
// `T`, so the projected tuple is heterogeneous.
//
// xs.get ≈ (xs[0].get(), xs[1].get())
#import "modules/std.sx";
Box :: protocol(T: Type) {
get :: (self: *Self) -> T;
}
IntCell :: struct { v: i64; }
StrCell :: struct { s: string; }
impl Box(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; }
impl Box(string) for StrCell { get :: (self: *StrCell) -> string => self.s; }
show :: (..xs: Box) -> void {
vals := xs.get; // tuple (i64, string)
print("0={} 1={}\n", vals.0, vals.1);
}
main :: () -> i32 {
show(IntCell.{ v = 42 }, StrCell.{ s = "hi" });
show(StrCell.{ s = "x" }, IntCell.{ v = 7 }); // order swapped → (string, i64)
0
}

View File

@@ -0,0 +1,26 @@
// Feature 1 — pack spread into a call's positional arguments. `f(..xs.get)`
// projects `get` over the pack and spreads the resulting tuple into f's params:
// add2(..xs.get) ≈ add2(xs[0].get(), xs[1].get())
// The canonical's `mapper(..sources.value)` is this shape.
#import "modules/std.sx";
Box :: protocol(T: Type) {
get :: (self: *Self) -> i64;
}
IntCell :: struct { v: i64; }
Dbl :: struct { n: i64; }
impl Box(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; }
impl Box(i64) for Dbl { get :: (self: *Dbl) -> i64 => self.n * 2; }
add2 :: (a: i64, b: i64) -> i64 { return a + b; }
add3 :: (a: i64, b: i64, c: i64) -> i64 { return a + b + c; }
via2 :: (..xs: Box) -> i64 { return add2(..xs.get); }
via3 :: (..xs: Box) -> i64 { return add3(..xs.get); }
main :: () -> i32 {
print("two={}\n", via2(IntCell.{ v = 10 }, Dbl.{ n = 5 })); // 10 + 10 = 20
print("three={}\n", via3(Dbl.{ n = 1 }, IntCell.{ v = 2 }, Dbl.{ n = 3 })); // 2 + 2 + 6 = 10
0
}

View File

@@ -0,0 +1,25 @@
// Feature 1 — materialize a tuple from a pack via `(..xs.method)` (Decision 2:
// a pack is stored by materializing a tuple). `(..xs.get)` projects `get` over
// the pack and collects the results into a real tuple value, which can then be
// stored, indexed, and (for `Box(T)`) is heterogeneous per position.
#import "modules/std.sx";
Box :: protocol(T: Type) {
get :: (self: *Self) -> T;
}
IntCell :: struct { v: i64; }
StrCell :: struct { s: string; }
impl Box(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; }
impl Box(string) for StrCell { get :: (self: *StrCell) -> string => self.s; }
snapshot :: (..xs: Box) -> void {
t := (..xs.get); // tuple (i64, string) materialized from the pack
print("0={} 1={}\n", t.0, t.1);
}
main :: () -> i32 {
snapshot(IntCell.{ v = 42 }, StrCell.{ s = "hi" });
snapshot(StrCell.{ s = "x" }, IntCell.{ v = 7 }); // order swapped → (string, i64)
0
}

View File

@@ -0,0 +1,38 @@
// Feature 1 — TYPE-position pack projection `xs.T`. The per-element protocol
// type-arg `T` projects into a Pack of types, usable in type/signature
// positions: a tuple type `(..xs.T)` and a closure signature
// `Closure(..xs.T) -> R`. (`T` of each element comes from its
// `impl Box(T) for <elem>`.)
#import "modules/std.sx";
Box :: protocol(T: Type) {
get :: (self: *Self) -> T;
}
IntCell :: struct { v: i64; }
StrCell :: struct { s: string; }
Dbl :: struct { n: i64; }
impl Box(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; }
impl Box(string) for StrCell { get :: (self: *StrCell) -> string => self.s; }
impl Box(i64) for Dbl { get :: (self: *Dbl) -> i64 => self.n * 2; }
// Tuple type `(..xs.T)` — heterogeneous (i64, string), matched by the
// value-projection `(..xs.get)`.
snap :: (..xs: Box) -> void {
t : (..xs.T) = (..xs.get);
print("0={} 1={}\n", t.0, t.1);
}
// Closure signature `Closure(..xs.T) -> i64` — here `Closure(i64, i64) -> i64`.
// The closure literal's params are contextually typed from the projection.
fold :: (..xs: Box) -> i64 {
cb : Closure(..xs.T) -> i64 = (a, b) => a + b;
return cb(xs[0].get(), xs[1].get());
}
main :: () -> i32 {
snap(IntCell.{ v = 42 }, StrCell.{ s = "hi" }); // (i64, string)
print("fold={}\n", fold(IntCell.{ v = 10 }, Dbl.{ n = 5 })); // 10 + 10 = 20
0
}

View File

@@ -0,0 +1,31 @@
// Slice-of-protocol variadic `..xs: []P` — the RUNTIME counterpart to the
// comptime pack `..xs: P`. Each trailing arg is `xx`-erased to a `P` protocol
// value {ctx, vtable} and packed into a runtime `[]P`, so the elements can be
// indexed by a RUNTIME index and dispatched through the protocol interface
// (unlike a pack, which is comptime-only — see examples/163).
//
// This is the type-safe way to iterate a heterogeneous arg list at runtime:
// concrete per-position types are erased to the constraint protocol.
#import "modules/std.sx";
Show :: protocol { show :: (self: *Self) -> string; }
A :: struct { x: i64; }
B :: struct { s: string; }
impl Show for A { show :: (self: *A) -> string => "A"; }
impl Show for B { show :: (self: *B) -> string => "B"; }
// Runtime loop over a []Show: runtime index + protocol-method dispatch.
each :: (..xs: []Show) -> void {
i := 0;
while i < xs.len {
print("[{}]={}\n", i, xs[i].show());
i = i + 1;
}
}
main :: () -> i32 {
each(A.{ x = 1 }, B.{ s = "hi" }, A.{ x = 3 }); // heterogeneous, erased to Show
each(); // empty is fine (len 0)
0
}

View File

@@ -0,0 +1,25 @@
// Step 2.7 — pack-as-value diagnostics. A pack is comptime-only (Decision 1),
// so using the bare pack name where a runtime value is required is an error,
// with a context-tailored suggestion. All four categories below fire (the
// functions are monomorphized when called from main).
#import "modules/std.sx";
Show :: protocol { show :: (self: *Self) -> string; }
A :: struct {}
impl Show for A { show :: (self: *A) -> string => "A"; }
sink :: (v: i64) -> void { _ = v; }
storage :: (..xs: Show) -> void { y := xs; _ = y; } // A: store
call :: (..xs: Show) -> void { sink(xs); } // B: pass to a call
ret :: (..xs: Show) -> i64 { return xs; } // C: return
iter :: (..xs: Show) -> void { for xs (x) { _ = x; } } // D: runtime iterate
main :: () -> i32 {
storage(A.{});
call(A.{});
_ = ret(A.{});
iter(A.{});
0
}

View File

@@ -0,0 +1,32 @@
// `xx <pack>` materializes a comptime pack into a runtime slice (issue 0053):
// the explicit pack→slice bridge. With a `[]Any` target each element is boxed
// to `Any`; with a `[]P` target each is `xx`-erased to the protocol `P`. This is
// how you forward a pack to a runtime (`[]Any` / `[]P`) helper.
#import "modules/std.sx";
Show :: protocol { show :: (self: *Self) -> string; }
A :: struct {}
B :: struct { s: string; }
impl Show for A { show :: (self: *A) -> string => "A"; }
impl Show for B { show :: (self: *B) -> string => "B"; }
count_any :: (items: []Any) -> i64 { return items.len; }
show_all :: (items: []Show) -> i64 {
i := 0;
while i < items.len { print("{}\n", items[i].show()); i = i + 1; }
return items.len;
}
// `..$args` pack → []Any via `xx`.
fwd_any :: (..$args) -> i64 { return count_any(xx args); }
// `..xs: Show` pack → []Show via `xx`.
fwd_show :: (..xs: Show) -> i64 { return show_all(xx xs); }
main :: () -> i32 {
print("any={}\n", fwd_any(1, "hi", 2.5)); // 3
print("show={}\n", fwd_show(A.{}, B.{ s = "x" }, A.{})); // A B A, 3
0
}

View File

@@ -0,0 +1,25 @@
// Phase 4.2 (core) — a generic struct with a pack type-param `..$Ts: []Type`
// and a pack-shaped tuple field `(..$Ts)`. Each instantiation binds the
// remaining type args as the pack, so the field is a tuple of those per-position
// types. Storing the whole tuple field and reading its elements both work.
#import "modules/std.sx";
Box :: struct($R: Type, ..$Ts: []Type) {
r: $R;
pair: (..$Ts); // tuple of the pack's element types
}
main :: () -> i32 {
// Box(i64, i32, string): R=i64, Ts=[i32, string], pair: (i32, string).
a : Box(i64, i32, string) = ---;
a.r = 7;
a.pair = (42, "hi"); // whole-tuple field store
print("a: r={} 0={} 1={}\n", a.r, a.pair.0, a.pair.1);
// A different shape → a different per-position tuple field.
b : Box(bool, string, bool) = ---; // Ts=[string, bool], pair: (string, bool)
b.pair = ("x", true);
print("b: 0={} 1={}\n", b.pair.0, b.pair.1);
0
}

View File

@@ -0,0 +1,29 @@
// Phase 4.2 — the canonical `Combined` struct's storage layer: a generic
// struct whose field is a pack of PARAMETERIZED-protocol values,
// `sources: (..VL(Ts))` → `(VL(T0), VL(T1), …)`. Each `VL(Ti)` is a real
// 16-byte protocol value (issue: parameterized-protocol value types), and
// `(..VL(Ts))` applies `VL` per pack element. Instantiate + whole-tuple store
// of `xx`-erased values + per-element method dispatch all work.
#import "modules/std.sx";
VL :: protocol(T: Type) { get :: (self: *Self) -> T; }
IntCell :: struct { v: i64; }
StrCell :: struct { s: string; }
impl VL(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; }
impl VL(string) for StrCell { get :: (self: *StrCell) -> string => self.s; }
Combined :: struct($R: Type, ..$Ts: []Type) {
sources: (..VL(Ts)); // (VL(T0), VL(T1), …) — tuple of protocol values
value: $R;
}
main :: () -> i32 {
// Combined(i64, i64, string): R=i64, Ts=[i64, string],
// sources: (VL(i64), VL(string)).
c : Combined(i64, i64, string) = ---;
c.sources = (xx IntCell.{ v = 10 }, xx StrCell.{ s = "hi" });
c.value = 99;
print("{} {} {}\n", c.sources.0.get(), c.sources.1.get(), c.value); // 10 hi 99
0
}

View File

@@ -0,0 +1,27 @@
// Phase 6 — pack-spread in a parameterized-type's arg list:
// `Combined($R, ..sources.T)`. Inside a pack-fn, `..sources.T` projects each
// source's protocol type-arg and spreads them into the generic struct's pack
// type-param `..$Ts`, so `Combined(i64, ..sources.T)` for a single `VL(i64)`
// source instantiates `Combined(i64, i64)` (field `sources: (VL(i64))`).
#import "modules/std.sx";
VL :: protocol(T: Type) { get :: (self: *Self) -> T; }
IntCell :: struct { v: i64; }
impl VL(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; }
Combined :: struct($R: Type, ..$Ts: []Type) {
sources: (..VL(Ts));
value: $R;
}
make :: (..sources: VL) -> i64 {
c : Combined(i64, ..sources.T) = ---; // instantiate with the spread type-arg
c.sources.0 = xx sources[0]; // erase the concrete source to VL(i64)
return c.sources.0.get();
}
main :: () -> i32 {
print("{}\n", make(IntCell.{ v = 7 })); // 7
0
}

View File

@@ -0,0 +1,28 @@
// Phase 6 — `c.sources = (..sources)`: materialize a pack into a
// protocol-typed tuple field, erasing each concrete pack element to the field's
// protocol slot. The pack `..sources: VL` holds concrete cells; `(..sources)`
// into a `(..VL(Ts))` field `xx`-erases each to its `VL(Ti)` value.
#import "modules/std.sx";
VL :: protocol(T: Type) { get :: (self: *Self) -> T; }
IntCell :: struct { v: i64; }
StrCell :: struct { s: string; }
impl VL(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; }
impl VL(string) for StrCell { get :: (self: *StrCell) -> string => self.s; }
Combined :: struct($R: Type, ..$Ts: []Type) {
sources: (..VL(Ts));
value: $R;
}
build :: (..sources: VL) -> void {
c : Combined(i64, ..sources.T) = ---;
c.sources = (..sources); // pack → tuple, per-element erase
print("{} {}\n", c.sources.0.get(), c.sources.1.get());
}
main :: () -> i32 {
build(IntCell.{ v = 10 }, StrCell.{ s = "hi" }); // 10 hi
0
}

View File

@@ -0,0 +1,18 @@
// Phase 6 — `mapper(..sources.value)`: project a method over a pack and spread
// the results into a closure call. The mapper lambda's params are contextually
// typed from the `Closure(...)` parameter even though `apply` is a pack-fn.
#import "modules/std.sx";
VL :: protocol(T: Type) { get :: (self: *Self) -> T; }
IntCell :: struct { v: i64; }
impl VL(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; }
apply :: (mapper: Closure(i64, i64) -> i64, ..sources: VL) -> i64 {
return mapper(..sources.get); // (a, b) => a + b applied to (i0.get(), i1.get())
}
main :: () -> i32 {
print("{}\n", apply((a, b) => a + b, IntCell.{ v = 40 }, IntCell.{ v = 2 })); // 42
0
}

View File

@@ -0,0 +1,36 @@
// Phase 6 — the canonical heterogeneous `map`, end to end. A pack-fn whose
// return type `$R` is inferred from the mapper's closure return:
// - `mapper: Closure(..sources.T) -> $R` types the lambda's params from the
// projected pack element types, and its body (`a + b`) drives `$R`.
// - `$R` is inferred at the call site from the lowered mapper's closure ret,
// bound into the mono (`-> VL($R)` ⇒ `VL(i64)`, `Combined($R, ..)` ⇒
// `Combined(i64, ..)`), and folded into the mangle.
// - `(..sources)` materializes the pack into the `(..VL(Ts))` field (per-element
// erase) and `mapper(..sources.get)` projects+spreads; `xx c` erases the
// generic-struct instance to `VL(i64)` via the generic impl's monomorphized
// thunk.
#import "modules/std.sx";
VL :: protocol(T: Type) { get :: (self: *Self) -> T; }
IntCell :: struct { v: i64; }
impl VL(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; }
Combined :: struct($R: Type, ..$Ts: []Type) {
sources: (..VL(Ts));
value: $R;
}
impl VL($R) for Combined($R, ..$Ts) { get :: (self: *Combined) -> $R => self.value; }
map :: (mapper: Closure(..sources.T) -> $R, ..sources: VL) -> VL($R) {
c : Combined($R, ..sources.T) = ---;
c.sources = (..sources);
c.value = mapper(..sources.get);
return xx c;
}
main :: () -> i32 {
r := map((a, b) => a + b, IntCell.{ v = 40 }, IntCell.{ v = 2 });
print("{}\n", r.get()); // 42
0
}

View File

@@ -0,0 +1,22 @@
// Regression (stdlib E4): an imported pack function whose fixed-prefix param
// type is visible only in its defining module must resolve during pack
// monomorphization. `lib.sx` imports `dep.sx` (which defines `Needs`) and
// exposes `make() -> Needs` plus `use_pack(n: Needs, ..$args)`. `main` imports
// ONLY `lib.sx`, so `Needs` is two flat hops away and not bare-visible here —
// main never names it.
//
// Before the fix, `monomorphizePackFn` restored the caller's source before
// re-binding the fixed-prefix params, so `n: Needs` was resolved in main's
// context and rejected with "type 'Needs' is not visible" — even though the
// control plain fn `use_plain(n: Needs)` (typed via the source-pinned call-arg
// path) ran fine. The fixed-prefix param is now resolved under the pack fn's own
// source (`fd.body.source_file`), matching the rest of the pack signature/body.
#import "modules/std.sx";
#import "0544-packs-imported-pack-fn-fixed-param-source-pin/lib.sx";
main :: () -> i32 {
x := make();
print("plain {}\n", use_plain(x));
print("pack {}\n", use_pack(x, 1, 2));
return 0;
}

View File

@@ -0,0 +1,3 @@
// `Needs` lives two flat-import hops away from `main` (main -> lib -> dep), so
// it is NOT bare-visible at the call site under non-transitive visibility.
Needs :: struct { v: i64; }

View File

@@ -0,0 +1,16 @@
// `lib.sx` imports `dep.sx`, so `Needs` is bare-visible HERE. A module that
// imports only `lib.sx` cannot see `Needs` (non-transitive). The pack fn's
// fixed-prefix param `n: Needs` must therefore resolve in this module's
// context, not the caller's.
#import "modules/std.sx";
#import "dep.sx";
make :: () -> Needs => Needs.{ v = 7 };
// Control: a plain (non-pack) fn with the same fixed param already resolves in
// its defining module — the cross-module call-arg typing path is source-pinned.
use_plain :: (n: Needs) -> i64 => n.v;
// Pack fn: the fixed-prefix param `n: Needs` is bound during pack
// monomorphization. Its type must resolve under this module's source.
use_pack :: (n: Needs, ..$args) -> i64 => n.v + args[0];

View File

@@ -0,0 +1,63 @@
// `inline for` element form over a pack — multi-iterable parity with the
// runtime for-loop. Position 0 drives the unroll count (a pack's arity or a
// bounded range's span); trailing iterables pair with it. A pack capture is
// the concrete per-position element viewed through the constraint protocol
// (same semantics as `xs[i]`); a range capture is a comptime cursor.
//
// inline for xs (x) — element form
// inline for xs, 0.. (x, i) — element + paired index
// inline for 0..xs.len, xs (i, x) — range driver, trailing pack
// inline for xs { } — captureless; N=0 unrolls nothing
#import "modules/std.sx";
Show :: protocol { show :: (self: *Self) -> string; }
IntBox :: struct { v: i64; }
StrBox :: struct { s: string; }
impl Show for IntBox { show :: (self: *IntBox) -> string { int_to_string(self.v) } }
impl Show for StrBox { show :: (self: *StrBox) -> string { self.s } }
bare :: (..xs: Show) {
inline for xs (x) {
print("bare: {}\n", x.show());
}
}
elem_and_index :: (..xs: Show) {
inline for xs, 0.. (x, i) {
print("{}: {}\n", i, x.show());
}
}
range_driver :: (..xs: Show) {
inline for 0..xs.len, xs (i, x) {
print("r{}: {}\n", i, x.show());
}
}
offset_index :: (..xs: Show) {
inline for xs, 10.. (x, i) {
print("{} -> {}\n", i, x.show());
}
}
captureless :: (..xs: Show) {
n := 0;
inline for xs { n += 1; }
print("ran {}\n", n);
}
value_pos :: (..xs: Show) {
inline for xs (x) {
print("val: {}\n", x);
}
}
empty :: (..xs: Show) {
inline for xs (x) { print("never\n"); }
print("empty ok\n");
}
main :: () {
bare(IntBox.{ v = 7 }, StrBox.{ s = "hi" });
elem_and_index(IntBox.{ v = 7 }, StrBox.{ s = "hi" });
range_driver(IntBox.{ v = 1 }, StrBox.{ s = "two" });
offset_index(StrBox.{ s = "x" }, StrBox.{ s = "y" });
captureless(IntBox.{ v = 0 }, IntBox.{ v = 0 }, IntBox.{ v = 0 });
value_pos(IntBox.{ v = 42 });
empty();
}

View File

@@ -0,0 +1,6 @@
// Companion of 0546: the authoring module for the fn-alias re-exports.
#import "modules/std.sx";
helper :: () -> i64 { 7 }
first_of :: (xs: []$T) -> T { xs[0] }
my_pack :: (..$args) -> i64 { args[0] + args[1] }

View File

@@ -0,0 +1,35 @@
// Function aliases dispatch exactly like their target, across every fn
// kind: plain, runtime-generic ([]$T), and comptime-pack (..$args) — with
// bare, renamed, and namespace-member RHS. The alias is an ordinary own
// declaration, so it re-exports one flat-import level (companion file
// -rich.sx authors the fns aliased here and via the namespace).
// Regression (issue 0121): comptime-pack fn aliases (and ALL renamed fn
// aliases) used to fail "unresolved '<alias>'" — only same-name re-exports
// worked, through the name-keyed global registry.
#import "modules/std.sx";
s :: #import "modules/std.sx";
r :: #import "0546-packs-fn-alias-rich.sx";
pack_sum :: (..$args) -> i64 {
args[0] + args[1]
}
sum_alias :: pack_sum; // same-file pack alias (the 0121 repro)
helper2 :: r.helper; // renamed plain, namespace RHS
head_of :: r.first_of; // renamed runtime-generic, namespace RHS
sum2 :: r.my_pack; // renamed pack, namespace RHS
my_print :: s.print; // std's print — comptime pack + $fmt
my_format :: s.format; // value-returning sibling
main :: () {
print("pack: {}\n", sum_alias(3, 4));
print("plain: {}\n", helper2());
arr := .[10, 20, 30];
xs : []i64 = arr;
print("generic: {}\n", head_of(xs));
print("ns-pack: {}\n", sum2(20, 22));
my_print("std-print: {} {}\n", 1, "two");
t := my_format("std-format {}", 42);
my_print("{}\n", t);
}

View File

@@ -0,0 +1,27 @@
// `xx <pack>[i]` erased to a protocol-typed local.
//
// Erasing a single comptime-pack element to a protocol scalar routes through
// buildProtocolErasure. A pack index is a comptime rvalue (a pack has no
// runtime storage — `sources[i]` resolves to the call-site arg, which only
// gets storage when lowered as a value), so the erasure must heap-copy the
// materialized element rather than take its address.
//
// Regression (issue 0135): `xx sources[0]` used to lower the bare pack as a
// value and error with "pack 'sources' has no runtime value".
#import "modules/std.sx";
VL :: protocol(T: Type) { get :: (self: *Self) -> T; }
IntCell :: struct { v: i64; }
impl VL(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; }
first :: (..sources: VL) -> i64 {
x : VL(i64) = xx sources[0]; // erase element 0 to VL(i64)
return x.get();
}
main :: () -> i32 {
print("{}\n", first(IntCell.{ v = 7 })); // 7
print("{}\n", first(IntCell.{ v = 42 }, IntCell.{ v = 99 })); // 42 (element 0)
0
}

View File

@@ -0,0 +1,25 @@
// Erase two DISTINCT comptime-pack elements to protocol locals — each gets
// its own heap copy and resolves to its OWN concrete type's method (IntCell.get
// vs Doubler.get), proving the per-element erasure picks the right vtable.
//
// Regression (issue 0135): single-element `xx pack[i]` erasure to a protocol
// scalar was unsupported (the bare pack lowered as a value and errored).
#import "modules/std.sx";
VL :: protocol(T: Type) { get :: (self: *Self) -> T; }
IntCell :: struct { v: i64; }
impl VL(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; }
Doubler :: struct { n: i64; }
impl VL(i64) for Doubler { get :: (self: *Doubler) -> i64 => self.n * 2; }
sum_two :: (..sources: VL) -> i64 {
a : VL(i64) = xx sources[0]; // erase element 0
b : VL(i64) = xx sources[1]; // erase element 1
return a.get() + b.get();
}
main :: () -> i32 {
print("{}\n", sum_two(IntCell.{ v = 10 }, Doubler.{ n = 16 })); // 10 + (16*2) = 42
0
}

View File

@@ -0,0 +1,36 @@
// E6BR-4 (MIXED pack source — the trap cell) — a pack-closure param-impl source
// `Closure(*Box, ..$args) -> $R` mixes a CONCRETE fixed prefix (`*Box`) with pack
// metadata (`..$args`, `$R`). The reconciled choke-point decides template-vs-author
// AT THE LEAF: `*Box` is resolved SOURCE-AWARE through `resolveCompound` while
// `..$args`/`$R` stay pack metadata via `PackResolver` — NOT a top-level
// "contains-unbound → no-author wrapper" router, which would send `*Box` down the
// global last-wins path (the trap both engines flagged). With two flat `Box`
// authors and none own, the concrete `*Box` prefix is a genuine collision and the
// build exits 1 — proving the prefix IS routed source-aware (it caught the
// ambiguity) while the pack parts did not spuriously error.
//
// Fail-before (pre-E6BR-4): the wrapped/pack source fell to the no-author
// `type_bridge.resolveTemplateSignatureType` wrapper (global last-wins, no
// diagnostic), so the `*Box` collision registered silently. Protects the pure-pack
// `Closure(..$args) -> $R` bridge in `library/modules/ffi/objc_block.sx` (0504 /
// 1302 / 1304 stay byte-identical), whose single author keeps the prefix-free path.
#import "modules/std.sx";
#import "0829-packs-param-impl-mixed-pack-source-ambiguous/a.sx";
#import "0829-packs-param-impl-mixed-pack-source-ambiguous/b.sx";
Block :: struct { tag: i32; }
Sink :: protocol(T: Type) {
convert :: (self: *Self) -> T;
}
impl Sink(Block) for Closure(*Box, ..$args) -> $R {
convert :: (self: Closure(*Box, ..$args) -> $R) -> Block {
.{ tag = 0 }
}
}
main :: () -> i32 {
0
}

View File

@@ -0,0 +1,9 @@
// One of two flat-imported `Box` authors; with no own author the FIXED PREFIX
// `*Box` of the pack-closure source is a genuine collision.
Box :: struct { a: i32; }
a_box :: () -> i32 {
b : Box = ---;
b.a = 1;
return b.a;
}

View File

@@ -0,0 +1,8 @@
// The second flat-imported `Box` author — a DISTINCT nominal from a.sx's `Box`.
Box :: struct { b: i32; }
b_box :: () -> i32 {
x : Box = ---;
x.b = 2;
return x.b;
}

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,4 @@
60
1 2 3 4 5
60
10 20 30

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,3 @@
42 hello true 3.140000
point: (10,20) 99
3

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
pack parse ok

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
pack type rep ok

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
pack impl match ok

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
7

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,2 @@
-1
42

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 @@
0 1 1 3 2

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
42 99

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
hello

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,5 @@
error: pack index 2 out of bounds: 'args' has 1 element
--> examples/packs/0510-packs-pack-index-oob.sx:14:32
|
14 | foo :: (..$args) -> $R => args[2];
| ^

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
3

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,5 @@
error: pack 'args' must be indexed by a compile-time constant — a pack is comptime-only and has no runtime value
--> examples/packs/0512-packs-pack-runtime-index.sx:18:24
|
18 | x : Any = args[i]; // ERROR: runtime index into a comptime-only pack
| ^

View File

@@ -0,0 +1 @@

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,2 @@
703
900

Some files were not shown because too many files have changed in this diff Show More