fix: gate implicit optional unwrap on flow narrowing (issue 0179)
Optional (?T) operands were implicitly unwrapped without proof of
presence, silently miscompiling a NULL ?T to garbage. Unwraps in
binary ops and other expression positions are now gated on flow
narrowing: a ?T value is only auto-unwrapped where control flow has
established it is non-null (the narrowed_refs set). Outside a narrowed
region, an implicit unwrap is rejected rather than producing garbage.
Touches the lowering pipeline (lower.zig + lower/{call,closure,coerce,
comptime,control_flow,expr,ffi,generic,pack,stmt}.zig). Adds optionals
examples 0919-0923 and closures example 0312 covering flow narrowing,
binop narrowing, no-implicit-unwrap rejection, and no closure leak of
narrowed state. Updates specs.md and readme.md.
This commit is contained in:
@@ -0,0 +1,33 @@
|
|||||||
|
// Calling a closure VALUE whose parameter is `?T` coerces the argument to the
|
||||||
|
// param type, just like a call to a top-level function does: a concrete arg
|
||||||
|
// wraps to a present optional, and `null` becomes an absent optional.
|
||||||
|
//
|
||||||
|
// Regression (issue 0186): the closure-value call path lowered args without
|
||||||
|
// coercing to the closure's declared param types, so a concrete `7` arrived as
|
||||||
|
// a bare payload (read ABSENT) and `null` reached a `{T,i1}` slot as a bare
|
||||||
|
// pointer (LLVM verifier failure). Fixed by typing args against the closure's
|
||||||
|
// params (`resolveCallParamTypes`) AND coercing them at the call site
|
||||||
|
// (`coerceClosureCallArgs`).
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
main :: () {
|
||||||
|
pick := (p: ?i64) -> i64 => {
|
||||||
|
if p == null { return -1; }
|
||||||
|
return p; // narrowed inside the lambda body
|
||||||
|
};
|
||||||
|
print("pick 7: {}\n", pick(7)); // 7 (concrete arg wraps present)
|
||||||
|
print("pick null: {}\n", pick(null)); // -1 (null arg → absent)
|
||||||
|
|
||||||
|
// also via a closure stored in a struct field
|
||||||
|
Holder :: struct { f: Closure(?i64) -> i64; }
|
||||||
|
h := Holder.{ f = pick };
|
||||||
|
print("h 5: {}\n", h.f(5)); // 5
|
||||||
|
print("h null: {}\n", h.f(null)); // -1
|
||||||
|
|
||||||
|
// and via a plain function-pointer VALUE (same coercion contract)
|
||||||
|
fp : (?i64) -> i64 = target;
|
||||||
|
print("fp 8: {}\n", fp(8)); // 8
|
||||||
|
print("fp null: {}\n", fp(null)); // -1
|
||||||
|
}
|
||||||
|
|
||||||
|
target :: (p: ?i64) -> i64 { if p == null { return -1; } return p; }
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
pick 7: 7
|
||||||
|
pick null: -1
|
||||||
|
h 5: 5
|
||||||
|
h null: -1
|
||||||
|
fp 8: 8
|
||||||
|
fp null: -1
|
||||||
54
examples/optionals/0919-optionals-flow-narrowing.sx
Normal file
54
examples/optionals/0919-optionals-flow-narrowing.sx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// Flow-sensitive narrowing: a `?T` proven present by a `!= null` guard
|
||||||
|
// converts to its concrete payload `T` in a value position (call arg,
|
||||||
|
// `return`, arithmetic). Covers the if-then form, the divergent `== null`
|
||||||
|
// guard form, compound `and` / `or`, the `else` arm, and reassignment killing
|
||||||
|
// narrowing (the value must be re-narrowed before reuse).
|
||||||
|
//
|
||||||
|
// Regression (issue 0179): an un-narrowed `?T → concrete` used to unwrap
|
||||||
|
// unconditionally — yielding the zero payload of a null optional with no
|
||||||
|
// diagnostic. Narrowing is now the ONLY implicit path; everything else is a
|
||||||
|
// compile error (see 0920).
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
takes_i32 :: (x: i32) { print("i32 {}\n", x); }
|
||||||
|
add :: (a: i64, b: i64) -> i64 { return a + b; }
|
||||||
|
|
||||||
|
// Divergent `== null` guard narrows the rest of the body.
|
||||||
|
guard :: (v: ?i64) -> i64 {
|
||||||
|
if v == null { return -1; }
|
||||||
|
return v; // v narrowed to i64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compound `or` guard narrows both names afterward.
|
||||||
|
guard2 :: (a: ?i64, b: ?i64) -> i64 {
|
||||||
|
if a == null or b == null { return 0; }
|
||||||
|
return a + b; // both narrowed
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: () {
|
||||||
|
// if-then narrowing
|
||||||
|
n : ?i64 = 41;
|
||||||
|
if n != null { takes_i32(n); } // i32 41
|
||||||
|
|
||||||
|
// else-branch narrowing (false ⇒ present)
|
||||||
|
m : ?i64 = 7;
|
||||||
|
if m == null { print("none\n"); } else { takes_i32(m); } // i32 7
|
||||||
|
|
||||||
|
// compound `and` narrows both inside the then-branch
|
||||||
|
a : ?i64 = 3;
|
||||||
|
b : ?i64 = 4;
|
||||||
|
if a != null and b != null { print("sum {}\n", add(a, b)); } // sum 7
|
||||||
|
|
||||||
|
print("guard 9: {}\n", guard(9)); // 9
|
||||||
|
print("guard null: {}\n", guard(null)); // -1
|
||||||
|
print("guard2: {}\n", guard2(5, 6)); // 11
|
||||||
|
print("guard2 null: {}\n", guard2(5, null)); // 0
|
||||||
|
|
||||||
|
// reassignment kills narrowing; re-narrow before reuse
|
||||||
|
k : ?i64 = 100;
|
||||||
|
if k != null {
|
||||||
|
takes_i32(k); // i32 100
|
||||||
|
k = 200; // narrowing killed here
|
||||||
|
if k != null { takes_i32(k); } // i32 200 (re-narrowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
16
examples/optionals/0920-optionals-no-implicit-unwrap.sx
Normal file
16
examples/optionals/0920-optionals-no-implicit-unwrap.sx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// An un-narrowed `?T` does NOT implicitly unwrap to its concrete payload in a
|
||||||
|
// value position — it is a compile error pointing at the explicit forms.
|
||||||
|
//
|
||||||
|
// Regression (issue 0179): passing a `?i64` where `i32` is expected used to
|
||||||
|
// compile and unconditionally extract the payload, so a NULL optional yielded
|
||||||
|
// `0` with no diagnostic (silent miscompile across the whole `?T → concrete`
|
||||||
|
// family, incl. `?bool → bool`). The legal extractions are `!` / `??` /
|
||||||
|
// binding / `case` / a `!= null` guard (see 0919); everything else is rejected.
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
takes_i32 :: (x: i32) { print("got {}\n", x); }
|
||||||
|
|
||||||
|
main :: () {
|
||||||
|
n : ?i64 = null;
|
||||||
|
takes_i32(n); // error: optional does not implicitly unwrap
|
||||||
|
}
|
||||||
29
examples/optionals/0921-optionals-binop-narrowing.sx
Normal file
29
examples/optionals/0921-optionals-binop-narrowing.sx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// Flow narrowing extends to binary-op OPERANDS: a `?T` proven present by a
|
||||||
|
// `!= null` guard reads as its concrete payload in arithmetic / comparison.
|
||||||
|
//
|
||||||
|
// Regression (issue 0185): an un-narrowed `?T` operand used to unwrap
|
||||||
|
// unconditionally, so `null + 10` silently yielded `10` (the zero payload).
|
||||||
|
// Narrowing is now the only implicit operand unwrap; everything else is a
|
||||||
|
// compile error (see 0922).
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
sum :: (a: ?i64, b: ?i64) -> i64 {
|
||||||
|
if a == null or b == null { return -1; }
|
||||||
|
return a + b; // both operands narrowed
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: () {
|
||||||
|
x : ?i64 = 30;
|
||||||
|
y : ?i64 = 12;
|
||||||
|
if x != null and y != null {
|
||||||
|
print("add {}\n", x + y); // 42
|
||||||
|
print("lt {}\n", x < y); // false
|
||||||
|
}
|
||||||
|
print("sum 11: {}\n", sum(5, 6)); // 11
|
||||||
|
print("sum null: {}\n", sum(5, null)); // -1
|
||||||
|
|
||||||
|
// guard form narrows for the rest of the scope
|
||||||
|
n : ?i64 = 9;
|
||||||
|
if n == null { return; }
|
||||||
|
print("times {}\n", n * 2); // 18
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// An un-narrowed `?T` used directly as a binary-op operand is a compile error
|
||||||
|
// — it does not implicitly unwrap.
|
||||||
|
//
|
||||||
|
// Regression (issue 0185): `null + 10` used to compile and unconditionally
|
||||||
|
// extract the payload, silently yielding `10` (0 + 10) with no diagnostic.
|
||||||
|
// The legal forms are `!` / `??` / a `!= null` guard (see 0921).
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
main :: () {
|
||||||
|
a : ?i64 = null;
|
||||||
|
b : i64 = 10;
|
||||||
|
c := a + b; // error: optional operand does not implicitly unwrap
|
||||||
|
print("c = {}\n", c);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// Flow narrowing (issue 0179) does NOT cross into a nested function body. A
|
||||||
|
// closure capturing a local that is narrowed in the ENCLOSING scope still sees
|
||||||
|
// it as `?T` inside the closure — using it unguarded is a compile error.
|
||||||
|
//
|
||||||
|
// This guards the soundness fix from the adversarial review of 0179/0185: the
|
||||||
|
// narrowing gate is keyed by per-function SSA `Ref`, and a closure body's `Ref`
|
||||||
|
// space overlaps the enclosing function's, so without isolation an outer
|
||||||
|
// narrowed `Ref` would falsely permit unwrapping a not-proven-present optional
|
||||||
|
// inside the closure (`Lowering.NarrowGuard`). The closure could run later when
|
||||||
|
// the captured value is null, so the reject is mandatory — write `n!` to assert.
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
takes_i64 :: (x: i64) { print("{}\n", x); }
|
||||||
|
|
||||||
|
main :: () {
|
||||||
|
n : ?i64 = 7;
|
||||||
|
if n != null {
|
||||||
|
// `n` is narrowed HERE, but the closure body is a separate function:
|
||||||
|
// `n` is `?i64` inside it, so this implicit unwrap must be rejected.
|
||||||
|
g := () => { takes_i64(n); };
|
||||||
|
g();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
i32 41
|
||||||
|
i32 7
|
||||||
|
sum 7
|
||||||
|
guard 9: 9
|
||||||
|
guard null: -1
|
||||||
|
guard2: 11
|
||||||
|
guard2 null: 0
|
||||||
|
i32 100
|
||||||
|
i32 200
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
error: cannot use a value of type '?i64' where 'i32' is expected: an optional does not implicitly unwrap; force-unwrap with '!', supply a fallback with '?? <default>', bind it (`if v := ...`), or guard with '!= null'
|
||||||
|
--> examples/optionals/0920-optionals-no-implicit-unwrap.sx:15:5
|
||||||
|
|
|
||||||
|
15 | takes_i32(n); // error: optional does not implicitly unwrap
|
||||||
|
| ^^^^^^^^^^^^
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
add 42
|
||||||
|
lt false
|
||||||
|
sum 11: 11
|
||||||
|
sum null: -1
|
||||||
|
times 18
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
error: cannot use a value of type '?i64' as an operand: an optional does not implicitly unwrap; force-unwrap with '!', supply a fallback with '?? <default>', or guard with '!= null'
|
||||||
|
--> examples/optionals/0922-optionals-binop-no-implicit-unwrap.sx:12:10
|
||||||
|
|
|
||||||
|
12 | c := a + b; // error: optional operand does not implicitly unwrap
|
||||||
|
| ^
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
error: cannot use a value of type '?i64' where 'i64' is expected: an optional does not implicitly unwrap; force-unwrap with '!', supply a fallback with '?? <default>', bind it (`if v := ...`), or guard with '!= null'
|
||||||
|
--> examples/optionals/0923-optionals-narrowing-no-closure-leak.sx:20:22
|
||||||
|
|
|
||||||
|
20 | g := () => { takes_i64(n); };
|
||||||
|
| ^^^^^^^^^^^^
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -1,5 +1,26 @@
|
|||||||
# 0179 — implicit `?T → <concrete>` unwrap silently miscompiles a NULL optional to garbage (whole family)
|
# 0179 — implicit `?T → <concrete>` unwrap silently miscompiles a NULL optional to garbage (whole family)
|
||||||
|
|
||||||
|
> **RESOLVED.** Root cause: `coerceMode`'s `.optional_unwrap` arm
|
||||||
|
> (`src/ir/lower/coerce.zig`) unwrapped any `?T → concrete` UNCONDITIONALLY,
|
||||||
|
> never reading the has_value flag — a null optional yielded its zero payload
|
||||||
|
> with no diagnostic. Fix took path (a) from the investigation prompt: **real
|
||||||
|
> flow-sensitive narrowing**. `?T → concrete` is now REJECTED at the coercion
|
||||||
|
> site (loud diagnostic listing `!` / `??` / binding / `!= null`) UNLESS the
|
||||||
|
> source is a local proven present by flow narrowing. Narrowing is tracked by
|
||||||
|
> name (`Lowering.narrowed`, region-scoped by `lowerBlock` / the if-then arm /
|
||||||
|
> a divergent `== null` guard / the `else` arm, killed on reassignment) and
|
||||||
|
> bridged to `coerceMode` via `narrowed_refs` (the loaded `Ref` of a narrowed
|
||||||
|
> identifier, tagged in `lowerIdentifier`). Closes the `?bool → bool` hole too
|
||||||
|
> (issue 0169's carve-out). specs.md §Optional Types + readme updated.
|
||||||
|
> Regressions: `examples/optionals/0919-optionals-flow-narrowing.sx` (narrowing
|
||||||
|
> works) + `examples/optionals/0920-optionals-no-implicit-unwrap.sx` (rejection);
|
||||||
|
> `0900-optionals-optionals.sx` now exercises genuine narrowing.
|
||||||
|
>
|
||||||
|
> **Out of scope / follow-up:** the binary-op operand auto-unwrap
|
||||||
|
> (`src/ir/lower/expr.zig` ~line 3211) is a SEPARATE silent-unwrap path that
|
||||||
|
> does NOT route through `classify`/`coerceMode` — `a + b` with a null `?T`
|
||||||
|
> still yields garbage with no diagnostic. Filed separately as issue 0185.
|
||||||
|
|
||||||
## Symptom
|
## Symptom
|
||||||
|
|
||||||
Passing an optional `?T` where a concrete `T`/other builtin is expected (function
|
Passing an optional `?T` where a concrete `T`/other builtin is expected (function
|
||||||
|
|||||||
11
readme.md
11
readme.md
@@ -295,8 +295,19 @@ if v := x { // safe unwrap
|
|||||||
// Optional chaining
|
// Optional chaining
|
||||||
node: ?Node = get_node();
|
node: ?Node = get_node();
|
||||||
name := node?.name ?? "unknown";
|
name := node?.name ?? "unknown";
|
||||||
|
|
||||||
|
// Flow-sensitive narrowing: a `!= null` guard proves the value present, so it
|
||||||
|
// reads as its concrete payload in the guarded region.
|
||||||
|
n: ?i32 = maybe();
|
||||||
|
if n != null { take_i32(n); } // `n` is i32 here
|
||||||
|
if n == null { return; }
|
||||||
|
take_i32(n); // narrowed for the rest of the scope
|
||||||
```
|
```
|
||||||
|
|
||||||
|
A `?T` never *implicitly* unwraps to `T` in a value position — a bare
|
||||||
|
`take_i32(n)` without a guard, `!`, `??`, or binding is a compile error (it
|
||||||
|
would otherwise silently yield a null optional's zero payload).
|
||||||
|
|
||||||
### Generics
|
### Generics
|
||||||
|
|
||||||
```sx
|
```sx
|
||||||
|
|||||||
8
specs.md
8
specs.md
@@ -1122,6 +1122,14 @@ wrap :: (n: i32) -> ?i32 {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The reverse is **not** implicit: a `?T` never silently unwraps to its payload
|
||||||
|
`T` in a value position (call argument, field initializer, `return`, assignment).
|
||||||
|
Such a use is a compile error — extract the payload explicitly with `!` / `??` /
|
||||||
|
a binding (`if v := opt`) / a `case` match, or rely on flow-sensitive narrowing
|
||||||
|
after a `!= null` guard (below). Unwrapping a null optional implicitly would
|
||||||
|
yield its zero payload with no diagnostic, so the conversion is rejected rather
|
||||||
|
than allowed.
|
||||||
|
|
||||||
#### Force Unwrap (`!`)
|
#### Force Unwrap (`!`)
|
||||||
Extracts the payload, traps at runtime if null:
|
Extracts the payload, traps at runtime if null:
|
||||||
```sx
|
```sx
|
||||||
|
|||||||
@@ -284,6 +284,19 @@ pub const Lowering = struct {
|
|||||||
current_runtime_method: ?ast.RuntimeMethodDecl = null, // the specific method whose body is being lowered; `super.<same_name>(...)` reuses its signature
|
current_runtime_method: ?ast.RuntimeMethodDecl = null, // the specific method whose body is being lowered; `super.<same_name>(...)` reuses its signature
|
||||||
type_bindings: ?std.StringHashMap(TypeId) = null, // generic type param bindings ($T → concrete TypeId)
|
type_bindings: ?std.StringHashMap(TypeId) = null, // generic type param bindings ($T → concrete TypeId)
|
||||||
current_match_tags: ?[]const u64 = null, // type tags for current match arm (for runtime dispatch)
|
current_match_tags: ?[]const u64 = null, // type tags for current match arm (for runtime dispatch)
|
||||||
|
/// Flow-sensitive narrowing (issue 0179). The set of local variable names
|
||||||
|
/// currently PROVEN present (`?T` known to carry a value) by a `!= null`
|
||||||
|
/// guard / branch. Region-scoped: `lowerBlock` snapshots+restores it, the
|
||||||
|
/// if-then branch narrows on `!= null`, a divergent `== null` guard narrows
|
||||||
|
/// the rest of the enclosing block, and an assignment kills the name's
|
||||||
|
/// narrowing. Consulted at the implicit `?T → concrete` unwrap (`coerceMode`):
|
||||||
|
/// a non-narrowed unwrap is REJECTED instead of silently yielding the zero
|
||||||
|
/// payload of a null optional.
|
||||||
|
narrowed: std.StringHashMap(void) = undefined,
|
||||||
|
/// The SSA `Ref`s produced by lowering a narrowed identifier — the bridge
|
||||||
|
/// from name-keyed narrowing to the Ref-keyed `coerceMode` unwrap gate.
|
||||||
|
/// Cleared per function body (the `Ref` space is per-function).
|
||||||
|
narrowed_refs: std.AutoHashMap(Ref, void) = undefined,
|
||||||
force_block_value: bool = false, // set by lowerBlockValue to extract if-else values
|
force_block_value: bool = false, // set by lowerBlockValue to extract if-else values
|
||||||
block_terminated: bool = false, // set when constant-folded if emits a return/br into current block
|
block_terminated: bool = false, // set when constant-folded if emits a return/br into current block
|
||||||
in_lambda_body: bool = false, // true while lowering a closure-literal body; sharpens the `raise`-not-failable diagnostic (ERR E5.1: tell the user to annotate `-> (T, !)`)
|
in_lambda_body: bool = false, // true while lowering a closure-literal body; sharpens the `raise`-not-failable diagnostic (ERR E5.1: tell the user to annotate `-> (T, !)`)
|
||||||
@@ -474,6 +487,8 @@ pub const Lowering = struct {
|
|||||||
pack_param_count: ?std.StringHashMap(u32),
|
pack_param_count: ?std.StringHashMap(u32),
|
||||||
pack_arg_types: ?std.StringHashMap([]const TypeId),
|
pack_arg_types: ?std.StringHashMap([]const TypeId),
|
||||||
inline_return_target: ?InlineReturnInfo,
|
inline_return_target: ?InlineReturnInfo,
|
||||||
|
narrowed: std.StringHashMap(void),
|
||||||
|
narrowed_refs: std.AutoHashMap(Ref, void),
|
||||||
|
|
||||||
pub fn enter(l: *Lowering) FnBodyReentry {
|
pub fn enter(l: *Lowering) FnBodyReentry {
|
||||||
const g = FnBodyReentry{
|
const g = FnBodyReentry{
|
||||||
@@ -491,7 +506,14 @@ pub const Lowering = struct {
|
|||||||
.pack_param_count = l.pack_param_count,
|
.pack_param_count = l.pack_param_count,
|
||||||
.pack_arg_types = l.pack_arg_types,
|
.pack_arg_types = l.pack_arg_types,
|
||||||
.inline_return_target = l.inline_return_target,
|
.inline_return_target = l.inline_return_target,
|
||||||
|
// Flow narrowing is lexical to one function body — a nested
|
||||||
|
// (closure / local-fn) lowering starts with a fresh, empty
|
||||||
|
// narrowing state and the outer state is restored after.
|
||||||
|
.narrowed = l.narrowed,
|
||||||
|
.narrowed_refs = l.narrowed_refs,
|
||||||
};
|
};
|
||||||
|
l.narrowed = std.StringHashMap(void).init(l.alloc);
|
||||||
|
l.narrowed_refs = std.AutoHashMap(Ref, void).init(l.alloc);
|
||||||
// The `#jni_env` Ref stack is lexical to ONE function's instruction
|
// The `#jni_env` Ref stack is lexical to ONE function's instruction
|
||||||
// stream; move the visible base to the current top. Pack-fn mono
|
// stream; move the visible base to the current top. Pack-fn mono
|
||||||
// state is likewise lexical to the pack-fn body — null it so a
|
// state is likewise lexical to the pack-fn body — null it so a
|
||||||
@@ -523,6 +545,38 @@ pub const Lowering = struct {
|
|||||||
l.pack_param_count = g.pack_param_count;
|
l.pack_param_count = g.pack_param_count;
|
||||||
l.pack_arg_types = g.pack_arg_types;
|
l.pack_arg_types = g.pack_arg_types;
|
||||||
l.inline_return_target = g.inline_return_target;
|
l.inline_return_target = g.inline_return_target;
|
||||||
|
l.narrowed.deinit();
|
||||||
|
l.narrowed = g.narrowed;
|
||||||
|
l.narrowed_refs.deinit();
|
||||||
|
l.narrowed_refs = g.narrowed_refs;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Save + clear + restore JUST the flow-narrowing state (issue 0179) around
|
||||||
|
/// a nested body lowering that does NOT go through `FnBodyReentry` —
|
||||||
|
/// closure literals, generic/pack/comptime monomorphization. Each lowers a
|
||||||
|
/// SEPARATE function whose `Ref` space (reset by `beginFunction`) OVERLAPS
|
||||||
|
/// the outer function's, so the outer `narrowed_refs` indices would falsely
|
||||||
|
/// match the nested body's `Ref`s and permit an UNSOUND unwrap of a
|
||||||
|
/// non-present optional. Clearing on entry isolates the nested body (it
|
||||||
|
/// builds its own narrowing from scratch); restore re-arms the outer.
|
||||||
|
pub const NarrowGuard = struct {
|
||||||
|
l: *Lowering,
|
||||||
|
narrowed: std.StringHashMap(void),
|
||||||
|
narrowed_refs: std.AutoHashMap(Ref, void),
|
||||||
|
|
||||||
|
pub fn enter(l: *Lowering) NarrowGuard {
|
||||||
|
const g = NarrowGuard{ .l = l, .narrowed = l.narrowed, .narrowed_refs = l.narrowed_refs };
|
||||||
|
l.narrowed = std.StringHashMap(void).init(l.alloc);
|
||||||
|
l.narrowed_refs = std.AutoHashMap(Ref, void).init(l.alloc);
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn restore(g: NarrowGuard) void {
|
||||||
|
g.l.narrowed.deinit();
|
||||||
|
g.l.narrowed = g.narrowed;
|
||||||
|
g.l.narrowed_refs.deinit();
|
||||||
|
g.l.narrowed_refs = g.narrowed_refs;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -548,6 +602,8 @@ pub const Lowering = struct {
|
|||||||
.struct_const_map = std.StringHashMap(StructConstInfo).init(module.alloc),
|
.struct_const_map = std.StringHashMap(StructConstInfo).init(module.alloc),
|
||||||
.extern_name_map = std.StringHashMap([]const u8).init(module.alloc),
|
.extern_name_map = std.StringHashMap([]const u8).init(module.alloc),
|
||||||
.comptime_constants = std.StringHashMap(ComptimeValue).init(module.alloc),
|
.comptime_constants = std.StringHashMap(ComptimeValue).init(module.alloc),
|
||||||
|
.narrowed = std.StringHashMap(void).init(module.alloc),
|
||||||
|
.narrowed_refs = std.AutoHashMap(Ref, void).init(module.alloc),
|
||||||
.xx_reentrancy = std.AutoHashMap(u64, void).init(module.alloc),
|
.xx_reentrancy = std.AutoHashMap(u64, void).init(module.alloc),
|
||||||
.inferred_error_sets = std.StringHashMap([]const u32).init(module.alloc),
|
.inferred_error_sets = std.StringHashMap([]const u32).init(module.alloc),
|
||||||
.impl_method_names = std.StringHashMap(void).init(module.alloc),
|
.impl_method_names = std.StringHashMap(void).init(module.alloc),
|
||||||
@@ -1685,6 +1741,13 @@ pub const Lowering = struct {
|
|||||||
|
|
||||||
// --- moved to lower/control_flow.zig (lower_control_flow) ---
|
// --- moved to lower/control_flow.zig (lower_control_flow) ---
|
||||||
pub const lowerIfExpr = lower_control_flow.lowerIfExpr;
|
pub const lowerIfExpr = lower_control_flow.lowerIfExpr;
|
||||||
|
pub const narrowableLocal = lower_control_flow.narrowableLocal;
|
||||||
|
pub const nullCmpName = lower_control_flow.nullCmpName;
|
||||||
|
pub const collectPresentIfTrue = lower_control_flow.collectPresentIfTrue;
|
||||||
|
pub const collectPresentIfFalse = lower_control_flow.collectPresentIfFalse;
|
||||||
|
pub const narrowSnapshot = lower_control_flow.narrowSnapshot;
|
||||||
|
pub const narrowRestore = lower_control_flow.narrowRestore;
|
||||||
|
pub const applyNarrowing = lower_control_flow.applyNarrowing;
|
||||||
pub const tryConstBoolCondition = lower_control_flow.tryConstBoolCondition;
|
pub const tryConstBoolCondition = lower_control_flow.tryConstBoolCondition;
|
||||||
pub const lowerWhile = lower_control_flow.lowerWhile;
|
pub const lowerWhile = lower_control_flow.lowerWhile;
|
||||||
pub const listView = lower_control_flow.listView;
|
pub const listView = lower_control_flow.listView;
|
||||||
@@ -2008,6 +2071,7 @@ pub const Lowering = struct {
|
|||||||
pub const lowerTupleLiteral = lower_expr.lowerTupleLiteral;
|
pub const lowerTupleLiteral = lower_expr.lowerTupleLiteral;
|
||||||
pub const lowerDerefExpr = lower_expr.lowerDerefExpr;
|
pub const lowerDerefExpr = lower_expr.lowerDerefExpr;
|
||||||
pub const lowerForceUnwrap = lower_expr.lowerForceUnwrap;
|
pub const lowerForceUnwrap = lower_expr.lowerForceUnwrap;
|
||||||
|
pub const diagOptionalOperand = lower_expr.diagOptionalOperand;
|
||||||
pub const lowerNullCoalesce = lower_expr.lowerNullCoalesce;
|
pub const lowerNullCoalesce = lower_expr.lowerNullCoalesce;
|
||||||
pub const resolveOptionalInner = lower_expr.resolveOptionalInner;
|
pub const resolveOptionalInner = lower_expr.resolveOptionalInner;
|
||||||
pub const lowerExpr = lower_expr.lowerExpr;
|
pub const lowerExpr = lower_expr.lowerExpr;
|
||||||
|
|||||||
@@ -563,6 +563,9 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
|
|||||||
const ty_info = self.module.types.get(binding.ty);
|
const ty_info = self.module.types.get(binding.ty);
|
||||||
if (ty_info == .closure) {
|
if (ty_info == .closure) {
|
||||||
const callee_ref = if (binding.is_alloca) self.builder.load(binding.ref, binding.ty) else binding.ref;
|
const callee_ref = if (binding.is_alloca) self.builder.load(binding.ref, binding.ty) else binding.ref;
|
||||||
|
// Coerce user args to the closure's param types
|
||||||
|
// (issue 0186) — a `?T` param must wrap the arg.
|
||||||
|
coerceClosureCallArgs(self, args.items, ty_info.closure.params);
|
||||||
// Closure trampolines carry `__sx_ctx` at
|
// Closure trampolines carry `__sx_ctx` at
|
||||||
// slot 0; emit_llvm's `call_closure` builds
|
// slot 0; emit_llvm's `call_closure` builds
|
||||||
// the call as [ctx, env, user_args], so we
|
// the call as [ctx, env, user_args], so we
|
||||||
@@ -656,6 +659,17 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
|
|||||||
const bti = self.module.types.get(binding.ty);
|
const bti = self.module.types.get(binding.ty);
|
||||||
break :blk if (bti == .function) bti.function.ret else .i64;
|
break :blk if (bti == .function) bti.function.ret else .i64;
|
||||||
} else .i64;
|
} else .i64;
|
||||||
|
// Coerce user args to the fn-pointer's param types (issue
|
||||||
|
// 0186) — same as the closure-value and global-fn-pointer
|
||||||
|
// paths. The arg loop already applied implicit address-of
|
||||||
|
// for `*T` params (resolveCallParamTypes now surfaces the
|
||||||
|
// `.function` param types), so this completes value
|
||||||
|
// coercions like a `?T` wrap. Without it a concrete arg to a
|
||||||
|
// `?T` fn-ptr param reaches `call_indirect` unconverted.
|
||||||
|
if (!binding.ty.isBuiltin()) {
|
||||||
|
const bti = self.module.types.get(binding.ty);
|
||||||
|
if (bti == .function) coerceClosureCallArgs(self, args.items, bti.function.params);
|
||||||
|
}
|
||||||
var final_args = std.ArrayList(Ref).empty;
|
var final_args = std.ArrayList(Ref).empty;
|
||||||
defer final_args.deinit(self.alloc);
|
defer final_args.deinit(self.alloc);
|
||||||
if (self.fnPtrTypeWantsCtx(binding.ty)) {
|
if (self.fnPtrTypeWantsCtx(binding.ty)) {
|
||||||
@@ -965,6 +979,8 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
|
|||||||
agg = self.builder.load(obj, oi.pointer.pointee);
|
agg = self.builder.load(obj, oi.pointer.pointee);
|
||||||
}
|
}
|
||||||
const closure_val = self.builder.structGet(agg, @intCast(fi), f.ty);
|
const closure_val = self.builder.structGet(agg, @intCast(fi), f.ty);
|
||||||
|
// Coerce user args to the closure's param types (issue 0186).
|
||||||
|
coerceClosureCallArgs(self, args.items, fti.closure.params);
|
||||||
// Prepend ctx for sx-side closure call ABI.
|
// Prepend ctx for sx-side closure call ABI.
|
||||||
const owned = if (self.implicit_ctx_enabled) blk: {
|
const owned = if (self.implicit_ctx_enabled) blk: {
|
||||||
const arr = self.alloc.alloc(Ref, args.items.len + 1) catch unreachable;
|
const arr = self.alloc.alloc(Ref, args.items.len + 1) catch unreachable;
|
||||||
@@ -1368,6 +1384,8 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
|
|||||||
const cti = self.module.types.get(callee_ty);
|
const cti = self.module.types.get(callee_ty);
|
||||||
if (cti == .closure) {
|
if (cti == .closure) {
|
||||||
const callee_ref = self.lowerExpr(c.callee);
|
const callee_ref = self.lowerExpr(c.callee);
|
||||||
|
// Coerce user args to the closure's param types (issue 0186).
|
||||||
|
coerceClosureCallArgs(self, args.items, cti.closure.params);
|
||||||
// Prepend implicit ctx for the sx-side closure call ABI
|
// Prepend implicit ctx for the sx-side closure call ABI
|
||||||
// (emit_llvm builds the call as [ctx, env, user_args]).
|
// (emit_llvm builds the call as [ctx, env, user_args]).
|
||||||
const owned = if (self.implicit_ctx_enabled) blk: {
|
const owned = if (self.implicit_ctx_enabled) blk: {
|
||||||
@@ -2635,6 +2653,22 @@ pub fn userParamTypes(self: *Lowering, func: *const Function) []TypeId {
|
|||||||
/// nothing as nominal names in that module: without this call's inferred
|
/// nothing as nominal names in that module: without this call's inferred
|
||||||
/// `$T → concrete` bindings the pin would resolve `T` as an undeclared
|
/// `$T → concrete` bindings the pin would resolve `T` as an undeclared
|
||||||
/// type in a non-main module and diagnose it unknown.
|
/// type in a non-main module and diagnose it unknown.
|
||||||
|
/// Coerce already-lowered closure-call arguments to the closure's declared
|
||||||
|
/// parameter types (issue 0186). The arg-lowering loop only sets `target_type`
|
||||||
|
/// (which steers literal lowering) but does NOT itself coerce, so a concrete
|
||||||
|
/// `7` flowing into a `?i64` param would reach `call_closure` as a bare `i64`
|
||||||
|
/// (read ABSENT by the callee) and a `null` as a bare pointer (LLVM verifier
|
||||||
|
/// failure). `args` are the USER args (no implicit ctx); `params` the closure's
|
||||||
|
/// user-visible param types. Coerces in place.
|
||||||
|
fn coerceClosureCallArgs(self: *Lowering, args: []Ref, params: []const TypeId) void {
|
||||||
|
const n = @min(args.len, params.len);
|
||||||
|
for (0..n) |i| {
|
||||||
|
if (args[i].isNone()) continue; // spread placeholder
|
||||||
|
const at = self.builder.getRefType(args[i]);
|
||||||
|
if (at != params[i]) args[i] = self.coerceToType(args[i], at, params[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn astCalleeParamTypes(self: *Lowering, fd: *const ast.FnDecl, args: []const *const Node) []const TypeId {
|
fn astCalleeParamTypes(self: *Lowering, fd: *const ast.FnDecl, args: []const *const Node) []const TypeId {
|
||||||
const saved_bindings = self.type_bindings;
|
const saved_bindings = self.type_bindings;
|
||||||
defer self.type_bindings = saved_bindings;
|
defer self.type_bindings = saved_bindings;
|
||||||
@@ -2788,6 +2822,22 @@ pub fn resolveCallParamTypes(self: *Lowering, c: *const ast.Call, sel_author: ?*
|
|||||||
}
|
}
|
||||||
if (c.callee.data != .identifier) return &.{};
|
if (c.callee.data != .identifier) return &.{};
|
||||||
const bare_name = c.callee.data.identifier.name;
|
const bare_name = c.callee.data.identifier.name;
|
||||||
|
// Closure / fn-pointer VALUE bound in scope (`g := () => ...; g(args)`):
|
||||||
|
// type each arg against the callee value's declared parameter types so a
|
||||||
|
// `?T` param wraps the argument (issue 0186) — without this the args lower
|
||||||
|
// with no target type and reach `call_closure` unconverted (a concrete arg
|
||||||
|
// arrives as a bare payload that reads ABSENT; `null` reaches a `{T,i1}`
|
||||||
|
// slot as a bare pointer → LLVM verifier failure). A local value shadows a
|
||||||
|
// same-named function, so this precedes the function-name resolution below.
|
||||||
|
if (self.scope) |scope| {
|
||||||
|
if (scope.lookup(bare_name)) |binding| {
|
||||||
|
if (!binding.ty.isBuiltin()) {
|
||||||
|
const bti = self.module.types.get(binding.ty);
|
||||||
|
if (bti == .closure) return bti.closure.params;
|
||||||
|
if (bti == .function) return bti.function.params;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const name = blk: {
|
const name = blk: {
|
||||||
const scoped = if (self.scope) |scope| scope.lookupFn(bare_name) orelse bare_name else bare_name;
|
const scoped = if (self.scope) |scope| scope.lookupFn(bare_name) orelse bare_name else bare_name;
|
||||||
if (self.program_index.ufcs_alias_map.get(bare_name)) |target| {
|
if (self.program_index.ufcs_alias_map.get(bare_name)) |target| {
|
||||||
|
|||||||
@@ -15,6 +15,15 @@ const Lowering = lower.Lowering;
|
|||||||
const Scope = lower.Scope;
|
const Scope = lower.Scope;
|
||||||
|
|
||||||
pub fn lowerLambda(self: *Lowering, lam: *const ast.Lambda) Ref {
|
pub fn lowerLambda(self: *Lowering, lam: *const ast.Lambda) Ref {
|
||||||
|
// Flow narrowing (issue 0179) does NOT cross into the lambda body: the
|
||||||
|
// body is a separate function whose `Ref` space overlaps the enclosing
|
||||||
|
// function's, so the outer `narrowed_refs` would falsely match body `Ref`s
|
||||||
|
// (unsound unwrap of a captured-but-not-proven-present optional). The body
|
||||||
|
// builds its own narrowing from scratch; the outer state is restored on
|
||||||
|
// return (re-arming narrowing for the rest of the enclosing expression).
|
||||||
|
var narrow_guard = Lowering.NarrowGuard.enter(self);
|
||||||
|
defer narrow_guard.restore();
|
||||||
|
|
||||||
// Lower the lambda body as a new anonymous function
|
// Lower the lambda body as a new anonymous function
|
||||||
var buf: [64]u8 = undefined;
|
var buf: [64]u8 = undefined;
|
||||||
const name = std.fmt.bufPrint(&buf, "__lambda_{d}", .{self.block_counter}) catch "__lambda";
|
const name = std.fmt.bufPrint(&buf, "__lambda_{d}", .{self.block_counter}) catch "__lambda";
|
||||||
|
|||||||
@@ -641,8 +641,22 @@ pub fn coerceMode(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId, mod
|
|||||||
}
|
}
|
||||||
return self.builder.emit(.{ .tuple_init = .{ .fields = self.alloc.dupe(Ref, elems.items) catch unreachable } }, dst_ty);
|
return self.builder.emit(.{ .tuple_init = .{ .fields = self.alloc.dupe(Ref, elems.items) catch unreachable } }, dst_ty);
|
||||||
},
|
},
|
||||||
// Optional → Concrete unwrapping (flow-sensitive narrowing coercion)
|
// Optional → Concrete unwrapping — ONLY when the value is PROVEN
|
||||||
|
// present by flow narrowing (issue 0179). An un-narrowed `?T` flowing
|
||||||
|
// into a concrete slot used to unwrap UNCONDITIONALLY, yielding the
|
||||||
|
// zero payload of a null optional with no diagnostic (silent
|
||||||
|
// miscompile across the whole `?T → concrete` family). Per spec the
|
||||||
|
// only legal extractions are `!` / `??` / binding / match / a `!= null`
|
||||||
|
// guard; reject everything else loudly. `lowerIdentifier` tags the
|
||||||
|
// loaded `Ref` of a guard-narrowed local into `narrowed_refs`.
|
||||||
.optional_unwrap => {
|
.optional_unwrap => {
|
||||||
|
if (!self.narrowed_refs.contains(val)) {
|
||||||
|
if (self.diagnostics) |d| {
|
||||||
|
const cs = self.builder.current_span;
|
||||||
|
d.addFmt(.err, ast.Span{ .start = cs.start, .end = cs.end }, "cannot use a value of type '{s}' where '{s}' is expected: an optional does not implicitly unwrap; force-unwrap with '!', supply a fallback with '?? <default>', bind it (`if v := ...`), or guard with '!= null'", .{ self.formatTypeName(src_ty), self.formatTypeName(dst_ty) });
|
||||||
|
}
|
||||||
|
return val; // hasErrors() aborts before codegen
|
||||||
|
}
|
||||||
const child_ty = self.module.types.get(src_ty).optional.child;
|
const child_ty = self.module.types.get(src_ty).optional.child;
|
||||||
const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = val } }, child_ty);
|
const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = val } }, child_ty);
|
||||||
return self.coerceMode(unwrapped, child_ty, dst_ty, mode);
|
return self.coerceMode(unwrapped, child_ty, dst_ty, mode);
|
||||||
|
|||||||
@@ -1127,6 +1127,12 @@ pub fn createComptimeFunctionWithPrelude(self: *Lowering, prefix: []const u8, pr
|
|||||||
const name = std.fmt.bufPrint(&buf, "{s}_{d}", .{ prefix, self.comptime_counter }) catch prefix;
|
const name = std.fmt.bufPrint(&buf, "{s}_{d}", .{ prefix, self.comptime_counter }) catch prefix;
|
||||||
self.comptime_counter += 1;
|
self.comptime_counter += 1;
|
||||||
|
|
||||||
|
// Flow narrowing (issue 0179) is per-function: this wrapper body has its
|
||||||
|
// own `Ref` space (overlapping the caller's), so isolate it from the
|
||||||
|
// caller's `narrowed`/`narrowed_refs` to avoid a false-positive unwrap gate.
|
||||||
|
var narrow_guard = Lowering.NarrowGuard.enter(self);
|
||||||
|
defer narrow_guard.restore();
|
||||||
|
|
||||||
// Save current builder + lowering state. The wrapper fn we're
|
// Save current builder + lowering state. The wrapper fn we're
|
||||||
// about to build runs the comptime expression in isolation —
|
// about to build runs the comptime expression in isolation —
|
||||||
// it must NOT inherit the enclosing call's `inline_return_target`
|
// it must NOT inherit the enclosing call's `inline_return_target`
|
||||||
|
|||||||
@@ -15,6 +15,93 @@ const Scope = lower.Scope;
|
|||||||
const ComptimeValue = Lowering.ComptimeValue;
|
const ComptimeValue = Lowering.ComptimeValue;
|
||||||
const isTypeCategoryMatch = Lowering.isTypeCategoryMatch;
|
const isTypeCategoryMatch = Lowering.isTypeCategoryMatch;
|
||||||
|
|
||||||
|
// ── Flow-sensitive narrowing (issue 0179) ───────────────────────
|
||||||
|
//
|
||||||
|
// `?T` only converts to a concrete `T` when the value is PROVEN present —
|
||||||
|
// otherwise the implicit unwrap silently yields the zero payload of a null
|
||||||
|
// optional (the bug). These helpers recognize the `!= null` / `== null`
|
||||||
|
// guard shapes and record which local names a branch / guard proves present;
|
||||||
|
// `lowerIdentifier` tags the loaded `Ref` of a narrowed name into
|
||||||
|
// `narrowed_refs`, and `coerceMode`'s `.optional_unwrap` arm only unwraps a
|
||||||
|
// tagged (proven-present) value.
|
||||||
|
|
||||||
|
/// The local-variable name if `node` is a bare identifier currently bound to
|
||||||
|
/// an OPTIONAL local/param in scope (the only thing flow-narrowing applies
|
||||||
|
/// to). Null for field paths, indexes, non-optionals, etc. — those keep the
|
||||||
|
/// explicit `!`/`??`/binding requirement.
|
||||||
|
pub fn narrowableLocal(self: *Lowering, node: *const Node) ?[]const u8 {
|
||||||
|
if (node.data != .identifier) return null;
|
||||||
|
const name = node.data.identifier.name;
|
||||||
|
const scope = self.scope orelse return null;
|
||||||
|
const b = scope.lookup(name) orelse return null;
|
||||||
|
if (b.ty.isBuiltin()) return null;
|
||||||
|
if (self.module.types.get(b.ty) != .optional) return null;
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If `bop` compares an optional local against the `null` literal (either
|
||||||
|
/// operand order), the narrowable local's name; else null.
|
||||||
|
pub fn nullCmpName(self: *Lowering, bop: ast.BinaryOp) ?[]const u8 {
|
||||||
|
const lhs_null = bop.lhs.data == .null_literal;
|
||||||
|
const rhs_null = bop.rhs.data == .null_literal;
|
||||||
|
if (lhs_null == rhs_null) return null; // need exactly one `null` side
|
||||||
|
return self.narrowableLocal(if (lhs_null) bop.rhs else bop.lhs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Names proven present when `cond` is TRUE: `x != null`, and the `and` of
|
||||||
|
/// such tests (`a != null and b != null`).
|
||||||
|
pub fn collectPresentIfTrue(self: *Lowering, cond: *const Node, out: *std.ArrayList([]const u8)) void {
|
||||||
|
if (cond.data != .binary_op) return;
|
||||||
|
const bop = cond.data.binary_op;
|
||||||
|
switch (bop.op) {
|
||||||
|
.neq => if (self.nullCmpName(bop)) |n| out.append(self.alloc, n) catch {},
|
||||||
|
.and_op => {
|
||||||
|
self.collectPresentIfTrue(bop.lhs, out);
|
||||||
|
self.collectPresentIfTrue(bop.rhs, out);
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Names proven present when `cond` is FALSE: `x == null` (false ⇒ present),
|
||||||
|
/// and the `or` of such tests (`a == null or b == null` — false ⇒ both
|
||||||
|
/// present). This is the guard-narrowing case (`if a == null or b == null
|
||||||
|
/// { return }` proves both present afterwards).
|
||||||
|
pub fn collectPresentIfFalse(self: *Lowering, cond: *const Node, out: *std.ArrayList([]const u8)) void {
|
||||||
|
if (cond.data != .binary_op) return;
|
||||||
|
const bop = cond.data.binary_op;
|
||||||
|
switch (bop.op) {
|
||||||
|
.eq => if (self.nullCmpName(bop)) |n| out.append(self.alloc, n) catch {},
|
||||||
|
.or_op => {
|
||||||
|
self.collectPresentIfFalse(bop.lhs, out);
|
||||||
|
self.collectPresentIfFalse(bop.rhs, out);
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Snapshot the currently-narrowed names so a region (block / branch) can
|
||||||
|
/// restore them on exit. Returns a list the caller must `deinit`.
|
||||||
|
pub fn narrowSnapshot(self: *Lowering) std.ArrayList([]const u8) {
|
||||||
|
var list = std.ArrayList([]const u8).empty;
|
||||||
|
var it = self.narrowed.keyIterator();
|
||||||
|
while (it.next()) |k| list.append(self.alloc, k.*) catch {};
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restore the narrowed-name set to a prior snapshot (drops anything added
|
||||||
|
/// since, re-adds anything killed since).
|
||||||
|
pub fn narrowRestore(self: *Lowering, saved: *std.ArrayList([]const u8)) void {
|
||||||
|
self.narrowed.clearRetainingCapacity();
|
||||||
|
for (saved.items) |n| self.narrowed.put(n, {}) catch {};
|
||||||
|
saved.deinit(self.alloc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark every name in `names` as narrowed (proven present) in the current set.
|
||||||
|
pub fn applyNarrowing(self: *Lowering, names: []const []const u8) void {
|
||||||
|
for (names) |n| self.narrowed.put(n, {}) catch {};
|
||||||
|
}
|
||||||
|
|
||||||
pub fn lowerIfExpr(self: *Lowering, ie: *const ast.IfExpr) Ref {
|
pub fn lowerIfExpr(self: *Lowering, ie: *const ast.IfExpr) Ref {
|
||||||
// inline if: evaluate condition at compile time, only lower taken branch
|
// inline if: evaluate condition at compile time, only lower taken branch
|
||||||
if (ie.is_comptime) {
|
if (ie.is_comptime) {
|
||||||
@@ -147,11 +234,27 @@ pub fn lowerIfExpr(self: *Lowering, ie: *const ast.IfExpr) Ref {
|
|||||||
scope.put(bind_name, .{ .ref = slot, .ty = inner_ty, .is_alloca = true });
|
scope.put(bind_name, .{ .ref = slot, .ty = inner_ty, .is_alloca = true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Flow narrowing (issue 0179): which local names this condition proves
|
||||||
|
// present in each arm. A binding `if v := opt` already unwraps `v`, so it
|
||||||
|
// contributes nothing here.
|
||||||
|
var present_true = std.ArrayList([]const u8).empty;
|
||||||
|
defer present_true.deinit(self.alloc);
|
||||||
|
var present_false = std.ArrayList([]const u8).empty;
|
||||||
|
defer present_false.deinit(self.alloc);
|
||||||
|
if (ie.binding_name == null) {
|
||||||
|
self.collectPresentIfTrue(ie.condition, &present_true);
|
||||||
|
self.collectPresentIfFalse(ie.condition, &present_false);
|
||||||
|
}
|
||||||
|
|
||||||
// Set target_type so null/undef in branches get the right type
|
// Set target_type so null/undef in branches get the right type
|
||||||
const saved_target = self.target_type;
|
const saved_target = self.target_type;
|
||||||
if (is_value and result_type != .void) self.target_type = result_type;
|
if (is_value and result_type != .void) self.target_type = result_type;
|
||||||
|
var then_diverged = false;
|
||||||
|
var then_snap = self.narrowSnapshot();
|
||||||
|
self.applyNarrowing(present_true.items);
|
||||||
if (is_value) {
|
if (is_value) {
|
||||||
var v = self.lowerExpr(ie.then_branch);
|
var v = self.lowerExpr(ie.then_branch);
|
||||||
|
then_diverged = self.currentBlockHasTerminator();
|
||||||
if (!self.currentBlockHasTerminator()) {
|
if (!self.currentBlockHasTerminator()) {
|
||||||
const v_ty = self.builder.getRefType(v);
|
const v_ty = self.builder.getRefType(v);
|
||||||
if (v_ty != result_type and v_ty != .void and result_type != .void) {
|
if (v_ty != result_type and v_ty != .void and result_type != .void) {
|
||||||
@@ -161,14 +264,18 @@ pub fn lowerIfExpr(self: *Lowering, ie: *const ast.IfExpr) Ref {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.lowerBlock(ie.then_branch);
|
self.lowerBlock(ie.then_branch);
|
||||||
|
then_diverged = self.currentBlockHasTerminator();
|
||||||
if (!self.currentBlockHasTerminator()) {
|
if (!self.currentBlockHasTerminator()) {
|
||||||
self.builder.br(merge_bb, &.{});
|
self.builder.br(merge_bb, &.{});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.narrowRestore(&then_snap);
|
||||||
|
|
||||||
// Else branch
|
// Else branch
|
||||||
if (has_else) {
|
if (has_else) {
|
||||||
self.builder.switchToBlock(else_bb.?);
|
self.builder.switchToBlock(else_bb.?);
|
||||||
|
var else_snap = self.narrowSnapshot();
|
||||||
|
self.applyNarrowing(present_false.items);
|
||||||
if (is_value) {
|
if (is_value) {
|
||||||
var v = self.lowerExpr(ie.else_branch.?);
|
var v = self.lowerExpr(ie.else_branch.?);
|
||||||
if (!self.currentBlockHasTerminator()) {
|
if (!self.currentBlockHasTerminator()) {
|
||||||
@@ -184,9 +291,15 @@ pub fn lowerIfExpr(self: *Lowering, ie: *const ast.IfExpr) Ref {
|
|||||||
self.builder.br(merge_bb, &.{});
|
self.builder.br(merge_bb, &.{});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.narrowRestore(&else_snap);
|
||||||
}
|
}
|
||||||
self.target_type = saved_target;
|
self.target_type = saved_target;
|
||||||
|
|
||||||
|
// Guard form: `if <x == null ...> { <diverges> }` with no else proves the
|
||||||
|
// tested names present for the remainder of the enclosing block. The
|
||||||
|
// enclosing `lowerBlock` snapshot drops this narrowing at block end.
|
||||||
|
if (!has_else and then_diverged) self.applyNarrowing(present_false.items);
|
||||||
|
|
||||||
// Continue at merge
|
// Continue at merge
|
||||||
self.builder.switchToBlock(merge_bb);
|
self.builder.switchToBlock(merge_bb);
|
||||||
if (is_value) {
|
if (is_value) {
|
||||||
|
|||||||
@@ -2036,6 +2036,15 @@ pub fn lowerDerefExpr(self: *Lowering, de: *const ast.DerefExpr) Ref {
|
|||||||
return ptr;
|
return ptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reject using an un-narrowed optional directly as a binary-op operand
|
||||||
|
/// (issue 0185). Mirrors the `coerceMode` `?T → concrete` rejection (0179):
|
||||||
|
/// the optional does not implicitly unwrap; steer the user to an explicit form.
|
||||||
|
pub fn diagOptionalOperand(self: *Lowering, opt_ty: TypeId, span: ast.Span) void {
|
||||||
|
if (self.diagnostics) |d| {
|
||||||
|
d.addFmt(.err, span, "cannot use a value of type '{s}' as an operand: an optional does not implicitly unwrap; force-unwrap with '!', supply a fallback with '?? <default>', or guard with '!= null'", .{self.formatTypeName(opt_ty)});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn lowerForceUnwrap(self: *Lowering, fu: *const ast.ForceUnwrap) Ref {
|
pub fn lowerForceUnwrap(self: *Lowering, fu: *const ast.ForceUnwrap) Ref {
|
||||||
const val = self.lowerExpr(fu.operand);
|
const val = self.lowerExpr(fu.operand);
|
||||||
const inner_ty = self.resolveOptionalInner(self.inferExprType(fu.operand));
|
const inner_ty = self.resolveOptionalInner(self.inferExprType(fu.operand));
|
||||||
@@ -2268,9 +2277,17 @@ pub fn lowerExpr(self: *Lowering, node: *const Node) Ref {
|
|||||||
// `inline for xs (x)` element capture — lower the
|
// `inline for xs (x)` element capture — lower the
|
||||||
// synthesized `xs[<i>]` it aliases.
|
// synthesized `xs[<i>]` it aliases.
|
||||||
if (binding.pack_elem) |elem| break :blk self.lowerExpr(elem);
|
if (binding.pack_elem) |elem| break :blk self.lowerExpr(elem);
|
||||||
|
// Flow narrowing (issue 0179): a name proven present by a
|
||||||
|
// `!= null` guard tags its loaded value so the implicit
|
||||||
|
// `?T → concrete` unwrap in `coerceMode` is permitted (an
|
||||||
|
// un-narrowed unwrap is rejected, not silently zeroed).
|
||||||
|
const is_narrowed = self.narrowed.count() > 0 and self.narrowed.contains(id.name);
|
||||||
if (binding.is_alloca) {
|
if (binding.is_alloca) {
|
||||||
break :blk self.builder.load(binding.ref, binding.ty);
|
const loaded = self.builder.load(binding.ref, binding.ty);
|
||||||
|
if (is_narrowed) self.narrowed_refs.put(loaded, {}) catch {};
|
||||||
|
break :blk loaded;
|
||||||
}
|
}
|
||||||
|
if (is_narrowed) self.narrowed_refs.put(binding.ref, {}) catch {};
|
||||||
break :blk binding.ref;
|
break :blk binding.ref;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3207,10 +3224,19 @@ pub fn lowerBinaryOp(self: *Lowering, bop: *const ast.BinaryOp) Ref {
|
|||||||
const rhs_inferred = rhs_ref_pointee orelse self.inferExprType(bop.rhs);
|
const rhs_inferred = rhs_ref_pointee orelse self.inferExprType(bop.rhs);
|
||||||
var ty = arithResultType(lhs_ty, rhs_inferred);
|
var ty = arithResultType(lhs_ty, rhs_inferred);
|
||||||
|
|
||||||
// Auto-unwrap optional operands for arithmetic/comparison
|
// Auto-unwrap optional operands for arithmetic/comparison — ONLY when the
|
||||||
|
// operand is PROVEN present by flow narrowing (issue 0185, the operand-side
|
||||||
|
// sibling of 0179). An un-narrowed `?T` operand used to unwrap
|
||||||
|
// UNCONDITIONALLY, so a null operand silently became its zero payload
|
||||||
|
// (`null + 10` → `10`, no diagnostic). `lowerIdentifier` tags a
|
||||||
|
// guard-narrowed local's loaded `Ref` into `narrowed_refs`; an un-narrowed
|
||||||
|
// optional operand is rejected loudly (then still unwrapped so the IR stays
|
||||||
|
// well-formed — `hasErrors()` aborts before codegen). Presence tests
|
||||||
|
// (`x == null` / `x != null`) returned early above, so they're unaffected.
|
||||||
if (!ty.isBuiltin()) {
|
if (!ty.isBuiltin()) {
|
||||||
const info = self.module.types.get(ty);
|
const info = self.module.types.get(ty);
|
||||||
if (info == .optional) {
|
if (info == .optional) {
|
||||||
|
if (!self.narrowed_refs.contains(lhs)) self.diagOptionalOperand(ty, bop.lhs.span);
|
||||||
ty = info.optional.child;
|
ty = info.optional.child;
|
||||||
lhs = self.builder.emit(.{ .optional_unwrap = .{ .operand = lhs } }, ty);
|
lhs = self.builder.emit(.{ .optional_unwrap = .{ .operand = lhs } }, ty);
|
||||||
}
|
}
|
||||||
@@ -3219,6 +3245,7 @@ pub fn lowerBinaryOp(self: *Lowering, bop: *const ast.BinaryOp) Ref {
|
|||||||
if (!rhs_ty.isBuiltin()) {
|
if (!rhs_ty.isBuiltin()) {
|
||||||
const rhs_info = self.module.types.get(rhs_ty);
|
const rhs_info = self.module.types.get(rhs_ty);
|
||||||
if (rhs_info == .optional) {
|
if (rhs_info == .optional) {
|
||||||
|
if (!self.narrowed_refs.contains(rhs)) self.diagOptionalOperand(rhs_ty, bop.rhs.span);
|
||||||
rhs = self.builder.emit(.{ .optional_unwrap = .{ .operand = rhs } }, rhs_info.optional.child);
|
rhs = self.builder.emit(.{ .optional_unwrap = .{ .operand = rhs } }, rhs_info.optional.child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1054,6 +1054,15 @@ pub fn synthesizeJniMainStubs(self: *Lowering) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn synthesizeJniMainStub(self: *Lowering, fcd: *const ast.RuntimeClassDecl, md: ast.RuntimeMethodDecl) void {
|
pub fn synthesizeJniMainStub(self: *Lowering, fcd: *const ast.RuntimeClassDecl, md: ast.RuntimeMethodDecl) void {
|
||||||
|
// Flow narrowing (issue 0179) is per-function: each native-method stub body
|
||||||
|
// gets its own `Ref` space (reset by `beginFunction` below) that OVERLAPS
|
||||||
|
// both the enclosing pass and a sibling method's stub. Without isolation the
|
||||||
|
// previous method's `narrowed_refs` indices falsely match this body's `Ref`s
|
||||||
|
// and permit an unsound unwrap of a non-present optional. Clear on entry,
|
||||||
|
// restore on exit — same contract as the closure / monomorphization paths.
|
||||||
|
var narrow_guard = Lowering.NarrowGuard.enter(self);
|
||||||
|
defer narrow_guard.restore();
|
||||||
|
|
||||||
const mangled = jni_descriptor.jniMangleNativeName(self.alloc, fcd.runtime_path, md.name) catch return;
|
const mangled = jni_descriptor.jniMangleNativeName(self.alloc, fcd.runtime_path, md.name) catch return;
|
||||||
const name_id = self.module.types.internString(mangled);
|
const name_id = self.module.types.internString(mangled);
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ pub fn monomorphizeFunction(self: *Lowering, fd: *const ast.FnDecl, mangled_name
|
|||||||
const owned_name = self.alloc.dupe(u8, mangled_name) catch return;
|
const owned_name = self.alloc.dupe(u8, mangled_name) catch return;
|
||||||
self.lowered_functions.put(owned_name, {}) catch {};
|
self.lowered_functions.put(owned_name, {}) catch {};
|
||||||
|
|
||||||
|
// Flow narrowing (issue 0179) is per-function: this monomorphized body has
|
||||||
|
// its own `Ref` space (overlapping the caller's), so isolate it from the
|
||||||
|
// caller's `narrowed`/`narrowed_refs` to avoid a false-positive unwrap gate.
|
||||||
|
var narrow_guard = Lowering.NarrowGuard.enter(self);
|
||||||
|
defer narrow_guard.restore();
|
||||||
|
|
||||||
// Save builder state
|
// Save builder state
|
||||||
const saved_func = self.builder.func;
|
const saved_func = self.builder.func;
|
||||||
const saved_block = self.builder.current_block;
|
const saved_block = self.builder.current_block;
|
||||||
|
|||||||
@@ -795,6 +795,12 @@ pub fn monomorphizePackFn(
|
|||||||
const owned_name = self.alloc.dupe(u8, mangled_name) catch return;
|
const owned_name = self.alloc.dupe(u8, mangled_name) catch return;
|
||||||
self.lowered_functions.put(owned_name, {}) catch {};
|
self.lowered_functions.put(owned_name, {}) catch {};
|
||||||
|
|
||||||
|
// Flow narrowing (issue 0179) is per-function: this monomorphized pack body
|
||||||
|
// has its own `Ref` space (overlapping the caller's), so isolate it from the
|
||||||
|
// caller's `narrowed`/`narrowed_refs` to avoid a false-positive unwrap gate.
|
||||||
|
var narrow_guard = Lowering.NarrowGuard.enter(self);
|
||||||
|
defer narrow_guard.restore();
|
||||||
|
|
||||||
// Find the pack param's name and position in fd.params, plus its
|
// Find the pack param's name and position in fd.params, plus its
|
||||||
// constraint protocol (`..xs: Box` ⇒ "Box"; comptime `..$args` has none).
|
// constraint protocol (`..xs: Box` ⇒ "Box"; comptime `..$args` has none).
|
||||||
var pack_name: []const u8 = "";
|
var pack_name: []const u8 = "";
|
||||||
|
|||||||
@@ -22,10 +22,14 @@ pub fn lowerBlock(self: *Lowering, node: *const Node) void {
|
|||||||
const saved_scope = self.scope;
|
const saved_scope = self.scope;
|
||||||
self.scope = &block_scope;
|
self.scope = &block_scope;
|
||||||
const saved_defer_len = self.defer_stack.items.len;
|
const saved_defer_len = self.defer_stack.items.len;
|
||||||
|
// Flow narrowing (issue 0179) is block-scoped: a guard inside this
|
||||||
|
// block narrows the rest of THIS block, no further.
|
||||||
|
var narrow_snap = self.narrowSnapshot();
|
||||||
defer {
|
defer {
|
||||||
self.emitBlockDefers(saved_defer_len);
|
self.emitBlockDefers(saved_defer_len);
|
||||||
self.scope = saved_scope;
|
self.scope = saved_scope;
|
||||||
block_scope.deinit();
|
block_scope.deinit();
|
||||||
|
self.narrowRestore(&narrow_snap);
|
||||||
}
|
}
|
||||||
for (blk.stmts) |stmt| {
|
for (blk.stmts) |stmt| {
|
||||||
if (self.block_terminated) break;
|
if (self.block_terminated) break;
|
||||||
@@ -76,10 +80,12 @@ pub fn lowerBlockValue(self: *Lowering, node: *const Node) ?Ref {
|
|||||||
const saved_scope = self.scope;
|
const saved_scope = self.scope;
|
||||||
self.scope = &block_scope;
|
self.scope = &block_scope;
|
||||||
const saved_defer_len = self.defer_stack.items.len;
|
const saved_defer_len = self.defer_stack.items.len;
|
||||||
|
var narrow_snap = self.narrowSnapshot();
|
||||||
defer {
|
defer {
|
||||||
self.emitBlockDefers(saved_defer_len);
|
self.emitBlockDefers(saved_defer_len);
|
||||||
self.scope = saved_scope;
|
self.scope = saved_scope;
|
||||||
block_scope.deinit();
|
block_scope.deinit();
|
||||||
|
self.narrowRestore(&narrow_snap);
|
||||||
}
|
}
|
||||||
// A block whose last statement is `;`-terminated (or not an
|
// A block whose last statement is `;`-terminated (or not an
|
||||||
// expression) discards its value: lower every statement as a
|
// expression) discards its value: lower every statement as a
|
||||||
@@ -801,6 +807,13 @@ fn tryLowerPropertyAssignment(self: *Lowering, asgn: *const ast.Assignment) bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn lowerAssignment(self: *Lowering, asgn: *const ast.Assignment) void {
|
pub fn lowerAssignment(self: *Lowering, asgn: *const ast.Assignment) void {
|
||||||
|
// Reassignment kills flow narrowing (issue 0179 / specs.md §Flow-Sensitive
|
||||||
|
// Narrowing): a fresh value may be null, so the name is no longer proven
|
||||||
|
// present. Drop it from the narrowed set before lowering the store.
|
||||||
|
if (asgn.target.data == .identifier) {
|
||||||
|
_ = self.narrowed.remove(asgn.target.data.identifier.name);
|
||||||
|
}
|
||||||
|
|
||||||
// Writes through a constant are rejected at compile time (issue 0116):
|
// Writes through a constant are rejected at compile time (issue 0116):
|
||||||
// the target chain's root naming a const global (array/struct consts,
|
// the target chain's root naming a const global (array/struct consts,
|
||||||
// #run consts) or a module value const cannot be stored to — for a
|
// #run consts) or a module value const cannot be stored to — for a
|
||||||
|
|||||||
Reference in New Issue
Block a user