fix: type-safe stores + Any unbox/eq; finish multi-return deferrals

Type-checking gaps (segfault/corruption → compile errors):

- 0197: reject a store into an annotated slot whose value has no modeled
  coercion AND a different byte width (a 16-byte string into a 4-byte i32
  overran the slot and segfaulted). New checkAssignable / noneReinterpretIsUnsafe
  (coerce.zig, width via the LLVM-accurate typeSizeBytes) wired into every store
  site: var/const-decl, single + multi assignment (identifier/field/index/
  element/deref), named-return defaults. Same-width reinterpretations (*T→[*]T,
  i64→isize, fn-ref) and explicit xx/cast stay allowed; cascades suppressed via
  externalErrorsExist. Examples 1205, 1206.
- 0198: an implicit `Any → T` unbox is now a compile error (it blindly
  reinterpreted the boxed payload — silent garbage for a wrong scalar, a segfault
  for an aggregate). xx and compiler-generated match/pack unboxes are unaffected.
  Example 1207.
- 0199: `Any == <concrete>` (one operand Any) aborted the LLVM verifier — the
  comparison arm now fires when either operand is Any, boxing the concrete side
  first. Example 0654.

Multi-return deferrals (PLAN-MULTIRET #6 + named-order + D3 + generic):

- Reorder named return elements by name instead of requiring slot order; error on
  unknown/duplicate/missing (value-only AND full-failable-tuple forms). Examples
  0210, 0214.
- Reject a bare-paren (A, B) multi-return signature in generic-arg position
  (return-position-only). Example 0215.
- Multi-return closure types / lambda literals work via the reused tuple
  machinery (destructure, single-bind+field, lambda arg). Example 0216.
- Generic multi-return: positional works (0217); 0200: the named-slot
  implicit-return form now works for generic free fns + struct methods —
  monomorphizeFunction now calls bindNamedReturnSlots. Example 0218.

readme.md documents the annotated-store coercion rule; CHECKPOINT-MULTIRET.md
updated. Full corpus green (850/0).
This commit is contained in:
agra
2026-06-27 17:28:27 +03:00
parent 97772abf54
commit b322dcfe61
51 changed files with 1000 additions and 56 deletions

View File

@@ -0,0 +1,27 @@
// Comparing an `Any` against a concrete value (a MIXED `Any == <concrete>`, in
// either operand order) compares the boxed value words — the same value-identity
// the both-`Any` comparison uses. Boxing the concrete side first keeps the
// operands shape-compatible.
//
// Regression (issue 0199): a mixed `Any == <concrete>` fell through to a plain
// `icmp` on a 16-byte `{tag, value}` aggregate vs a scalar, aborting the LLVM
// verifier ("Both operands to ICmp are not of the same type"). The both-`Any`
// form already worked; this extends it to one-sided `Any` comparisons.
#import "modules/std.sx";
main :: () -> i64 {
x : Any = 5;
print("{}\n", x == 5); // true
print("{}\n", x == 6); // false
print("{}\n", x != 6); // true
print("{}\n", 5 == x); // true (concrete on the left)
b : Any = true;
print("{}\n", b == true); // true
print("{}\n", b == false); // false
y : Any = 5;
print("{}\n", x == y); // true (both Any — unchanged)
return 0;
}

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,7 @@
true
false
true
true
true
false
true

View File

@@ -0,0 +1,27 @@
// Initializing (or reassigning) an explicitly-annotated slot with a value
// whose type has NO coercion to the annotation is a type error, diagnosed at
// lowering with a located message.
//
// Regression (issue 0197): `x : i32 = "hi"` was accepted with no diagnostic —
// the incompatible value passed through a `.none` coercion plan UNCHANGED, so a
// 16-byte `string` was stored into a 4-byte `i32` slot, bit-mangling the slot
// and SIGSEGV'ing at run time (`sx ir` lowered fine; only the run crashed). The
// guard (`checkAssignable`) now rejects an un-coercible initializer at every
// store-into-annotated-slot site — var-decl, body-local const-decl, and
// reassignment — emitting a diagnostic and aborting the build cleanly (exit 1).
//
// The explicit `xx` / `cast(T)` escape hatch is unaffected: a deliberate
// reinterpretation (pointer↔int, etc.) still passes through.
#import "modules/std.sx";
main :: () -> i64 {
x : i32 = "hi"; // error: cannot initialize 'x' (string ↛ i32)
y : i32 = 0;
y = "nope"; // error: cannot reassign 'y' (string ↛ i32)
C : i32 : "also"; // error: cannot initialize 'C' (string ↛ i32)
return 0;
}

View File

@@ -0,0 +1,34 @@
// A store into ANY annotated slot whose value type has no coercion to the slot
// AND a different byte width is a type error — the raw `.none` passthrough would
// overrun the slot and corrupt adjacent memory (issue 0197). The guard covers
// every store site, not just plain var-decls: a struct field, an array element,
// a pointer deref, and a multi-assignment target.
//
// Regression (issue 0197): `struct{a:i32; b:i32}` is 8 bytes, but a 16-byte
// `string` stored raw into one of its fields (or into an i32 array element, or
// through an `*i32`) overran the slot and SIGSEGV'd / clobbered neighbors. The
// discriminator is BYTE WIDTH (via `typeSizeBytes`), so a same-width
// reinterpretation (`*T → [*]T`, a bare fn-ref into a function slot) still
// passes — only a genuine width mismatch is rejected.
#import "modules/std.sx";
S :: struct { a: i32; b: i32; }
main :: () -> i64 {
s : S = ---;
s.a = 1; s.b = 2;
s.a = "field"; // error: struct field, string ↛ i32
arr := i32.[1, 2, 3];
arr[0] = "elem"; // error: array element, string ↛ i32
n : i32 = 0;
p : *i32 = @n;
p.* = "deref"; // error: pointer deref, string ↛ i32
u : i32 = 0; v : i32 = 0;
u, v = "multi", 9; // error: multi-assign target, string ↛ i32
return 0;
}

View File

@@ -0,0 +1,22 @@
// An `Any` does not IMPLICITLY unbox to a concrete type. A blind unbox
// reinterprets the boxed payload word as the target with NO runtime tag check,
// so a wrong target silently yields garbage (a scalar) or dereferences the
// payload as a pointer and segfaults (an aggregate). sx rejects the implicit
// unbox at compile time — like the no-implicit-optional-unwrap rule — and
// directs the user to `match` on the value's type or an explicit `xx`.
//
// Regression (issue 0198): `s : S = some_any` segfaulted and `f : f64 = some_any`
// silently produced 0.0; both are now compile errors. The fix is in `coerceMode`
// (`.unbox_any` arm, mode == .implicit). The `xx` escape hatch and the
// compiler-generated type-dispatch / pack-extraction unboxes are unaffected.
#import "modules/std.sx";
S :: struct { a: i64; }
main :: () -> i64 {
x : Any = 5;
n : i64 = x; // error: 'Any' does not implicitly unbox to 'i64'
s : S = x; // error: 'Any' does not implicitly unbox to 'S'
return 0;
}

View File

@@ -0,0 +1,17 @@
error: cannot initialize 'x' of type 'i32' with a value of type 'string'
--> examples/diagnostics/1205-diagnostics-annotated-init-type-mismatch.sx:19:15
|
19 | x : i32 = "hi"; // error: cannot initialize 'x' (string ↛ i32)
| ^^^^
error: cannot reassign 'y' of type 'i32' with a value of type 'string'
--> examples/diagnostics/1205-diagnostics-annotated-init-type-mismatch.sx:22:9
|
22 | y = "nope"; // error: cannot reassign 'y' (string ↛ i32)
| ^^^^^^
error: cannot initialize 'C' of type 'i32' with a value of type 'string'
--> examples/diagnostics/1205-diagnostics-annotated-init-type-mismatch.sx:24:15
|
24 | C : i32 : "also"; // error: cannot initialize 'C' (string ↛ i32)
| ^^^^^^

View File

@@ -0,0 +1,23 @@
error: cannot assign 'a' of type 'i32' with a value of type 'string'
--> examples/diagnostics/1206-diagnostics-store-width-mismatch.sx:21:11
|
21 | s.a = "field"; // error: struct field, string ↛ i32
| ^^^^^^^
error: cannot assign 'element' of type 'i64' with a value of type 'string'
--> examples/diagnostics/1206-diagnostics-store-width-mismatch.sx:24:14
|
24 | arr[0] = "elem"; // error: array element, string ↛ i32
| ^^^^^^
error: cannot assign 'target' of type 'i32' with a value of type 'string'
--> examples/diagnostics/1206-diagnostics-store-width-mismatch.sx:28:11
|
28 | p.* = "deref"; // error: pointer deref, string ↛ i32
| ^^^^^^^
error: cannot assign 'u' of type 'i32' with a value of type 'string'
--> examples/diagnostics/1206-diagnostics-store-width-mismatch.sx:31:12
|
31 | u, v = "multi", 9; // error: multi-assign target, string ↛ i32
| ^^^^^^^

View File

@@ -0,0 +1,11 @@
error: an 'Any' does not implicitly unbox to 'i64': the boxed type is not checked, so a wrong target reinterprets the payload (a wrong scalar silently yields garbage; an aggregate dereferences it and crashes). Dispatch on the value's type with `match`, or force it with `xx` if you know the boxed type.
--> examples/diagnostics/1207-diagnostics-any-implicit-unbox-rejected.sx:19:5
|
19 | n : i64 = x; // error: 'Any' does not implicitly unbox to 'i64'
| ^^^^^^^^^^^^
error: an 'Any' does not implicitly unbox to 'S': the boxed type is not checked, so a wrong target reinterprets the payload (a wrong scalar silently yields garbage; an aggregate dereferences it and crashes). Dispatch on the value's type with `match`, or force it with `xx` if you know the boxed type.
--> examples/diagnostics/1207-diagnostics-any-implicit-unbox-rejected.sx:20:5
|
20 | s : S = x; // error: 'Any' does not implicitly unbox to 'S'
| ^^^^^^^^^^

View File

@@ -1,8 +1,19 @@
// Negative: named return elements must be given in SLOT ORDER. A mismatched
// name would otherwise be matched positionally and silently produce the wrong
// result, so it is rejected. (Here `b` is given where slot `a` is expected.)
// Named return elements may be given in ANY order — they are matched to the
// return slots BY NAME and permuted to slot order before lowering. Here `b` is
// given before `a`; the result still destructures as (a, b).
#import "modules/std.sx";
pair :: (n: i32) -> (a: i32, b: i32) {
return b = n, a = n + 1; // out of slot order
return b = n, a = n + 1; // out of slot order — reordered by name
}
// Works through the value-carrying-failable channel too (error slot is implicit).
ErrX :: error { Bad }
fpair :: (n: i32) -> (a: i32, b: i32, !) {
return b = n, a = n + 1;
}
main :: () -> i64 {
x, y := pair(5); // a = 6, b = 5
print("{} {}\n", x, y);
r := fpair(10) catch { return 9; };
print("{} {}\n", r.a, r.b); // a = 11, b = 10
return 0;
}
main :: () -> i64 { x, y := pair(5); print("{} {}\n", x, y); return 0; }

View File

@@ -0,0 +1,17 @@
// Negative: named return elements are matched to slots BY NAME (any order), so
// a name that matches NO slot, or a slot named MORE THAN ONCE, is a hard error
// (rather than a silent positional mismatch). Missing/extra arity is caught
// separately. Here `c` names no slot and `a` would be duplicated.
#import "modules/std.sx";
bad_unknown :: (n: i32) -> (a: i32, b: i32) {
return a = n, c = n + 1; // error: 'c' names no return slot
}
bad_dup :: (n: i32) -> (a: i32, b: i32) {
return a = n, a = n + 1; // error: 'a' given more than once
}
main :: () -> i64 {
x, y := bad_unknown(5);
p, q := bad_dup(5);
print("{} {} {} {}\n", x, y, p, q);
return 0;
}

View File

@@ -0,0 +1,11 @@
// Negative: a bare-paren `(A, B)` is a MULTI-RETURN signature — valid ONLY as a
// function/closure return type, never as a value type. As a generic type
// argument it is rejected (a tuple-valued argument uses `Tuple(A, B)`). This
// completes the return-position-only gating (param / field / variable positions
// were already rejected; 0213 covers those).
#import "modules/std.sx";
main :: () -> i64 {
xs : List((i32, bool)) = ---; // error: multi-return signature, not a type
return 0;
}

View File

@@ -0,0 +1,26 @@
// Multi-return CLOSURE types and lambda literals work via the same tuple
// machinery as function multi-returns (D3): a `Closure() -> (A, B)` value's call
// result destructures (`a, b := cb()`), single-binds with field access
// (`c := cb(); c.0`), and a `() => { return v1, v2; }` lambda literal satisfies a
// multi-return closure parameter. No dedicated ClosureInfo marker is needed —
// the return slots ride as the reused `.tuple` TypeId, consistent with the
// function-decl multi-return surface.
#import "modules/std.sx";
apply :: (cb: Closure() -> (i32, bool)) -> i32 {
a, b := cb(); // destructure a multi-return closure result
return if b { a } else { 0 };
}
main :: () -> i64 {
cb : Closure() -> (i32, bool) = () => { return 7, true; };
x, y := cb();
print("{} {}\n", x, y); // 7 true
c := cb(); // single-bind + positional field access
print("{} {}\n", c.0, c.1); // 7 true
r := apply(() => { return 9, true; }); // lambda literal as the closure arg
print("{}\n", r); // 9
return 0;
}

View File

@@ -0,0 +1,24 @@
// Generic multi-return: a positional multi-return whose slots are generic type
// params resolves with the inferred (or explicit) bindings — `-> (T, U)` with
// `a: $T, b: $U` infers from the args; an explicit `$T: Type` form also works.
// (The NAMED-slot implicit-return form with generics is a separate gap — issue
// 0200; the positional explicit-`return` form here is the supported surface.)
#import "modules/std.sx";
// inferred type params from the value args
pair :: (a: $T, b: $U) -> (T, U) { return a, b; }
// explicit comptime type params
mk :: ($T: Type, $U: Type, a: T, b: U) -> (T, U) { return a, b; }
main :: () -> i64 {
x, y := pair(7, true);
print("{} {}\n", x, y); // 7 true
s, n := pair("hi", 42);
print("{} {}\n", s, n); // hi 42
p, q := mk(i32, bool, 3, false);
print("{} {}\n", p, q); // 3 false
return 0;
}

View File

@@ -0,0 +1,40 @@
// A NAMED multi-return whose slots are generic type params, using the implicit
// return (assign the named slot locals, no explicit `return`), works for both a
// generic free function and a generic struct method.
//
// Regression (issue 0200): the generic monomorph path (`monomorphizeFunction`)
// bound params but never called `bindNamedReturnSlots`, so `named_return_names`
// stayed null and the implicit-return synthesis didn't fire — the body wrongly
// reported "produces no value". Now the binder runs on the generic path too
// (mirroring `lowerFunctionBodyInto`), incl. with defaults and the failable
// error channel.
#import "modules/std.sx";
ErrX :: error { Bad }
// generic free function, named slots, implicit return, mixed inference + default
split :: (a: $T, b: $U) -> (first: T, second: U) { first = a; second = b; }
withd :: (a: $T) -> (x: T, y: i32 = 99) { x = a; }
fallible :: (a: $T, b: $U) -> (x: T, y: U, !) { x = a; y = b; }
// generic struct method, named slots, implicit return
Box :: struct ($T: Type) {
v: T;
pair :: (self: *Box(T)) -> (a: T, b: T) { a = self.v; b = self.v + 1; }
}
main :: () -> i64 {
x, y := split(7, true);
print("{} {}\n", x, y); // 7 true
p, q := withd(5);
print("{} {}\n", p, q); // 5 99
r := fallible(3, false) catch { return 9; };
print("{} {}\n", r.x, r.y); // 3 false
bx := Box(i32).{ v = 10 };
m, n := bx.pair();
print("{} {}\n", m, n); // 10 11
return 0;
}

View File

@@ -1,11 +1 @@
error: named return element 'b' does not match the slot 'a' at position 0 — name the elements in slot order
--> examples/types/0210-types-multi-return-name-order.sx:6:5
|
6 | return b = n, a = n + 1; // out of slot order
| ^^^^^^^^^^^^^^^^^^^^^^^^
error: named return element 'a' does not match the slot 'b' at position 1 — name the elements in slot order
--> examples/types/0210-types-multi-return-name-order.sx:6:5
|
6 | return b = n, a = n + 1; // out of slot order
| ^^^^^^^^^^^^^^^^^^^^^^^^

View File

@@ -0,0 +1,11 @@
error: named return element 'c' does not name any return slot
--> examples/types/0214-types-multi-return-name-invalid.sx:7:5
|
7 | return a = n, c = n + 1; // error: 'c' names no return slot
| ^^^^^^^^^^^^^^^^^^^^^^^^
error: named return element 'a' is given more than once
--> examples/types/0214-types-multi-return-name-invalid.sx:10:5
|
10 | return a = n, a = n + 1; // error: 'a' given more than once
| ^^^^^^^^^^^^^^^^^^^^^^^^

View File

@@ -0,0 +1,5 @@
error: a bare-paren `(A, B)` is a multi-return signature, valid only as a return type; a tuple-valued generic type argument uses `Tuple(…)`
--> examples/types/0215-types-multi-return-as-generic-arg.sx:9:15
|
9 | xs : List((i32, bool)) = ---; // error: multi-return signature, not a type
| ^^^^^^^^^^^

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,3 @@
7 true
7 true
9

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,3 @@
7 true
hi 42
3 false

View File

@@ -0,0 +1,4 @@
7 true
5 99
3 false
10 11