ERR/E1.8: path-sensitive value-slot liveness check
A `v, err := failable()` destructure now binds the value slot(s) "live
only where `err` is proven absent". Reading `v` where the compiler cannot
prove `err == null` is a compile error.
New diagnostic-only Pass 1e (`checkErrorFlow` in ir/lower.zig): a
structured, path-sensitive walk over each main-file function body. A
proven-null set is threaded across branches and joined by intersection
at each `if`'s merge. Proof shapes recognized:
- `if !err { … v … }` (proven inside the guard)
- `if err { return/raise } … v` (proven on the fall-through)
- `if err { … } else { … v … }` (proven in the else branch)
- `!err and <reads v>` (short-circuit refinement)
Error-set tag compares (`if err == error.X`) prove nothing about
absence — they narrow the tag only. Nested lambdas are analyzed as their
own boundaries. Library modules are trusted (skipped).
Migrated the canon value-failable examples (1011/1012/1018/1044) to read
their value slots under `if !err` guards — output unchanged. New
regressions: 1046 (every proof shape compiles + runs, exit 210) and 1047
(unproven reads rejected, exit 1).
Gates: zig build, zig build test, run_examples.sh -> 338 passed, 0 failed.
This commit is contained in:
@@ -19,9 +19,10 @@ parse :: (n: s32) -> (s32, !E) {
|
||||
main :: () -> s32 {
|
||||
r : s32 = 0;
|
||||
|
||||
// The value slot is live only where the error is proven absent (ERR E1.8):
|
||||
// read `v1` under an `if !e1` guard, not after a bare tag-compare.
|
||||
v1, e1 := parse(5); // success → v1 = 50, e1 = no error
|
||||
if e1 == error.Bad { r = r + 1000; } // false
|
||||
r = r + v1; // +50
|
||||
if !e1 { r = r + v1; } // success → +50
|
||||
|
||||
v2, e2 := parse(-1); // Bad
|
||||
if e2 == error.Bad { r = r + 7; } // true → +7
|
||||
|
||||
@@ -46,8 +46,7 @@ classify :: (n: s32) -> s32 {
|
||||
main :: () -> s32 {
|
||||
r : s32 = 0;
|
||||
a, ea := inc(5); // parse(5)=10 → v=10 → 11
|
||||
if ea == error.Bad { r = r + 100; } // false
|
||||
r = r + a; // +11
|
||||
if !ea { r = r + a; } // success → +11 (value live only when proven ok)
|
||||
b, eb := inc(-1); // parse(-1)=Bad → propagate {undef, Bad}
|
||||
if eb == error.Bad { r = r + 4; } // true → +4
|
||||
er := relay(3); // parse(3)=6 ok → relay ok
|
||||
|
||||
@@ -52,15 +52,13 @@ main :: () -> s32 {
|
||||
// Destructure binds EVERY slot including the error tag (e1 / e2 / e3) —
|
||||
// the error is treated, never dropped.
|
||||
v1, b1, e1 := parse(5); // success → (10, 6, no-error)
|
||||
if e1 == error.Bad { r = r + 1000; } // false
|
||||
r = r + v1 + b1; // +16
|
||||
if !e1 { r = r + v1 + b1; } // success → +16 (slots live only when proven ok)
|
||||
|
||||
v2, b2, e2 := parse(-1); // Bad → {undef, undef, Bad}
|
||||
if e2 == error.Bad { r = r + 4; } // +4
|
||||
|
||||
a, c, ea := inc(5); // parse(5)=(10,6) → (11, 7, no-error)
|
||||
if ea == error.Bad { r = r + 2000; } // false
|
||||
r = r + a + c; // +18
|
||||
if !ea { r = r + a + c; } // success → +18
|
||||
|
||||
a2, c2, e3 := inc(-1); // try parse(-1)=Bad → propagate {undef, undef, Bad}
|
||||
if e3 == error.Bad { r = r + 5; } // +5
|
||||
|
||||
@@ -15,10 +15,10 @@ main :: () -> s32 {
|
||||
// success, consumed by catch
|
||||
print("catch={}\n", wrap(s32, closure(() -> (s32, !E) { return 7; })) catch e -1); // 7
|
||||
|
||||
// success, consumed by destructure (binds value + error slot)
|
||||
// success, consumed by destructure (binds value + error slot); the value
|
||||
// slot is read only under an `if !err` guard (ERR E1.8 path-sensitivity)
|
||||
r, err := wrap(s32, closure(() -> (s32, !E) { return 9; }));
|
||||
no_err := if err == error.Bad then false else true;
|
||||
print("destr={} ok={}\n", r, no_err); // destr=9 ok=true
|
||||
if !err { print("destr={} ok=true\n", r); } // destr=9 ok=true
|
||||
|
||||
// failure path: the raised tag propagates through the generic `try`
|
||||
print("fail={}\n", wrap(s32, closure(() -> (s32, !E) { raise error.Bad; }) ) catch e -1); // -1
|
||||
|
||||
60
examples/1046-errors-value-slot-liveness.sx
Normal file
60
examples/1046-errors-value-slot-liveness.sx
Normal file
@@ -0,0 +1,60 @@
|
||||
// Path-sensitive value-slot liveness (ERR step E1.8). After `v, err := f()`, the
|
||||
// value slot `v` is "live only where `err` is proven absent". Every read of `v`
|
||||
// below sits on a path where the compiler can prove `err == null`:
|
||||
//
|
||||
// • `if !err { … v … }` — proven inside the guard
|
||||
// • `if err { return } … v …` — proven on the fall-through
|
||||
// • `if err { raise } … v …` — fall-through in a failable function
|
||||
// • `if err { … } else { … v … }` — proven in the else branch
|
||||
// • `!err and <reads v>` — short-circuit keeps the proof
|
||||
//
|
||||
// A bare tag-compare (`if err == error.X`) proves NOTHING about absence — see the
|
||||
// rejection regression in 1047. (Regression for the E1.8 path-sensitive slice.)
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
E :: error { Bad, Empty }
|
||||
|
||||
parse :: (n: s32) -> (s32, !E) {
|
||||
if n < 0 { raise error.Bad; }
|
||||
if n == 0 { raise error.Empty; }
|
||||
return n * 10;
|
||||
}
|
||||
|
||||
// Early-return guard: the fall-through proves `err` absent.
|
||||
guarded :: (n: s32) -> s32 {
|
||||
v, err := parse(n);
|
||||
if err { return -1; }
|
||||
return v; // err proven absent here
|
||||
}
|
||||
|
||||
// `if err { raise }` in a failable function: same fall-through proof.
|
||||
relay :: (n: s32) -> (s32, !E) {
|
||||
v, err := parse(n);
|
||||
if err { raise err; }
|
||||
return v + 1; // err proven absent here
|
||||
}
|
||||
|
||||
main :: () -> s32 {
|
||||
total : s32 = 0;
|
||||
|
||||
// (1) proven inside `if !err`
|
||||
v1, e1 := parse(5);
|
||||
if !e1 { total = total + v1; } // +50
|
||||
|
||||
// (2) proven in the else branch
|
||||
v2, e2 := parse(7);
|
||||
if e2 { total = total + 1; } else { total = total + v2; } // +70
|
||||
|
||||
// (3) short-circuit `&&` keeps the proof for the rhs
|
||||
v3, e3 := parse(3);
|
||||
if !e3 and v3 > 0 { total = total + v3; } // +30
|
||||
|
||||
// (4) early-return / raise helpers
|
||||
total = total + guarded(4); // +40
|
||||
total = total + guarded(-1); // -1
|
||||
total = total + (relay(2) catch e 0); // parse(2)=20 → +1 = 21
|
||||
|
||||
print("liveness total: {}\n", total); // 50+70+30+40-1+21 = 210
|
||||
return total;
|
||||
}
|
||||
35
examples/1047-errors-value-slot-liveness-reject.sx
Normal file
35
examples/1047-errors-value-slot-liveness-reject.sx
Normal file
@@ -0,0 +1,35 @@
|
||||
// Rejection counterpart to 1046 (ERR step E1.8). Reading a failable's value slot
|
||||
// where its error is NOT proven absent is a compile error. Two unproven shapes:
|
||||
//
|
||||
// (A) reading the value inside the `if err { … }` error path itself
|
||||
// (B) reading the value after a bare tag-compare (`if err == error.X`), which
|
||||
// narrows the tag but proves nothing about absence
|
||||
//
|
||||
// Each read is rejected with the E1.8 diagnostic; the program never runs (exit 1).
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
E :: error { Bad }
|
||||
|
||||
parse :: (n: s32) -> (s32, !E) {
|
||||
if n < 0 { raise error.Bad; }
|
||||
return n * 10;
|
||||
}
|
||||
|
||||
// (A) the read sits on the error path — `err` is present here, not absent.
|
||||
bad_a :: () -> s32 {
|
||||
v, err := parse(5);
|
||||
if err { return v; } // REJECTED: err present on this path
|
||||
return 0;
|
||||
}
|
||||
|
||||
// (B) a tag-compare narrows which error, but does not prove there is none.
|
||||
bad_b :: () -> s32 {
|
||||
v, err := parse(5);
|
||||
if err == error.Bad { return 1; }
|
||||
return v; // REJECTED: err not proven absent
|
||||
}
|
||||
|
||||
main :: () -> s32 {
|
||||
return bad_a() + bad_b();
|
||||
}
|
||||
1
examples/expected/1046-errors-value-slot-liveness.exit
Normal file
1
examples/expected/1046-errors-value-slot-liveness.exit
Normal file
@@ -0,0 +1 @@
|
||||
210
|
||||
1
examples/expected/1046-errors-value-slot-liveness.stderr
Normal file
1
examples/expected/1046-errors-value-slot-liveness.stderr
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
examples/expected/1046-errors-value-slot-liveness.stdout
Normal file
1
examples/expected/1046-errors-value-slot-liveness.stdout
Normal file
@@ -0,0 +1 @@
|
||||
liveness total: 210
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,11 @@
|
||||
error: value `v` from a failable can be used only where its error `err` is proven absent — guard the use with `if !err { … }`, or return early with `if err { return; }` before reading `v`
|
||||
--> /Users/agra/projects/sx/examples/1047-errors-value-slot-liveness-reject.sx:22:21
|
||||
|
|
||||
22 | if err { return v; } // REJECTED: err present on this path
|
||||
| ^
|
||||
|
||||
error: value `v` from a failable can be used only where its error `err` is proven absent — guard the use with `if !err { … }`, or return early with `if err { return; }` before reading `v`
|
||||
--> /Users/agra/projects/sx/examples/1047-errors-value-slot-liveness-reject.sx:30:12
|
||||
|
|
||||
30 | return v; // REJECTED: err not proven absent
|
||||
| ^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
Reference in New Issue
Block a user