diff --git a/examples/1058-errors-reexport-value-failable-channel.sx b/examples/1058-errors-reexport-value-failable-channel.sx new file mode 100644 index 00000000..11061920 --- /dev/null +++ b/examples/1058-errors-reexport-value-failable-channel.sx @@ -0,0 +1,23 @@ +// A generic value-failable fn `($R, !E)` reached through a RE-EXPORT alias +// keeps its `!` error channel at the call site — the result types as a +// value-failable, so `or` / `try` accept it. Mirrors std.sx's +// `await :: io_mod.await` (+ `IoErr :: io_mod.IoErr`) re-export. +// Regression (issue 0153): the planned call-result type was resolved in the +// CALL-SITE module (where `LE` is a re-export alias → a non-`.error_set` +// TypeId), so `errorChannelOf` saw a plain tuple and `b.get() or {…}` built a +// malformed i1 PHI. The fix pins return-type resolution to the fn's defining +// module, matching `monomorphizeFunction`. Needs BOTH generic + re-export. +#import "modules/std.sx"; +lib :: #import "examples/1058-errors-reexport-value-failable-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 }; // value-failable channel preserved → r=42 + print("r={}\n", r); + return 0; +} diff --git a/issues/0153-reexport-generic-value-failable-loses-error-channel/lib.sx b/examples/1058-errors-reexport-value-failable-channel/lib.sx similarity index 100% rename from issues/0153-reexport-generic-value-failable-loses-error-channel/lib.sx rename to examples/1058-errors-reexport-value-failable-channel/lib.sx diff --git a/examples/expected/1058-errors-reexport-value-failable-channel.exit b/examples/expected/1058-errors-reexport-value-failable-channel.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/expected/1058-errors-reexport-value-failable-channel.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/1058-errors-reexport-value-failable-channel.stderr b/examples/expected/1058-errors-reexport-value-failable-channel.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/expected/1058-errors-reexport-value-failable-channel.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/1058-errors-reexport-value-failable-channel.stdout b/examples/expected/1058-errors-reexport-value-failable-channel.stdout new file mode 100644 index 00000000..ef8f1a45 --- /dev/null +++ b/examples/expected/1058-errors-reexport-value-failable-channel.stdout @@ -0,0 +1 @@ +r=42 diff --git a/issues/0153-reexport-generic-value-failable-loses-error-channel.md b/issues/0153-reexport-generic-value-failable-loses-error-channel.md index 59733ee2..7714d97a 100644 --- a/issues/0153-reexport-generic-value-failable-loses-error-channel.md +++ b/issues/0153-reexport-generic-value-failable-loses-error-channel.md @@ -1,5 +1,34 @@ # 0153 — a re-exported generic value-failable `($R, !E)` loses its `!` error channel +## ✅ RESOLVED (2026-06-21) + +**Root cause** — `GenericResolver.inferGenericReturnType` +(`src/ir/generics.zig`) resolved the generic call's return-type AST +(`($R, !E)`) in the CALL-SITE module context. For a re-exported fn the error +set name (`LE` / `IoErr`, re-exported as `LE :: lib.LE`) resolved through the +call-site alias to a TypeId that is NOT tagged `.error_set`, so the planned +result was a tuple whose last field wasn't an error set — `errorChannelOf` +(`lower/error.zig:148`) saw a plain tuple and the failable channel was lost. +`monomorphizeFunction` already pins the source to the fn's defining module +before resolving the return type; `inferGenericReturnType` did not, so the +planned call-result type and the instance's real signature disagreed. + +**Fix** — pin the source to the function's defining module +(`fd.body.source_file`) around the return-type resolution in +`inferGenericReturnType`, mirroring `monomorphizeFunction`. The binding-build +stays in the call-site context (its args are typed there). Now the `!E` +resolves to the same `.error_set` TypeId the instance's signature uses. + +**Verified** — the repro prints `r=42`; regression test +`examples/1058-errors-reexport-value-failable-channel.sx` (+ companion +`lib.sx`). This also unblocked the B1.2 async surface end-to-end: +`examples/1805-concurrency-io-blocking-async.sx` (`sum: 42` / `double: 42` / +`clock ok`) + `examples/1806-concurrency-io-cancel.sx` (cancel → `await` +raises `.Canceled`). Full suite green. + +--- + + ## Symptom A generic function returning a value-failable `($R, !E)` keeps its error diff --git a/issues/0153-reexport-generic-value-failable-loses-error-channel.sx b/issues/0153-reexport-generic-value-failable-loses-error-channel.sx deleted file mode 100644 index 300eefb5..00000000 --- a/issues/0153-reexport-generic-value-failable-loses-error-channel.sx +++ /dev/null @@ -1,20 +0,0 @@ -// 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; -} diff --git a/src/ir/generics.zig b/src/ir/generics.zig index d10c47c3..00a922a9 100644 --- a/src/ir/generics.zig +++ b/src/ir/generics.zig @@ -284,6 +284,18 @@ pub const GenericResolver = struct { // tag. var scope = TypeBindingScope.enter(self.l, tmp_bindings); defer scope.exit(); + // Resolve the return type in the function's DEFINING module, exactly + // as `monomorphizeFunction` does — so a name in the return type (e.g. + // the error set of a value-failable `(… , !E)`) resolves to the SAME + // TypeId the instance's real signature uses, not whatever a re-export + // alias at the call site resolves it to. Without this pin a re-exported + // generic value-failable's `!E` resolved to a non-`.error_set` alias, + // so the planned call result was a plain tuple and `errorChannelOf` + // missed the failable channel (issue 0153). The binding-building above + // stays in the call-site context (its args are typed there). + const saved_src = self.l.current_source_file; + defer self.l.setCurrentSourceFile(saved_src); + if (fd.body.source_file) |src| self.l.setCurrentSourceFile(src); return self.l.resolveTypeWithBindings(fd.return_type.?); } };