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 @@
// Accessing an unknown field on a struct produces a clear
// `error: field 'X' not found on type 'Y'` diagnostic and exit 1.
Vec :: struct { x: f32; y: f32; }
main :: () -> i32 {
v := Vec.{ x = 1.0, y = 2.0 };
return xx v.bogus;
}

View File

@@ -0,0 +1,7 @@
// Out-of-range tuple index produces a clear
// `error: field 'N' not found on type 'tuple'` diagnostic and exit 1.
main :: () -> i32 {
t := (10, 20);
return xx t.42;
}

View File

@@ -0,0 +1,7 @@
// Dot-shorthand `.Variant(args)` without a tagged-union target type produces
// `error: cannot infer enum type for '.X'` instead of crashing.
main :: () -> i32 {
x := .Foo(1, 2);
return 0;
}

View File

@@ -0,0 +1,22 @@
// A match arm with a variant name that doesn't exist on the subject's
// enum/tagged-union produces `error: no variant 'X' on type 'Y'` instead of
// falling back to the arm index (which used to cause duplicate switch cases
// and LLVM verification failures).
#import "modules/std.sx";
Shape :: enum {
circle: f32;
rect: struct { w, h: f32; };
none;
}
main :: () {
s :Shape = .circle(3.14);
if s == {
case .circle: (r) { print("r={}\n", r); }
case .Bogus: (x) { print("bogus={}\n", x); }
case .none: print("none\n");
case .rect: print("rect\n");
}
}

View File

@@ -0,0 +1,14 @@
// Passing a default-conv sx function as a abi(.c) fn-pointer
// silently mismatches ABIs — historically that meant the C-side caller
// supplied no `__sx_ctx` slot 0 and the sx-side body read garbage.
// The compiler now rejects the coercion outright with a "call-convention
// mismatch" diagnostic.
#import "modules/std.sx";
sx_handler :: (arg: *void) -> *void { return arg; }
main :: () -> i32 {
fp : (*void) -> *void abi(.c) = sx_handler;
return 0;
}

View File

@@ -0,0 +1,14 @@
// `compile_error(msg)` raises a build-time diagnostic at the call
// site. Used by builder fns (e.g. `#insert build_block_convert(...)`)
// to reject malformed pack shapes with a clear message rather than
// silently producing wrong code.
//
// The diagnostic appears at the source position of the
// `compile_error` call. Argument must be a string literal — runtime
// expressions can't be reported as compile errors.
#import "modules/std.sx";
#run compile_error("intentional compile error from #run");
main :: () { }

View File

@@ -0,0 +1,25 @@
// Scalar binary operators check operand-type compatibility. The result
// type is otherwise taken from the left operand, so mixing a non-numeric
// type (here `string`) would lower as `<op> : i64` and either reinterpret
// the string's bytes (arithmetic / bitwise → garbage) or feed mismatched
// types to `icmp` (ordering → LLVM verifier failure). All such mismatches
// are now rejected at compile time:
// - arithmetic `+ - * / %` (numeric / vector / pointer operands)
// - ordering `< <= > >=` (numeric / enum / pointer operands)
// - bitwise `& | ^` (integer / enum operands)
// - shift `<< >>` (integer / enum operands)
// Legitimate mixes (int+float promotion, flags-enum bitwise, enum/pointer
// comparison) are unaffected — see `examples/50-smoke.sx`.
#import "modules/std.sx";
main :: () -> i32 {
n : i64 = 40;
s : string = "nope";
a := n + s; // arithmetic: i64 + string
b := s * n; // arithmetic: non-numeric LHS (string * i64)
c := n < s; // ordering: i64 < string
d := n & s; // bitwise: i64 & string
e := n << s; // shift: i64 << string
0
}

View File

@@ -0,0 +1,18 @@
// A by-reference loop capture (`for xs (*m)`) binds `m` to a `*T`.
// Passing it where a `T` value is expected used to slip through to the
// LLVM verifier ("Call parameter type does not match function signature").
// The compiler now reports it at the call site with a fix-it: write `m.*`.
#import "modules/std.sx";
Move :: struct { flag: i64; }
take :: (m: Move) -> i64 { return m.flag; }
main :: () -> i32 {
moves : [2]Move = .[ Move.{ flag = 1 }, Move.{ flag = 2 } ];
for moves (*m) {
take(m);
}
return 0;
}

View File

@@ -0,0 +1,18 @@
// Passing a `*T` where a `T` value is expected is caught at the call site —
// not only for `for xs (*m)` loop captures (see 215) but for any pointer,
// here a `*Move` parameter forwarded into a by-value parameter. Without the
// check this slipped through to the LLVM verifier as "Call parameter type
// does not match function signature".
#import "modules/std.sx";
Move :: struct { flag: i64; }
take :: (m: Move) -> i64 { return m.flag; }
forward :: (m: *Move) -> i64 { return take(m); }
main :: () -> i32 {
mv : Move = .{ flag = 7 };
return xx forward(@mv);
}

View File

@@ -0,0 +1,14 @@
// `.*` on a non-pointer must be a clean compile diagnostic, NOT a codegen
// panic. Regression: a stale `value.*` (e.g. after a parameter changed from
// `*T` to `T` by value) used to lower a `.deref` with an `.unresolved` result
// type, which slipped through to emit_llvm's "unresolved type reached LLVM
// emission" panic with no source location. `lowerDerefExpr` now diagnoses it.
// Expected: a clean error pointing at the deref; exit 1.
Point :: struct { x: i32; y: i32; }
main :: () -> i32 {
p : Point = .{ x = 3, y = 4 };
q := p.*; // ERROR: `p` is a Point value, not a pointer
return q.x;
}

View File

@@ -0,0 +1,20 @@
// Auto-ref of a COMPOUND lvalue (field access / index / deref) passed to a
// `*T` parameter must reference the REAL lvalue, not a copy. Regression: a
// field-access argument (e.g. `make_move(self.board, m)`) silently passed the
// address of a temporary copy, so mutations through the pointer were lost with
// no diagnostic. A plain local (`bump(x)`) already auto-ref'd; now compound
// lvalues do too. Expected: w.s.v == 42 (the real field was mutated).
#import "modules/std.sx";
S :: struct { v: i32; }
W :: struct { s: S; }
bump :: (p: *S) { p.v = p.v + 41; }
main :: () -> i32 {
w : W = .{ s = .{ v = 1 } };
bump(w.s); // field access, no `@` — auto-refs &w.s
print("w.s.v = {}\n", w.s.v); // 42, not 1
return 0;
}

View File

@@ -0,0 +1,10 @@
// A value parameter declared `T: Type` (WITHOUT the `$` generic sigil) used in
// a type position is rejected with a hint to write `$T: Type`. Previously the
// type resolver silently fabricated a 0-field struct named `T`, so the call
// compiled and rendered as `T{}` at runtime with no diagnostic.
// Regression (issue 0064). Expected: one error per `T` use site; exit 1.
idwrap :: (T: Type, f: Closure() -> T) -> T { return f(); }
main :: () -> i32 {
return idwrap(i32, closure(() -> i32 { return 7; }));
}

View File

@@ -0,0 +1,13 @@
// An identifier used in a type position that names no declared type, builtin,
// or in-scope generic parameter is rejected. Previously the type resolver's
// empty-struct-stub fallback silently interned a 0-field struct under the typo,
// so the program compiled and ran. Regression (issue 0064, broader fix).
// Expected: a clean "unknown type" error at the field; exit 1.
Point :: struct {
x: i32;
y: Coordnate; // typo for a non-existent type
}
main :: () -> i32 {
return 0;
}

View File

@@ -0,0 +1,10 @@
// An identifier used in a LOCAL variable's type annotation that names no
// declared type is rejected, exactly like a signature or struct-field use.
// Previously a body-level annotation bypassed the check: the type resolver's
// empty-struct stub silently gave the local a 0-field type, so `v: Coordnate
// = 5` compiled and ran (the `5` dropped) with no diagnostic. Regression
// (issue 0064, body-level positions). Expected: error at the annotation; exit 1.
main :: () -> i32 {
v: Coordnate = 5;
return 0;
}

View File

@@ -0,0 +1,12 @@
// An unknown type in a NESTED closure body's local annotation is rejected,
// with the enclosing function's generic params still in scope. Previously the
// body walk stopped at closure / nested-function boundaries, so a typo'd type
// inside a closure slipped through and silently became a 0-field struct.
// Regression (issue 0064, nested scopes). Expected: error at the annotation; exit 1.
main :: () -> i32 {
f := closure(() -> i32 {
bad: Coordnate = ---;
return 0;
});
return f();
}

View File

@@ -0,0 +1,12 @@
// `cast(T)` where `T` is a value parameter declared `: Type` (without the `$`
// generic sigil) is rejected with the generic-param hint. Previously it
// silently cast to a fabricated empty struct (an unknown *literal* cast target
// already errored via value resolution, but the value-param case was silent).
// Regression (issue 0064, cast position). Expected: tailored error; exit 1.
conv :: (T: Type, x: i32) -> i32 {
return cast(T) x;
}
main :: () -> i32 {
return conv(i32, 5);
}

View File

@@ -0,0 +1,13 @@
// A tuple literal used in a type position (`(i32, i32)` reinterpreted as a tuple
// type at a type-demanding site like `size_of`) must list only types. A non-type
// element — here the `1` in `(i32, 1)` — is rejected with a user-facing
// diagnostic instead of silently fabricating an `i64` field for that slot.
// Regression (issue 0067).
// Expected: a clean "tuple type element is not a type" error at the `1`; exit 1.
#import "modules/std.sx";
main :: () -> i32 {
print("bad tuple type size = {}\n", size_of((i32, 1)));
0
}

View File

@@ -0,0 +1,18 @@
// A top-level VALUE constant name used in a type position is rejected. Without
// the fix the unknown-type pass added every `const_decl` name to its declared-
// type set, so a value const (`NotAType :: 123`) satisfied the check and the
// type resolver's unknown-name fallback then fabricated an empty struct — the
// program ran and printed `NotAType{}`. Now only consts whose value introduces a
// type (declarations / type-expression aliases) count as type names.
// Regression (issue 0068).
// Expected: a clean "unknown type 'NotAType'" error at the annotation; exit 1.
#import "modules/std.sx";
NotAType :: 123;
main :: () -> i32 {
v: NotAType = ---;
print("value = {}\n", v);
return 0;
}

View File

@@ -0,0 +1,19 @@
// A top-level global initialized from a non-constant expression (here a field
// access on a module constant, `K.x`) is rejected with a diagnostic. Without
// the fix `registerTopLevelGlobal`'s init_val serializer handled only literals
// / array / struct literals / identifiers and let every other shape fall through
// to a null payload, so the global silently zero-initialized (`g=0`) — a wrong
// value with no error.
// Regression (issue 0072).
// Expected: "global 'g' must be initialized by a compile-time constant"; exit 1.
#import "modules/std.sx";
Point :: struct { x: i32; y: i32; }
K : Point : Point.{ x = 9, y = 4 };
g : i32 = K.x;
main :: () -> i32 {
print("g={}\n", g);
return g;
}

View File

@@ -0,0 +1,16 @@
// A value binding (parameter or local `var`) spelled as a reserved/builtin
// type name is rejected at the declaration site, across every declaration
// form: a parameter name (`u8`), a typed local (`i64`, `bool`), and a `:=`
// local (`string`). Such a spelling parses as a `.type_expr` rather than an
// `.identifier`, so the address-of family in lowering mis-lowers it (issue
// 0076). Expected: one error per offending name; exit 1.
#import "modules/std.sx";
takes_u8 :: (u8: i32) -> i32 { return u8; }
main :: () -> i32 {
i64 : i32 = 3;
bool : bool = true;
string := "x";
return 0;
}

View File

@@ -0,0 +1,16 @@
// A value binding spelled as a reserved type name (`i2`, the `sN` arbitrary-
// width int syntax) is rejected at its declaration site even when it lives in
// an IMPORTED module — the reserved-name binding diagnostic covers every
// compiled module, not just the main file. Without universal coverage the
// binding reaches lowering and aborts LLVM verification (a loaded aggregate
// passed by value to a `*Box` param).
//
// Regression (issue 0077): the imported-module facet of issue 0076. Expected:
// one clean diagnostic pointing at the imported module's `i2 := ...`, exit 1 —
// NOT an LLVM verifier abort.
#import "modules/std.sx";
mod :: #import "1120-diagnostics-imported-reserved-type-name/mod.sx";
main :: () -> i32 {
return mod.run_imported_reserved_name();
}

View File

@@ -0,0 +1,16 @@
#import "modules/std.sx";
Box :: struct { total: i64 = 0; count: i64 = 0; }
update :: ufcs (self: *Box, n: i64) {
self.total += n;
self.count += 1;
}
run_imported_reserved_name :: () -> i32 {
i2 := Box.{ total = 0, count = 0 };
update(@i2, 5);
i2.update(7);
print("imported i2 total={} count={}\n", i2.total, i2.count);
return 0;
}

View File

@@ -0,0 +1,30 @@
// Reserved/builtin type names are rejected as binding NAMES across every
// control-flow and destructuring form, not just plain `var`/param decls: a
// destructure name (`i2`), an `if`/`while` optional binding (`u8`/`i16`), a
// `for` capture and index name (`bool`/`i32`), and a match-arm capture
// (`string`). Each spelling parses as a `.type_expr`, so the address-of family
// in lowering mis-lowers it (a loaded aggregate passed by value to a `ptr`
// param → LLVM verifier abort). The declaration-site diagnostic comes from one
// EXHAUSTIVE binding-name walk, so no syntactic binding form can slip through.
//
// Regression (issue 0076, attempt-4 coverage). Expected: one error per
// offending name; exit 1 — NOT an LLVM verifier abort.
#import "modules/std.sx";
pair :: () -> (i64, i64) { (1, 2) }
maybe :: () -> ?i64 { return null; }
main :: () -> i32 {
i2, rest := pair(); // destructure name
if u8 := maybe() { } // if optional binding
while i16 := maybe() { break; } // while optional binding
xs := [3]i64.{ 10, 20, 30 };
for xs (bool) { } // for capture name
for xs, 0.. (v, i32) { } // for index name
opt: ?i64 = 5;
r := if opt == { // match-arm capture
case .some: (string) { 0 }
case .none: { 0 }
};
return 0;
}

View File

@@ -0,0 +1,30 @@
// A reserved/builtin type name is rejected as a binding name inside an `impl`
// block's method too — both as a parameter (`u8`) and as a local (`i2`). The
// impl method is reached through the exhaustive binding-name walk's
// `impl_block` arm (→ each method's `fn_decl`), so an `impl` method is no more
// exempt than a free function. Without the diagnostic the reserved local's
// `@i2` mis-lowers (a loaded aggregate passed by value to a `*Box` param →
// LLVM verifier abort).
//
// Regression (issue 0076, attempt-4 coverage). Expected: one error for the
// param and one for the local; exit 1.
#import "modules/std.sx";
Box :: struct { total: i64 = 0; count: i64 = 0; }
update :: (self: *Box, n: i64) { self.total += n; self.count += 1; }
Doer :: protocol { go :: (self: *Self, n: i64); }
impl Doer for Box {
go :: (self: *Box, u8: i64) {
i2 := Box.{ total = 1 };
update(@i2, u8);
self.total += i2.total;
}
}
main :: () -> i32 {
b := Box.{};
b.go(7);
return 0;
}

View File

@@ -0,0 +1,28 @@
// A reserved/builtin type name is rejected as the error-tag binding of a
// `catch` (`u8`) and of an `onfail` (`i64`). Both are reached through the
// exhaustive binding-name walk's `catch_expr` / `onfail_stmt` arms. The tag is
// a scalar, so before the diagnostic these spellings were silently accepted
// (they never reached the address-of mis-lowering) — the binding must still be
// rejected at its declaration.
//
// Regression (issue 0076, attempt-4 coverage). Expected: one error for each
// binding; exit 1.
#import "modules/std.sx";
E :: error { Bad }
must :: (n: i32) -> !E {
if n < 0 { raise error.Bad; }
return;
}
classify :: (n: i32) -> !E {
onfail (i64) { } // onfail tag binding
must(n) catch (u8) { return; }; // catch tag binding
return;
}
main :: () -> i32 {
classify(-1) catch { };
return 0;
}

View File

@@ -0,0 +1,15 @@
// A reserved type name used as a DESTRUCTURE binding name (`i2`) is rejected
// even when it lives in an IMPORTED module — the exhaustive binding-name walk
// descends the `namespace_decl` an `mod :: #import` wraps and renders the
// diagnostic against that module's source (issue 0077's universal-coverage
// rule applied to the destructure form). Without it the binding reaches
// lowering and aborts LLVM verification.
//
// Regression (issues 0076 + 0077, attempt-4 coverage). Expected: one clean
// diagnostic pointing at the imported module's `i2, rest := ...`, exit 1.
#import "modules/std.sx";
mod :: #import "1124-diagnostics-imported-reserved-destructure/mod.sx";
main :: () -> i32 {
return mod.run();
}

View File

@@ -0,0 +1,8 @@
#import "modules/std.sx";
pair :: () -> (i64, i64) { (1, 2) }
run :: () -> i32 {
i2, rest := pair(); // destructure name in an IMPORTED module
return 0;
}

View File

@@ -0,0 +1,30 @@
// A reserved/builtin type name used as a PARAMETER name is rejected inside the
// two method-with-body forms that carry their params as bare name lists rather
// than `Param` nodes: a protocol default-body method (`u8`) and a sx-defined
// runtime-class (`#objc_class`) method (`i16`). The declaration-site diagnostic
// underlines the OFFENDING PARAMETER itself, not the enclosing `protocol` /
// `#objc_class` block — each method's `param_name_spans` is threaded from the
// parser so the caret lands on the parameter token.
//
// Regression (issue 0076, attempt-5 span precision). Expected: one error per
// offending parameter, each caret on the parameter name; exit 1.
#import "modules/std.sx";
#import "modules/build.sx";
Greeter :: protocol {
greet :: (self: *Self, u8: i64) -> i64 {
return u8;
}
}
SxFoo :: #objc_class("SxFoo") {
counter: i32;
bump :: (self: *Self, i16: i32) {
self.counter += i16;
}
}
main :: () -> i32 {
return 0;
}

View File

@@ -0,0 +1,21 @@
// A module-global aggregate with a NULL pointer field is fine (null is a
// compile-time constant), but a sibling field initialized from a NON-constant
// expression (here a runtime function call) must still be rejected loudly. The
// presence of an accepted `null` must NOT widen the gate to admit the
// non-constant neighbor.
// Regression (issue 0081): the null-pointer fix must not regress the
// reject-loud behavior for genuinely non-constant initializers (issues
// 0072/0080). Expected: "global 'boxes' must be initialized by a compile-time
// constant"; exit 1.
#import "modules/std.sx";
runtime_marker :: () -> i64 { return 7; }
Box :: struct { p: *i64; marker: i64; }
boxes : [1]Box = .[ .{ p = null, marker = runtime_marker() } ];
main :: () -> i32 {
print("marker={}\n", boxes[0].marker);
return 0;
}

View File

@@ -0,0 +1,16 @@
// A module-global enum-literal initializer naming a variant that does not exist
// must be rejected loudly — never silently zero-initialized to the first tag.
// Regression (issue 0082): the enum-literal global serializer resolves the tag
// against the destination enum type; an unknown variant emits a diagnostic and
// fails the build instead of falling back to a null (zero-tag) initializer.
// Expected: "'.purple' is not a variant of enum 'Color'"; exit 1.
#import "modules/std.sx";
Color :: enum u8 { red; green; blue; }
bad : Color = .purple;
main :: () -> i32 {
print("{}\n", bad);
return 0;
}

View File

@@ -0,0 +1,26 @@
// A comptime `#run` global initializer that yields a function reference cannot
// be serialized to a static constant: at global-init time (Pass 0) functions
// are not yet declared, and the comptime serialization path has no later
// re-emit, so the func_ref can never resolve to a real function pointer. The
// compiler must reject this with a diagnostic AND a CLEAN non-zero exit — never
// print the error and then fall through into an undef initializer that crashes
// (pre-fix: the diagnostic printed, emission continued, and the JIT segfaulted
// calling through the undef pointer → exit 134).
// Regression (issue 0079 follow-up): every global-init serialization bail now
// routes through `failGlobalInit`, which sets the halt flag so the driver aborts
// after emit() instead of shipping the placeholder.
// Expected: "comptime init of 'fp' produced a reference to function 'add'…";
// exit 1, no segfault.
#import "modules/std.sx";
add :: (a: i32, b: i32) -> i32 { a + b }
pick :: () -> (i32, i32) -> i32 { return add; }
fp :: #run pick();
main :: () -> i32 {
print("{}\n", fp(3, 4));
return 0;
}

View File

@@ -0,0 +1,24 @@
// An array dimension that is not a compile-time integer constant is a hard
// error, not a silently-fabricated 0-length array. Here a type alias's
// dimension is a runtime function call (`get()`), which is genuinely not
// compile-time-known — the registration-time resolver cannot evaluate it.
//
// (A const-FOLDABLE expression dimension such as `[M + 1]` is NOT an error — it
// folds; see examples/0144-types-const-expr-array-dim.sx. Only a dimension with
// a genuinely runtime operand halts here.)
//
// Regression (issue 0083): the stateless resolver printed a non-fatal warning
// and fabricated length 0, then let compilation continue — producing a 0-byte
// alloca and corrupt element access. It now yields the `.unresolved` sentinel,
// which the alias registration surfaces as this diagnostic, aborting the build
// with a non-zero exit.
#import "modules/std.sx";
get :: () -> i64 { return 5; }
BadArr :: [get()]i64;
main :: () {
a : BadArr = ---;
a[0] = 7;
print("a0={}\n", a[0]);
}

View File

@@ -0,0 +1,15 @@
// An array dimension that folds to a valid compile-time integer but exceeds a
// `u32` (`[5_000_000_000]i64`) is a hard error — a clean sx diagnostic with a
// non-zero exit, NOT a compiler panic.
//
// Regression (issue 0087 / F0.4 attempt 6): the dimension folded to a valid i64
// (5e9) and was then narrowed with an unchecked `@intCast` to u32, aborting the
// COMPILER with "integer does not fit in destination type". Every dim/lane fold
// now narrows through the single range-checked `program_index.foldDimU32`, which
// reports an out-of-u32-range dimension as this diagnostic and halts the build.
#import "modules/std.sx";
main :: () {
a : [5000000000]i64 = ---;
print("unreachable: {}\n", a.len);
}

View File

@@ -0,0 +1,9 @@
// An atomic op on a non-scalar type is rejected with a clean diagnostic
// (not a raw LLVM verifier error). Stream A (atomics) guard.
#import "modules/std.sx";
#import "modules/std/atomic.sx";
main :: () {
arr : [8]u8 = .[0, 0, 0, 0, 0, 0, 0, 0];
x := atomic_load([8]u8, @arr, .seq_cst);
}

View File

@@ -0,0 +1,24 @@
// An array dimension that folds to a valid compile-time integer but exceeds a
// `u32` is a hard error — and it must report the SAME precise diagnostic whether
// the array is written directly (`a : [5_000_000_000]i64`, see example 1130) or
// behind a type ALIAS (`Big :: [5_000_000_000]i64`, here). Both forms now route
// the dimension through one shared folder + one shared message map, so they
// cannot diverge.
//
// Regression (issue 0083 / F0.4 attempt 7): the stateless alias-registration
// path collapsed `foldDimU32`'s distinct `.too_large` outcome into `null` and
// emitted ONE generic "an array dimension is not a compile-time integer
// constant" message — FALSE, since 5_000_000_000 IS a compile-time integer
// constant; it merely doesn't fit a `u32`. The alias path now consults the
// shared fold and emits the precise "does not fit in u32" message, matching the
// direct form. (A genuinely non-const alias dim still gets the generic message —
// see example 1129.)
#import "modules/std.sx";
Big :: [5000000000]i64;
main :: () {
a : Big = ---;
a[0] = 7;
print("unreachable: {}\n", a[0]);
}

View File

@@ -0,0 +1,9 @@
// An atomic load with an ordering LLVM forbids (.release / .acq_rel) is
// rejected with a clean diagnostic. Stream A (atomics) guard.
#import "modules/std.sx";
#import "modules/std/atomic.sx";
main :: () {
n : i64 = 0;
x := atomic_load(i64, @n, .release);
}

View File

@@ -0,0 +1,19 @@
// A NON-integral float constant (`4.5`) used as an array dimension is a hard
// error — only an integral float (`4.0`) folds to a count. Clean diagnostic +
// non-zero exit, NOT a fabricated length.
//
// The dimension follows the unified float→int narrowing rule (F0.11 / issue
// 0095): an integral float folds, a non-integral one is rejected as "not an
// integer" — the SAME `floatToIntExact` judgement the typed local/field/param/
// const sites use (the count path is the last of the five sites to unify).
//
// Regression (F0.4 attempt 8, Agra ruling): the integral-float rule accepts
// `4.0` as a dimension but must keep rejecting `4.5` (it is not an integer).
#import "modules/std.sx";
N : f64 : 4.5;
main :: () {
a : [N]i64 = ---;
print("unreachable: {}\n", a.len);
}

View File

@@ -0,0 +1,12 @@
// A NEGATIVE integral float (`-2.0`) used as an array dimension is a hard error.
// The integral-float rule folds and negates it to `-2`, then the shared u32 dim
// gate rejects a below-minimum dimension — a clean diagnostic + non-zero exit.
//
// Regression (F0.4 attempt 8, Agra ruling): integral floats fold, but a negative
// result is still rejected (a dimension must be non-negative).
#import "modules/std.sx";
main :: () {
a : [-2.0]i64 = ---;
print("unreachable: {}\n", a.len);
}

View File

@@ -0,0 +1,17 @@
// A generic value-param arg that does not fit the param's declared integer type
// (`Box(5_000_000_000)` for `$K: u32`) is a hard error — a clean diagnostic +
// non-zero exit, NOT a silent truncating bind.
//
// Regression (F0.4 attempt 8, item 1): `resolveValueParamArg` bound the folded
// i64 without range-checking the declared type, so an out-of-u32 arg compiled
// and ran. The bind now routes a `u32` count through the shared
// `program_index.foldDimU32` gate (the same one array dims / Vector lanes use),
// so an oversized value is rejected before instantiation.
#import "modules/std.sx";
Box :: struct ($K: u32) { value: i64; }
main :: () {
b : Box(5000000000) = ---;
print("unreachable\n");
}

View File

@@ -0,0 +1,23 @@
// A generic value-param arg that does not fit the param's declared integer type
// is a hard error even when that type is reached through a type ALIAS
// (`$K: Count` where `Count :: u32`, `$K: Small` where `Small :: i8`) — a clean
// diagnostic + non-zero exit, NOT a silent truncating bind.
//
// Regression (issue 0083): the value-param range gate matched only BUILTIN
// constraint names, so an aliased constraint slipped past `intTypeRange` and
// `Box(5_000_000_000)` with `$K: Count` compiled and bound a truncated value.
// The constraint now resolves to its underlying builtin (`Count` → u32,
// `Small` → i8) before range-checking, so an aliased integer constraint behaves
// exactly like the builtin it names — at both the struct and type-fn binders.
#import "modules/std.sx";
Count :: u32;
Small :: i8;
Box :: struct ($K: Count) { value: i64; }
Tiny :: struct ($K: Small) { value: i64; }
main :: () {
b : Box(5000000000) = ---;
t : Tiny(300) = ---;
print("unreachable {} {}\n", b.value, t.value);
}

View File

@@ -0,0 +1,22 @@
// A DIRECT array dimension that is genuinely not a compile-time integer (a
// runtime call) is a hard error — ONE clean diagnostic + non-zero exit. Crucially
// it must NOT fabricate a length and must NOT crash later in lowering: the bad
// var is used downstream (element store + read, `.len`), and lowering has to bail
// gracefully on the `.unresolved` type rather than `@panic` in `sizeOf` or pile
// on cascade errors.
//
// Regression (issue 0083): the stateful `resolveArrayLen` emitted the diagnostic
// then `return 0` — fabricating a 0-length array (0-byte alloca, OOB access) to
// dodge the `sizeOf` panic. It now returns null → the `.unresolved` sentinel; the
// binding's lowering bails on it (a field access on an already-diagnosed
// `.unresolved` value stays silent), so the single real diagnostic aborts the
// build with no fabrication and no panic.
#import "modules/std.sx";
get :: () -> i64 { return 5; }
main :: () {
a : [get()]i64 = ---;
a[0] = 7;
print("unreachable: {} {}\n", a.len, a[0]);
}

View File

@@ -0,0 +1,27 @@
// A FAILED value-param bind on a type-RETURNING FUNCTION must emit exactly its
// own diagnostic and NOT cascade a bogus `field '…' not found on '<fn>'` when the
// binding is later field-accessed. The type-fn binder must poison the binding to
// `.unresolved` (the diagnosed-poison sentinel) — exactly like the struct binder —
// so the downstream `.len` is suppressed, not reported as a second error.
//
// Regression (issue 0083): the type-fn path (`instantiateTypeFunction`) fell
// through to an empty-struct placeholder named after the function on a failed
// value-param bind, so `a.len` produced a second `field 'len' not found on
// 'MakeC'` error. The struct binder already returned `.unresolved` here; the
// type-fn binder now matches it. Three failure modes, three clean diagnostics,
// zero field cascades:
// - value-param overflow via an aliased integer constraint (`$K: Count`),
// - a non-const value-param arg (`get()`),
// - an unknown TYPE arg (`NoSuchType`) — must still report the unknown type.
#import "modules/std.sx";
Count :: u32;
MakeC :: ($K: Count, $T: Type) -> Type { return [K]T; }
get :: () -> u32 { return 4; }
main :: () {
a : MakeC(5000000000, i64) = ---;
b : MakeC(get(), i64) = ---;
c : MakeC(3, NoSuchType) = ---;
print("unreachable {} {} {}\n", a.len, b.len, c.len);
}

View File

@@ -0,0 +1,14 @@
// A NON-integral float (`4.5`) as an `inline for` range bound is a hard error:
// the loop cursor must be a compile-time integer, so only an integral float
// (`4.0`, `-2.0`) folds. Clean diagnostic + non-zero exit.
//
// Regression (F0.4 attempt 11, Agra ruling): range bounds are exempt from the
// count negative-rejection (negatives are valid endpoints), but the
// must-be-integer requirement still applies — `4.5` has no integer value.
#import "modules/std.sx";
main :: () {
s := 0;
inline for 0..4.5 (i) { s += i; }
print("unreachable: {}\n", s);
}

View File

@@ -0,0 +1,19 @@
// A reserved/builtin type-name spelling is rejected as the NAME of a `::`
// declaration too — both a constant (`i2 :: 5`) and a function
// (`u8 :: (…) {…}`). A function name and a const name are binding sites just
// like `i2 := …`; previously the `::` decl forms slipped past the
// reserved-name check, so a bare reserved-name function compiled silently and
// became callable — bypassing the backtick rule that handwritten sx must use.
// The backtick escape (`` `i2 :: … ``, examples/0153) is the only way to spell
// these names; `#import c` extern decls remain exempt (examples/1220).
//
// Regression (issue 0089). Expected: one error per declaration, each caret on
// the declared name; exit 1.
#import "modules/std.sx";
i2 :: 5;
u8 :: (n: i64) -> i64 { return n + 7; }
main :: () -> i32 {
return 0;
}

View File

@@ -0,0 +1,22 @@
// A reserved/builtin type-name spelling is rejected as the NAME of EVERY
// type-introducing `::` declaration too — struct, enum, union, error-set, and
// a typed constant — not just `:=` / value-const / function names (those are
// examples/1140). Each is a declaration-name binding site: a bare reserved
// spelling there mis-classifies and is rejected, exactly like `i2 := …`. The
// backtick escape (`` `i2 :: struct{…} ``, examples/0154) is the only way to
// spell these names in handwritten sx; `#import c` extern decls stay exempt
// (examples/1220).
//
// Regression (issue 0089 — attempt-4: 0076 holds across every decl kind).
// Expected: one error per declaration, each caret ON the declared name; exit 1.
#import "modules/std.sx";
i8 :: struct { v: i64; }
i16 :: enum { A; B; }
u16 :: union { a: i32; b: f32; }
u32 :: error { Bad, Empty }
i2 : i64 : 5;
main :: () -> i32 {
return 0;
}

View File

@@ -0,0 +1,20 @@
// A bare reserved/builtin type-name spelling is rejected as the NAME of a
// STRUCT-BODY constant too — both the untyped (`i2 :: 5`) and the typed
// (`u8 : i64 : 9`) forms — exactly like a top-level const (examples/1140) or a
// type decl (examples/1141). A struct member constant is a binding site, so a
// bare reserved spelling mis-classifies and is rejected; the caret lands ON the
// constant's name (not at 1:1). The backtick escape (examples/0156) is the only
// way to spell these names in handwritten sx.
//
// Regression (issue 0089 — attempt-5: 0076 holds for struct-body consts, with
// the caret on the name). Expected: one error per const, caret on the name; exit 1.
#import "modules/std.sx";
Holder :: struct {
i2 :: 5;
u8 : i64 : 9;
}
main :: () -> i32 {
return 0;
}

View File

@@ -0,0 +1,35 @@
// A typed module-level constant whose initializer does not match its
// annotation is a compile-time type error — not a silently-accepted const.
// Each declaration below pairs an initializer with an incompatible annotation,
// so the compiler must emit a `type mismatch` diagnostic at the initializer and
// abort (exit 1) rather than registering a usable const.
//
// Regression (issue 0088): `N : string : 4` was accepted; `print(N)` then
// segfaulted (an integer emitted as a `string` const → a bogus pointer) and
// `[N]i64` folded `N` to 4. The fix rejects the declaration at the root. The
// validation is type-based, so a const-EXPRESSION initializer (`E : string :
// M + 2`, `V : string : -M`) is rejected just like a literal — not skipped
// because its node kind isn't a literal (issue 0088, attempt 2).
//
// The mixed-numeric pair (`i64 : M + 0.5`, `i64 : 0.5 + M`) is rejected in BOTH
// operand orders: arithmetic binary-op inference now promotes int+float to the
// float result (`Lowering.arithResultType`), so an i64 annotation no longer
// matches a float-producing initializer regardless of which operand is the
// float (issue 0088, attempt 3 — the inferExprType promotion root fix).
#import "modules/std.sx";
M :: 2;
N : string : 4; // integer literal where a string is annotated
F : i64 : "x"; // string literal where an integer is annotated
B : i64 : true; // boolean literal where an integer is annotated
G : i64 : 1.5; // float literal where an integer is annotated
E : string : M + 2; // integer EXPRESSION where a string is annotated
V : string : -M; // integer (unary) expression where a string is annotated
BAD : i64 : M + 0.5; // mixed int+float (int LHS) → f64, rejected vs i64
BAD2 : i64 : 0.5 + M; // mixed float+int (float LHS) → f64, rejected vs i64 — order-independent
main :: () {
print("unreachable\n");
}

View File

@@ -0,0 +1,28 @@
// The 7 type-introspection builtins (size_of, align_of, field_count,
// type_name, type_eq, type_is_unsigned, is_flags) take ONLY types. A
// value argument is a compile-time error, not a silently-reinterpreted
// TypeId index.
//
// Regression (issue 0090, attempt 2): each builtin used to accept a
// non-type value and reinterpret it — `type_is_unsigned(6)` read 6 as a
// TypeId and returned the signedness of types[6] (`u8` → true);
// `size_of(true)` sized `typeof(true)` (8). The strict `$T: Type` guard
// (`Lowering.reflectionTypeArgGuard`) now rejects any argument whose
// static type is not `Type` and emits "<builtin> expects a type, got
// '<type>'" at the offending argument, aborting (exit 1).
//
// Reject cases use `true`/`1.5` (whose types — bool/f64 — are stable)
// rather than an integer literal, so the pinned diagnostics don't drift
// when the int-literal default type changes.
#import "modules/std.sx";
main :: () {
print("{}\n", size_of(true)); // bool, not a type
print("{}\n", align_of(1.5)); // f64, not a type
print("{}\n", field_count(true)); // bool, not a type
print("{}\n", type_name(1.5)); // f64, not a type
print("{}\n", type_eq(true, false)); // both bool — both rejected
print("{}\n", type_is_unsigned(true)); // bool, not a type
print("{}\n", is_flags(1.5)); // f64, not a type
}

View File

@@ -0,0 +1,29 @@
// Assigning to a field that does not exist on a struct produces the same
// `field 'X' not found on type 'Y'` diagnostic as the read path (1100), and
// exits 1 — never the `.unresolved` LLVM-emission panic, never a silent store
// into a neighbouring field.
//
// Regression (issue 0094): the lvalue field lookup left `field_ty = .unresolved`
// (lowerAssignment's assignment-target path) or silently GEP'd field 0 as `.i64`
// (lowerExprAsPtr's fallback / lowerMultiAssign's struct loop), so a missing-field
// store either built a pointer-to-`.unresolved` that panicked at LLVM emission or
// silently wrote field 0. All three lvalue sites now emit the field-not-found
// diagnostic: the assignment-target path (`p.q`), the nested lvalue-pointer path
// (`o.missing.a`), and the multi-target store path (`p.r, y`).
Point :: struct { x: i64; }
Inner :: struct { a: i64; }
Outer :: struct { inner: Inner; }
main :: () -> i32 {
p := Point.{ x = 1 };
p.q = 2; // site 1: lowerAssignment target path
o := Outer.{ inner = Inner.{ a = 1 } };
o.missing.a = 5; // site 2: lowerExprAsPtr fallback
y : i64 = 0;
p.r, y = 3, 4; // site 3: lowerMultiAssign field path
return 0;
}

View File

@@ -0,0 +1,50 @@
// Unified float→int narrowing rule (F0.11), NEGATIVE side: a NON-INTEGRAL float
// implicitly narrowing to an integer-typed binding is a COMPILE ERROR — not a
// silent truncation. The rule fires at a typed LOCAL initializer, a function
// PARAM default, a struct FIELD default, AND an array DIMENSION; each emits a
// narrowing diagnostic at the offending float and aborts (exit 1). It fires
// whether the float is a LITERAL (`1.5`), an INT-const-expression (`M + 0.5`,
// with `M :: 2`), a FLOAT-const-leaf expression (`F + 0.25`, with `F : f64 : 2.5`,
// = 2.75), a builtin FLOAT numeric-limit leaf inside an expression
// (`f64.true_min + 0.5` = 0.5), or a float `%` whose remainder is non-integral
// (`5.5 % 2.0` = 1.5) — all of these are the core of issue 0095, which previously
// slipped through and truncated. The fix is the integral-fold / non-integral-error
// rule shared across all five sites (local, field, param, const, and array
// dimension), applied to ANY compile-time-constant float expression (literal,
// int-const leaf, float-const leaf, numeric-limit leaf, `+ - * / %`, and
// combinations) — the compile-time float evaluator is at parity with the integer
// one, so no float leaf shape escapes. The array-dimension site phrases the same
// rejection as "must be an integer".
//
// The escape hatch stays open: `y : i64 = xx 1.5` (or `cast(i64) 1.5`)
// truncates with no error — exercised on the POSITIVE side (example 0168).
//
// Regression (issue 0095): `y : i64 = 1.5` silently truncated to 1,
// `y : i64 = M + 0.5` to 2, and `y : i64 = F + 0.25` (float-const leaf) to 2.
#import "modules/std.sx";
M :: 2; // int module const, for the INT-const-EXPRESSION cases
F : f64 : 2.5; // float module const, for the FLOAT-const-LEAF cases
Bad :: struct {
f : i64 = 3.5; // non-integral float LITERAL field default → error
fe : i64 = M + 0.5; // non-integral int-const-EXPR field default → error
ff : i64 = F + 0.25; // non-integral float-const-LEAF field default → error
}
badLit :: (x : i64 = 2.5) -> i64 { return x; } // non-integral LITERAL param default → error
badExpr :: (x : i64 = M + 0.5) -> i64 { return x; } // non-integral int-const-EXPR param default → error
badFlt :: (x : i64 = F + 0.25) -> i64 { return x; } // non-integral float-const-LEAF param default → error
main :: () {
y : i64 = 1.5; // non-integral float LITERAL local → error
ye : i64 = M + 0.5; // non-integral int-const-EXPRESSION local → error
yf : i64 = F + 0.25; // non-integral float-const-LEAF local → error
yn : i64 = f64.true_min + 0.5; // non-integral numeric-limit float expr → error
ym : i64 = 5.5 % 2.0; // non-integral float `%` remainder (1.5) → error
ad : [F + 0.25]i64 = ---; // non-integral float-const-LEAF array DIMENSION → error
b := Bad.{};
print("{} {} {}\n", b.f, b.fe, b.ff);
print("{} {} {}\n", badLit(), badExpr(), badFlt());
print("{} {} {} {} {} {}\n", y, ye, yf, yn, ym, ad.len);
}

View File

@@ -0,0 +1,46 @@
// Unified float→int narrowing rule (F0.11), float-DIVISION pin: a compile-time
// float division (`5.0 / 2.0` = 2.5) is a NON-INTEGRAL float, so narrowing it
// implicitly into an integer-typed binding is a COMPILE ERROR — exactly like any
// other non-integral float (example 1146). The division is the subtle case: its
// operands (`5.0`, `2.0`) are individually INTEGRAL, so a naive integer fold
// would truncate `5.0 / 2.0` to 2 with no diagnostic. The rule fires at all five
// sites — a typed module CONST, a struct FIELD default, a function PARAM default,
// a typed LOCAL, and an array DIMENSION — because the shared compile-time integer
// folder now refuses a division with a float operand, deferring it to the float
// evaluator + the unified rule (integral folds, non-integral errors). A float
// operand on either side (literal or float-typed const) makes the `/` a float
// division.
//
// The escape hatch stays open: `xx (5.0 / 2.0)` truncates to 2 with no error, and
// an INTEGRAL float division (`6.0 / 2.0` → 3) folds — both exercised on the
// POSITIVE side (example 0168).
//
// Regression (issue 0095, F0.11-6): `5.0 / 2.0` at a typed local, field default,
// param default, typed const, and array dimension all silently folded to 2 via
// integer truncating division; each now rejects the non-integral float.
#import "modules/std.sx";
// An UNTYPED float-EXPRESSION const carries a placeholder `i64` type, yet its
// value is float — `ME / 2` is still float division and must reject (judged by
// the const's VALUE, not its declared type), at BOTH the typed-binding path and
// the count path.
ME :: 4.0 + 1.0; // untyped float-EXPRESSION const (= 5.0)
// Typed CONST: declared but not referenced, so the single narrowing error is not
// followed by a downstream "unresolved const" cascade.
K : i64 : 5.0 / 2.0; // 2.5 non-integral float-DIVISION const → error
BadField :: struct {
f : i64 = 5.0 / 2.0; // non-integral float-DIVISION field default → error
}
badParam :: (x : i64 = 5.0 / 2.0) -> i64 { return x; } // float-DIVISION param default → error
main :: () {
local : i64 = 5.0 / 2.0; // non-integral float-DIVISION local → error
dim : [5.0 / 2.0]i64 = ---; // non-integral float-DIVISION array dimension → error
cdiv : i64 = ME / 2; // untyped float-EXPR const division (5.0/2 = 2.5) → error
cdim : [ME / 2]i64 = ---; // same, at the count path → error
b := BadField.{};
print("{} {} {} {} {} {}\n", local, b.f, badParam(), dim.len, cdiv, cdim.len);
}

View File

@@ -0,0 +1,29 @@
// A raw value binding whose spelling shadows a builtin INTEGER type name
// (`` `i8 ``) used as an array DIMENSION through one of its fields. Field
// access on a raw value is an ORDINARY runtime field read, so `` `i8.max `` is
// a runtime value — NOT the builtin `i8.max` (= 127) and NOT a compile-time
// constant. An array dimension demands a compile-time integer constant, so the
// dimension is rejected with the same diagnostic a plainly-named runtime field
// read (`b.max`) earns — the backtick spelling changes nothing.
//
// Sibling (integer) half of the F0.11-7 fix: the compile-time INTEGER evaluator
// (`evalConstIntExpr`) misclassified a raw value-shadow receiver as the builtin
// `<IntType>.min`/`.max` accessor, silently folding 127 and fabricating a
// 127-element array. The `is_raw` guard now defers it to an ordinary field
// read, so it surfaces as a non-constant dimension instead of a silent wrong
// length.
//
// Negative companion to 0169 (the FLOAT-field narrowing half, exit 0).
//
// Regression (issue 0095 / F0.11-7).
#import "modules/std.sx";
DimBox :: struct { max: i64; }
main :: () {
`i8 := DimBox.{ max = 3 };
// Raw value-shadow field read → a runtime value, not the builtin `i8.max`
// (127) and not a compile-time constant → rejected as a non-const dim.
arr : [`i8.max]f32 = ---;
print("len={}\n", arr.len);
}

View File

@@ -0,0 +1,9 @@
// The pre-multi-iterable `for xs: (x)` syntax (colon before the capture) is
// rejected with a migration hint.
#import "modules/std.sx";
main :: () {
xs : [2]i64 = .[1, 2];
for xs: (x) { }
}

View File

@@ -0,0 +1,9 @@
// A for capture group is positional: one capture per iterable (or none).
#import "modules/std.sx";
main :: () {
xs : [2]i64 = .[1, 2];
ys : [2]i64 = .[3, 4];
for xs, ys (x) { }
}

View File

@@ -0,0 +1,8 @@
// The FIRST iterable drives the loop, so it must be bounded; an open range
// `a..` may only follow it.
#import "modules/std.sx";
main :: () {
for 0.. (i) { }
}

View File

@@ -0,0 +1,8 @@
// A range with an explicit end marker (`..=` / `..<`) is the bounded form —
// it requires an end expression; the open form is `a..`.
#import "modules/std.sx";
main :: () {
for 0..= (i) { }
}

View File

@@ -0,0 +1,8 @@
// Range elements are synthesized values with no storage — `*` capture is
// rejected.
#import "modules/std.sx";
main :: () {
for 0..3 (*i) { }
}

View File

@@ -0,0 +1,11 @@
// In a for header the trailing paren group is the CAPTURE; a call iterable
// therefore needs one too. `()` cannot be a capture — parse error with the
// hint (`for f(n) (x) { }` / `for (f(n)) { }` / bind it first).
#import "modules/std.sx";
g :: () -> i64 { 1 }
main :: () {
for g() { }
}

View File

@@ -0,0 +1,12 @@
// The collision case of the positional capture rule: `for f(n) { }` reads
// `(n)` as the capture, making the iterable `f` itself — not iterable, with
// the parenthesize/add-capture hint.
#import "modules/std.sx";
f :: (n: i64) -> i64 { n }
main :: () {
n := 1;
for f(n) { }
}

View File

@@ -0,0 +1,12 @@
// An integer literal that does not fit its integer target type is a
// compile error (no silent wrap): both faces diagnosed in one run.
// Regression (issue 0112): `x : i8 = 300` bound 44, `y : u8 = 256` bound 0.
#import "modules/std.sx";
main :: () {
x : i8 = 300;
print("x: {}\n", x);
y : u8 = 256;
print("y: {}\n", y);
}

View File

@@ -0,0 +1,14 @@
// The catch error binding is parenthesized — `catch (e) { }`; a bare
// binding is rejected with a migration hint. (Same rule as the for-loop
// capture; `onfail (e) { }` follows it too.)
#import "modules/std.sx";
E :: error { Bad };
f :: () -> (i64, !E) { raise error.Bad; }
main :: () {
v := f() catch e { 0 };
print("{}\n", v);
}

View File

@@ -0,0 +1,11 @@
// An extensionless #import that matches BOTH a `.sx` file and a sibling
// directory of the same name is ambiguous — the compiler refuses to pick
// silently and asks for the explicit `.sx` spelling. `modules/std` is the
// canonical collision: `modules/std.sx` (the prelude) sits next to
// `modules/std/` (mem/fs/process/...).
#import "modules/std";
main :: () -> i32 {
0
}

View File

@@ -0,0 +1,12 @@
// An untyped array-literal constant infers its element type from the
// elements (ints -> i64; a numeric int/float mix promotes to f64). A
// NON-NUMERIC mix has no unified element type — diagnosed, with the
// annotation as the fix.
#import "modules/std.sx";
BAD :: .["alpha", 1];
main :: () {
print("{}\n", BAD[0]);
}

View File

@@ -0,0 +1,12 @@
// An array constant's elements must be compile-time constants — a call
// (or any runtime expression) in an element slot is diagnosed. `#run`
// is the tool for computed constants.
#import "modules/std.sx";
f :: () -> i64 { 7 }
BAD : [2]i64 : .[1, f()];
main :: () {
print("{}\n", BAD[0]);
}

View File

@@ -0,0 +1,10 @@
// A typed array constant's annotation dimension must match the
// initializer's element count.
#import "modules/std.sx";
BAD : [3]i64 : .[1, 2];
main :: () {
print("{}\n", BAD[0]);
}

View File

@@ -0,0 +1,19 @@
// Writes through a module constant are compile errors — for every target
// chain rooted at a const: a struct const's field (this used to compile
// and BUS-ERROR at runtime — issue 0116), an array const's element,
// a compound assignment, and a bare scalar const. A local that shadows
// the const name stays writable (see 0177/0178 for the value semantics).
#import "modules/std.sx";
Color :: struct { r, g, b: i64; }
WHITE :: Color.{ r = 255, g = 255, b = 255 };
K : [4]i64 : .[11, 22, 33, 44];
N : i64 : 4;
main :: () {
WHITE.r = 0;
K[0] = 5;
K[1] += 2;
N = 9;
}

View File

@@ -0,0 +1,12 @@
// A compile-time index into an array constant is bounds-checked at fold
// time — out of range is a diagnostic, never a wrap or a silent
// runtime read.
#import "modules/std.sx";
K : [4]i64 : .[11, 22, 33, 44];
main :: () {
b : [K[9]]u8 = ---;
print("{}\n", b.len);
}

View File

@@ -0,0 +1,31 @@
// `inline for` pack rejections: (1) a pack-element capture exposes only the
// constraint protocol's interface (same rule as `xs[i]`, example 0530);
// (2) a pack element cannot be captured by reference (`(*x)` — an element is
// an AST-substituted call arg, no storage); (3) a trailing pack shorter than
// the driving iterable; (4) a non-pack, non-range iterable.
#import "modules/std.sx";
Show :: protocol { show :: (self: *Self) -> string; }
IntBox :: struct { v: i64; }
impl Show for IntBox { show :: (self: *IntBox) -> string { int_to_string(self.v) } }
leak :: (..xs: Show) {
inline for xs (x) {
print("{}\n", x.v);
}
}
borrow :: (..xs: Show) {
inline for xs (*x) { }
}
short :: (..xs: Show) {
inline for 0..5, xs (i, x) { }
}
main :: () {
leak(IntBox.{ v = 5 });
borrow(IntBox.{ v = 5 });
short(IntBox.{ v = 1 }, IntBox.{ v = 2 });
arr := .[1, 2, 3];
inline for arr (x) { }
}

View File

@@ -0,0 +1,24 @@
// A `$T`-generic RETURN type with no parameter mentioning `$T` is rejected
// at the declaration: the fn isn't a template (type params derive from
// params), and no call site could ever bind the return. All three declare
// surfaces diagnose: a top-level fn, a struct-body method, and a
// (non-parameterised) impl method. Each used to PANIC the compiler at LLVM
// emission via the `.unresolved` tripwire — even when never called.
#import "modules/std.sx";
make :: () -> $T { 0 }
Foo :: struct {
x: i64;
weird :: (self: *Foo) -> $T { 0 }
}
Show2 :: protocol { show2 :: (self: *Self) -> string; }
IntBox :: struct { v: i64; }
impl Show2 for IntBox {
show2 :: (self: *IntBox) -> string { "x" }
leak :: (self: *IntBox) -> $T { 0 }
}
main :: () { print("ok\n"); }

View File

@@ -0,0 +1,11 @@
// A dot-call on a PLAIN free function (no `ufcs` marker, no alias) is
// rejected with a tailored help: direct call, pipe, or declare it ufcs.
#import "modules/std.sx";
bump :: (x: i64) -> i64 { x + 1 }
main :: () {
f : i64 = 40;
print("{}\n", f.bump());
}

View File

@@ -0,0 +1,26 @@
// Wrong argument counts to fixed-arity functions are rejected at the call
// site — bare calls, flat-imported stdlib fns, method dot-calls, and ufcs
// dot-calls — instead of reaching LLVM verification ("Incorrect number of
// arguments passed to called function!").
// Regression (issue 0123).
#import "modules/std.sx";
add2 :: (a: i64, b: i64) -> i64 { return a + b; }
Point :: struct {
x: i64;
scaled :: (self: Point, k: i64) -> i64 { return self.x * k; }
}
bump :: ufcs (p: Point, by: i64) -> i64 { return p.x + by; }
main :: () -> i32 {
_ = add2(1, 2, 3); // plain bare call, too many
_ = add2(1); // plain bare call, too few
_ = concat("a", "b", "c"); // flat-imported stdlib fn, too many
p := Point.{ x = 5 };
_ = p.scaled(2, 9); // method dot-call, too many
_ = p.bump(1, 2); // ufcs dot-call, too many
return 0;
}

View File

@@ -0,0 +1,19 @@
// A direct call to a generic fn whose arguments cannot bind a TYPE
// param diagnoses at the call site instead of monomorphizing with the
// param unbound. A `string` arg at a `[]$T` param is the canonical
// uninferrable shape (string deliberately does not bind `[]$T`); it
// used to stamp `.unresolved` through the body and PANIC the compiler
// at LLVM emission via the sentinel tripwire.
//
// Regression (issue 0126, diagnostic half).
#import "modules/std.sx";
first :: (xs: []$T) -> T {
return xs[0];
}
main :: () -> i32 {
print("{}\n", first("abc"));
return 0;
}

View File

@@ -0,0 +1,14 @@
// Enum literals against unusable destinations are DIAGNOSED, never a
// silent variant 0 (issue 0098's sibling holes): an unknown variant of a
// real enum, a non-enum destination type, and a destination-less literal
// each get their own error.
#import "modules/std.sx";
Platform :: enum u8 { ios; android_apk; }
main :: () -> i32 {
a : Platform = .nonexistent; // unknown variant: lists the real ones
b : i64 = .foo; // non-enum destination
print("{}{}\n", a, b);
return 0;
}

View File

@@ -0,0 +1,11 @@
// A destination-less enum literal is diagnosed (issue 0098's third hole —
// it previously panicked the LLVM backend with an unresolved type). Kept
// as the ONLY error in this file: the diagnostic is cascade-guarded, so it
// stays silent when the destination type itself already failed to resolve.
#import "modules/std.sx";
main :: () -> i32 {
c := .ios;
print("{}\n", c);
return 0;
}

View File

@@ -0,0 +1,10 @@
// `!` on an operand that has no truthiness (neither bool, integer, nor
// an error binding) is diagnosed instead of silently bit-flipped
// (issue 0129's diagnostic half).
#import "modules/std.sx";
main :: () -> i32 {
s := "text";
if !s { print("unreachable\n"); }
return 0;
}

View File

@@ -0,0 +1,15 @@
// One C symbol bound twice with DIFFERENT sx signatures is diagnosed
// (issue 0128): the first registration used to silently win, mis-typing
// every call through the second declaration. Equal signatures share one
// registration silently (see std's read/write bound by several modules).
#import "modules/std.sx";
libc :: #library "c";
// std/process.sx already binds getenv as `-> *u8`; this view disagrees.
getenv_opt :: (name: [:0]u8) -> ?[:0]u8 extern libc "getenv";
main :: () -> i32 {
p := getenv_opt("PATH");
if p == null { return 1; }
return 0;
}

View File

@@ -0,0 +1,13 @@
// cstring's coercion discipline (Odin-style): only a string LITERAL
// converts implicitly — an arbitrary string may be an unterminated view
// (use to_cstring); and cstring never converts to string implicitly —
// the length is an O(n) strlen the code must ask for (from_cstring).
#import "modules/std.sx";
main :: () -> i32 {
s := concat("a", "b");
c : cstring = s; // error: non-literal string -> cstring
t : string = c; // error: cstring -> string
print("{}{}\n", t, cstring_len(c));
return 0;
}

View File

@@ -0,0 +1,11 @@
// Phase 4 (FFI-linkage) interplay diagnostic: `extern` and `export` are the two
// values of the same linkage axis — a declaration is either an import (`extern`)
// or a definition (`export`), never both. The parser rejects the redundant
// second keyword with a clear message (instead of the bare "expected ';'" the
// body parser would otherwise emit).
//
// Expected: one error caret on the second keyword; exit 1.
f :: (a: i32) -> i32 extern export;
main :: () -> i32 { 0 }

View File

@@ -0,0 +1,13 @@
// A parse error in an IMPORTED file must be located in THAT file, not the
// importer. Regression: `import_sources` was wired to the diagnostics only
// AFTER import resolution finished, so a parse error raised MID-resolution
// (which aborts before that wiring) could not resolve the imported file's
// source — the caret fell back to the root file and landed on an unrelated
// line. The fix wires `import_sources` before resolving and pins the
// diagnostic's `source_file` + offset to the imported file.
//
// The companion's error sits several lines down (after comments) so a caret
// mislocated against THIS importer would be unmistakable.
#import "1176-diagnostics-import-parse-error-location/broken.sx";
main :: () {}

View File

@@ -0,0 +1,6 @@
// Deliberately broken: exercises import parse-error LOCATION reporting.
// These leading comment lines push the parse error down so its line number
// differs from the importer's — a mislocated caret would point here-or-wrong
// instead of at the real offending token below.
//
broken :: 1 2;

View File

@@ -0,0 +1,18 @@
// Taking the address of a scalar `::` constant is a compile error: a scalar
// constant folds to its value and has NO storage (only array/struct constants
// are immutable globals with a real address — see 0177). Covers a module-scope
// const, a local const, and an inline-asm `-> @const` write-through (the path
// that surfaced the bug). Before the fix, `@N` lowered to `inttoptr (i64 40 to
// ptr)` — a wild pointer that segfaulted on deref and emitted invalid stores
// for asm `-> @const`. Regression (issue 0138).
takes :: (p: *i64) {}
N :: 40;
main :: () {
takes(@N); // module scalar const — no storage
x :: 7;
takes(@x); // local scalar const — no storage
asm volatile { "mov %[c], #99", [c] "=r" -> @N }; // write-through to a const
}

View File

@@ -0,0 +1,16 @@
// Diagnostic: a type that contains ITSELF by value has no finite size and must
// be rejected loudly (not infinite-loop the size computation into a crash). A
// pointer payload (`*Tree`) would break the cycle and is the fix the message
// suggests. Covers both source decls and comptime-constructed types — this is
// the source form (regression for issue 0139).
#import "modules/std.sx";
Tree :: enum {
node: Tree; // by-VALUE self-reference → infinitely sized
leaf;
}
main :: () -> i32 {
t : Tree = .leaf;
return 0;
}

View File

@@ -0,0 +1,30 @@
// A comptime type construction (declare/define, reflection) that leaves a type
// INCOMPLETE must surface a build-gating DIAGNOSTIC naming the reason — not
// poison the decl to `.unresolved` silently and let that crash at LLVM emission
// or hide behind a downstream cascade. Here `declare("Undefined")` mints a
// forward nominal slot that is NEVER completed by a matching `define(handle, …)`;
// the compiler rejects the incomplete type at its construction site (exit 1, no
// panic).
//
// NOTE: an EXPLICITLY-defined empty type (empty struct/tuple/enum/tagged_union)
// is VALID — see examples/0641. The remaining rejection is purely the
// never-defined `declare` placeholder, which would otherwise panic codegen
// (`verifySizes`: llvm_size != ir_size on an unsized forward slot).
//
// Regression (issue 0140): before the fix this panicked with "unresolved type
// reached LLVM emission" (exit 134), because the interp's bail detail was
// dropped (`catch return null`) and `.unresolved` reached codegen unannounced.
#import "modules/std.sx";
#import "modules/std/meta.sx";
// Declared but never `define`d — an incomplete forward slot.
mk_undefined :: () -> Type {
return declare("Undefined");
}
Undefined :: mk_undefined();
main :: () -> i32 {
u : Undefined = ---;
return 0;
}

View File

@@ -0,0 +1,17 @@
// A comptime-constructed enum (declare/define) with two same-named variants is
// rejected loudly. Two `value` variants would make construction (`.value`) and
// matching ambiguous — `define` bails naming the duplicate, instead of silently
// minting a malformed enum that picks one arbitrarily.
#import "modules/std.sx";
#import "modules/std/meta.sx";
Bad :: define(declare("Bad"), .enum(.{ variants = .[
EnumVariant.{ name = "value", payload = i64 },
EnumVariant.{ name = "closed", payload = void },
EnumVariant.{ name = "value", payload = f64 }, // duplicate name
] }));
main :: () -> i32 {
b : Bad = ---;
return 0;
}

View File

@@ -0,0 +1,14 @@
// A `declare("X")` that is never completed by `define(handle, info)` is rejected
// loudly. `declare` mints an empty (undefined) nominal slot; without a matching
// `define` it has zero variants, which is never a usable type — sizing /
// constructing it would otherwise panic at codegen. The diagnostic names the
// type and points at the bare `declare` so the missing `define` is obvious.
#import "modules/std.sx";
#import "modules/std/meta.sx";
Undef :: declare("Undef"); // never define()d
main :: () -> i32 {
x : Undef = ---;
return 0;
}

View File

@@ -0,0 +1,27 @@
// Diagnostic: a comptime-CONSTRUCTED enum (declare/define) that contains itself
// BY VALUE is infinitely sized and rejected loudly — the same `checkInfiniteSize`
// guard that covers source decls (examples/1178) also covers minted types. A
// pointer payload (`*L`) breaks the cycle and is the fix the message suggests
// (see examples/0618 for the working recursive `*List`).
//
// This is the constructed-type companion to 1178, and pins the "use-before-
// define by value" corner of the metatype validation story: referencing a
// declared slot by VALUE in its own definition is the one self-reference shape
// that isn't legal (a `*L` pointer needs no layout, so it is).
#import "modules/std.sx";
#import "modules/std/meta.sx";
make :: () -> Type {
h := declare("L");
return define(h, .enum(.{ variants = .[
EnumVariant.{ name = "cons", payload = L }, // by VALUE, not *L
EnumVariant.{ name = "nil", payload = void },
] }));
}
L :: make();
main :: () -> i32 {
x : L = .nil;
return 0;
}

View File

@@ -0,0 +1,24 @@
// A many-pointer `[*]T` carries NO length, so it cannot coerce to a slice `[]T`
// implicitly — doing so would pass a bare 8-byte pointer where a 16-byte
// `{ptr,len}` fat pointer is expected, silently corrupting the callee's view of
// the data (garbage length, mis-aligned element reads). The compiler rejects it
// loudly and tells the user to supply the length via `ptr[0..len]`.
//
// Regression (issue 0141): this silent mis-coercion segfaulted the comptime VM
// and failed LLVM verification at runtime; it now produces a clean diagnostic.
#import "modules/std.sx";
sum :: (s: []i64) -> i64 {
total := 0;
for s (x) { total += x; }
return total;
}
main :: () -> i32 {
xs : List(i64) = .{};
xs.append(10);
xs.append(20);
r := sum(xs.items); // [*]i64 → []i64 — needs xs.items[0..xs.len]
print("{}\n", r);
return 0;
}

View File

@@ -0,0 +1,10 @@
// Diagnostic: a `fn abi(.compiler)` whose name is NOT on the compiler
// library's function-export list is a build error — the export list is the
// safety boundary, so an unbound name can't silently fall through to dlsym.
#import "modules/std.sx";
not_a_real_compiler_fn :: (x: i64) -> i64 abi(.compiler);
main :: () { print("unreached\n"); }

View File

@@ -0,0 +1,16 @@
// Diagnostic: a welded `compiler`-library function is comptime-only — it has no
// runtime symbol (the comptime interpreter dispatches it to a Zig handler).
// Calling one from runtime code is a build error with a clear message, NOT an
// undefined-symbol link failure. (A comptime use — inside `#run` or a `::` —
// is fine; see examples/0626.)
#import "modules/std.sx";
StringId :: u32;
intern :: (s: string) -> StringId abi(.compiler);
main :: () {
id := intern("called at runtime");
print("{}\n", id);
}

View File

@@ -0,0 +1,12 @@
// Atomic compare-exchange dual-ordering validation: the FAILURE ordering may not
// be stronger than the SUCCESS ordering (LLVM rule). Here failure=.seq_cst (rank
// 3) is stronger than success=.relaxed (rank 0) → loud diagnostic, not invalid IR.
// Calls the intrinsic directly so the diagnostic span is stable (user file, not
// the lib forward site). Stream A (atomics) A.2.
#import "modules/std.sx";
#import "modules/std/atomic.sx";
main :: () {
n : i64 = 0;
_ := atomic_cmpxchg(i64, @n, 0, 1, .relaxed, .seq_cst);
}

View File

@@ -0,0 +1,8 @@
// A fence with .relaxed ordering is rejected (LLVM has no monotonic/unordered
// fence). Stream A (atomics) guard.
#import "modules/std.sx";
#import "modules/std/atomic.sx";
main :: () {
atomic_fence(.relaxed);
}

View File

@@ -0,0 +1,12 @@
// `sx run` on a program with no `main` must emit a clean diagnostic and exit
// non-zero — never call into a garbage JIT address and segfault. A pre-JIT
// entry-point check in main.zig (plus a defensive `main_addr == 0` backstop in
// target.zig's runJITFromObject) replaces the old silent garbage-pointer call.
//
// Regression (issue 0137).
#import "modules/std.sx";
// Intentionally no `main` — only a helper.
greet :: () {
print("unreachable\n");
}

View File

@@ -0,0 +1,16 @@
// A bodiless `#builtin` carrying a `$T: Type` parameter, whose name the
// compiler does not recognize, must fail LOUDLY with an "unknown #builtin"
// diagnostic — not silently evaluate to 0 (the CLAUDE.md silent-fallback
// pattern). The generic monomorphization path (monomorphizeFunction's
// builtin-body branch) now diagnoses an unresolved builtin name instead of
// falling through to `ensureTerminator`'s `constInt(0)`.
//
// Regression (issue 0144).
#import "modules/std.sx";
// `mystery` is not a recognized builtin.
mystery :: ($T: Type, x: T) -> T #builtin;
main :: () {
print("mystery(42) = {}\n", mystery(i64, 42));
}

View File

@@ -0,0 +1,12 @@
// A protocol method that omits the explicit receiver (the old implicit form)
// is now a parse error — the receiver `self: *Self`/`self: Self` is required as
// the first parameter. This guards the diagnostic.
#import "modules/std.sx";
Show :: protocol {
show :: () -> string; // ERROR: missing `self` receiver
}
main :: () -> i32 {
return 0;
}

View File

@@ -0,0 +1,5 @@
error: field 'bogus' not found on type 'Vec'
--> examples/diagnostics/1100-diagnostics-err-field-not-found.sx:8:15
|
8 | return xx v.bogus;
| ^^^^^^^

View File

@@ -0,0 +1,5 @@
error: field '42' not found on type 'tuple'
--> examples/diagnostics/1101-diagnostics-err-tuple-oob.sx:6:15
|
6 | return xx t.42;
| ^^^^

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