issue 0153 RESOLVED: pin generic return-type resolution to the fn's defining module

inferGenericReturnType resolved a 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 NOT tagged .error_set, so the planned result was a tuple whose last
field wasn't an error set — errorChannelOf saw a plain tuple and the value-
failable's ! channel was lost (try/or rejected it / built a malformed i1 PHI).

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 disagreed with the instance's real signature. Fix:
pin the source to fd.body.source_file around the return-type resolution
(binding-build stays in the call-site context — its args are typed there).

Regression test examples/1058-errors-reexport-value-failable-channel.sx
(+ companion lib.sx). Suite green 732/0.
This commit is contained in:
agra
2026-06-21 05:54:52 +03:00
parent a7499d5f51
commit 68c1991e11
8 changed files with 67 additions and 20 deletions

View File

@@ -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;
}

View File

@@ -0,0 +1 @@
r=42

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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.?);
}
};