fibers B1.2: 0152 fixed → Atomic(bool) works; blocked on 0153 (re-export value-failable loses ! channel)
With 0151 + 0152 fixed, the async surface is callable and Atomic(bool) works.
Building the async examples isolated the TRUE remaining blocker (the earlier
'secondary or PHI' symptom, confirmed NOT an Atomic cascade): a re-exported
generic value-failable ($R, !E) fn loses its ! error channel at the call site
— the result types as a plain tuple, so await(...) or { ... } / try ...await()
fail / build a malformed i1 PHI. await/IoErr are re-exported via std.sx, so the
async surface hits it.
Narrowed to the generic + re-export co-requirement (non-generic re-export OK;
direct generic import OK). Filed issues/0153 with a minimal co-located 2-file
repro + a single-file stdlib-await repro + investigation prompt (root cause:
the monomorphized return-type's error-set, reached via the re-export alias,
resolves to a non-.error_set TypeId, so errorChannelOf misses the channel).
Per the STOP rule, paused B1.2's async examples pending the 0153 fix.
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
# 0153 — a re-exported generic value-failable `($R, !E)` loses its `!` error channel
|
||||
|
||||
## Symptom
|
||||
|
||||
A generic function returning a value-failable `($R, !E)` keeps its error
|
||||
channel when called from the module that declares it, but **loses it when
|
||||
the function is reached through a re-export alias** (`get :: lib.get;`). At
|
||||
the consumer the call result is typed as a plain **tuple** (last field is a
|
||||
*non*-`.error_set` type), so:
|
||||
|
||||
- `try f()` → `error: `try` requires a failable expression; operand has type 'tuple'`
|
||||
- `f() or { default }` → LLVM verification failure — the `or` merge PHI is
|
||||
typed `i1` (the lost-channel discriminant) but carries the `i64` default:
|
||||
```
|
||||
PHI node operands are not the same type as the result!
|
||||
%bp = phi i1 [ true, %entry ], [ -1, %or.rhs.0 ]
|
||||
```
|
||||
|
||||
It requires **both** conditions — drop either and it works:
|
||||
|
||||
- non-generic re-exported value-failable + `or` → **works**
|
||||
- generic value-failable imported **directly** (no re-export) + `or` → **works**
|
||||
- generic value-failable, **direct call** (no UFCS) through a re-export → **fails** too
|
||||
(so it is NOT UFCS-specific)
|
||||
|
||||
## Reproduction
|
||||
|
||||
Co-located minimal repro (two files, no project deps beyond `modules/std.sx`):
|
||||
`issues/0153-...sx` (consumer) + `issues/0153-.../lib.sx` (impl). Run the
|
||||
consumer; expect `r=42`, get the PHI verification failure.
|
||||
|
||||
```sx
|
||||
// lib.sx
|
||||
#import "modules/std.sx";
|
||||
LE :: error { Bad }
|
||||
Box :: struct ($R: Type) { v: R; }
|
||||
get :: ufcs (b: *Box($R)) -> ($R, !LE) { return b.v; }
|
||||
```
|
||||
```sx
|
||||
// main
|
||||
#import "modules/std.sx";
|
||||
lib :: #import ".../lib.sx";
|
||||
Box :: lib.Box; // re-export the generic struct,
|
||||
get :: lib.get; // the generic value-failable fn,
|
||||
LE :: lib.LE; // AND its error set (the std.sx facade pattern)
|
||||
main :: () -> i32 {
|
||||
b : Box(i64) = .{ v = 42 };
|
||||
r := b.get() or { -1 }; // ← PHI i1/i64 mismatch
|
||||
print("r={}\n", r);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
Real-world one-liner (same bug, via the stdlib facade — `await`/`IoErr` are
|
||||
re-exported from `std/io.sx` through `std.sx`):
|
||||
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
#import "modules/std/atomic.sx";
|
||||
main :: () -> i32 {
|
||||
f : Future(i64) = ---;
|
||||
f.value = 42; f.state = .ready; f.canceled = Atomic(bool).init(false);
|
||||
r := f.await() or { -1 }; // ← same PHI mismatch
|
||||
print("r={}\n", r);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## Impact
|
||||
|
||||
Blocks the B1.2 async surface (the LAST blocker after 0151 + 0152). `await`
|
||||
returns `($R, !IoErr)` and is re-exported via `std.sx`
|
||||
(`await :: io_mod.await; IoErr :: io_mod.IoErr;`), so every
|
||||
`context.io.async(...).await() or { … }` / `try …await()` hits this. The
|
||||
async runtime itself is correct (Futures build, `$R` infers, the value is
|
||||
right) — only the call-site failable typing is wrong.
|
||||
|
||||
## Investigation prompt
|
||||
|
||||
A value-failable `(T, !E)` is represented as a **tuple whose LAST field is
|
||||
an `.error_set` TypeId** — that is exactly what `Lowering.errorChannelOf`
|
||||
(`src/ir/lower/error.zig:148`) keys on. The bug is that the call-result type
|
||||
inferred for a re-exported generic fn is a tuple whose last field is NOT an
|
||||
`.error_set`, so `errorChannelOf` returns null (→ "plain tuple").
|
||||
|
||||
Suspect: the generic return-type resolution
|
||||
(`inferGenericReturnType` / `buildTypeBindings` in `src/ir/generics.zig` +
|
||||
`monomorphizeFunction` in `src/ir/lower/generic.zig`) resolves the fn's
|
||||
return-type AST `($R, !LE)` in a module context where the error-set name
|
||||
reached through the re-export alias (`LE :: lib.LE`) resolves to a TypeId
|
||||
that is NOT tagged `.error_set` (a duplicate/plain interning of the aliased
|
||||
error type, or the alias is followed to a non-error-set placeholder). The
|
||||
"generic + re-export" co-requirement points at the monomorphized return-type
|
||||
path specifically — a non-generic re-export keeps the channel (its return
|
||||
type isn't re-resolved per-instance), and a direct generic import keeps it
|
||||
(the error set resolves in its own module).
|
||||
|
||||
Steps:
|
||||
1. At the consumer call site, dump the inferred call-result TypeId for
|
||||
`b.get()` and inspect its last tuple field's `TypeInfo` — confirm it is
|
||||
NOT `.error_set` (vs the direct-import case, where it IS).
|
||||
2. Trace where the aliased error-set name (`LE` / `IoErr`) is resolved during
|
||||
the instance's return-type construction; ensure it resolves to the SAME
|
||||
`.error_set` TypeId the declaring module interned (follow the re-export
|
||||
alias to the original error set, don't re-intern a plain type).
|
||||
|
||||
Verification: run the co-located repro; expect `r=42`. Then restore the B1.2
|
||||
async examples (`examples/1805-concurrency-io-blocking-async.sx` +
|
||||
`1806-...-io-cancel.sx`) per CHECKPOINT-FIBERS and confirm
|
||||
`sum: 42` / `double: 42` / cancel raises `.Canceled`.
|
||||
@@ -0,0 +1,20 @@
|
||||
// Repro for issue 0153 — a generic value-failable fn `($R, !E)` reached
|
||||
// through a RE-EXPORT alias loses its `!` error channel at the call site:
|
||||
// the result is typed as a plain tuple, so `try`/`or` reject it / build a
|
||||
// malformed PHI. Needs BOTH generic + re-export: a non-generic re-export
|
||||
// works, and a directly-imported (non-re-exported) generic value-failable
|
||||
// works. Mirrors std.sx's `await :: io_mod.await` (+ `IoErr :: io_mod.IoErr`).
|
||||
#import "modules/std.sx";
|
||||
lib :: #import "issues/0153-reexport-generic-value-failable-loses-error-channel/lib.sx";
|
||||
|
||||
// Re-export the generic fn AND its error set (the std.sx facade pattern).
|
||||
Box :: lib.Box;
|
||||
get :: lib.get;
|
||||
LE :: lib.LE;
|
||||
|
||||
main :: () -> i32 {
|
||||
b : Box(i64) = .{ v = 42 };
|
||||
r := b.get() or { -1 }; // BUG: PHI i1/i64 mismatch (was: clean → r=42)
|
||||
print("r={}\n", r);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Implementation module: a generic value-failable `ufcs` fn + its error set.
|
||||
#import "modules/std.sx";
|
||||
|
||||
LE :: error { Bad }
|
||||
Box :: struct ($R: Type) { v: R; }
|
||||
|
||||
// Returns `($R, !LE)` — a value-failable. `$R` is inferred from the arg.
|
||||
get :: ufcs (b: *Box($R)) -> ($R, !LE) { return b.v; }
|
||||
Reference in New Issue
Block a user