Experiment: ? re-raises errors (open-row widening at the desugar), replacing hosted try-question widening - #10366
Draft
jaredramirez wants to merge 1 commit into
Draft
Experiment: ? re-raises errors (open-row widening at the desugar), replacing hosted try-question widening#10366jaredramirez wants to merge 1 commit into
? re-raises errors (open-row widening at the desugar), replacing hosted try-question widening#10366jaredramirez wants to merge 1 commit into
Conversation
… try-question widening Desugar the Err branch of ? to re-tag the error payload through a new CIR node, e_reraise_err (conceptually the identity map_err). Checking gives the node a fresh open copy of the operand's closed tag-union row, so a callee's closed error row composes with the enclosing return row (flips issue #9798 to accepted, per the rewritten design.md rule 'Try Question Error Re-raise'). Monotype lowering compiles it to a re-tag match (identity when rows agree). The node subsumes the hosted-try-question-widening special case: the solver redirect (RedirectRule.hosted_try_question_widening), its gating probes, and the generated hosted adapters in monotype lowering are all deleted; hosted callees now always specialize at their declared ABI row, enforced by an invariant. Net -118 lines. Bumps the module-cache and checked-artifact serialization versions (new CIR node changes both layouts). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019T2TVkkH3TScCj9tV1GB2m
jaredramirez
force-pushed
the
try-question-reraise
branch
from
July 25, 2026 21:40
d48f5a5 to
a0a5ea2
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Experiment:
?re-raises errors (open-row widening at the desugar), replacing hosted try-question wideningDraft / experiment — this changes language semantics (flips issue #9798 from rejected-by-design to accepted) and exists to evaluate the "re-raising" alternative to polarity discussed in handling err tag accumulation.
What it does
Beginners keep hitting this: a callee's closed error row doesn't compose with the enclosing function's wider error row through
?:This PR makes
?re-raise the error instead of passing it through: the desugaredErrbranch wraps the outgoing payload in a new CIR node,e_reraise_err— conceptually the identitymap_err, destructuring each tag and re-constructing it at a fresh open row.addTryReturnErrwraps the payload ine_reraise_err. Covers suffix?and thelhs ? handlerbinop (any?condition shape, includingx?on a stored binding — the node lives in the desugar, not on calls).lowerReraiseErr— the identity when source/target monotypes are equal, otherwise a generated match that re-tags each tag at the target row's layout (rows can differ in representation, so this is a real coercion, done exactly once at the?site).It subsumes the hosted special case
The compiler already carried exactly this mechanism, gated to hosted functions: the
hosted_try_question_wideningsolver redirect plus generated hosted adapters in monotype lowering (a hosted callee's ABI row is fixed, so the widening had to happen at the use site). The re-raise node does the same thing for every callee, so all of that is deleted:widenTryConditionForExpectedReturn,tryConditionIsDirectHostedCall, and the whole inclusion-probe cluster inCheck.zig(9 functions)RedirectRule.hosted_try_question_wideninghostedUseNeedsTryAdapter,hostedTryAdapterBody,tryReturnInjectionExpr,errorRowInjectionExpr, and friends in monotype lowering (7 functions), replaced by an invariant that hosted specialization requests always arrive at the declared ABI typeNet diff: −118 lines including the new tests. Hosted
?flows through the same path as everything else (test/fx-open/issue_9963_hosted_try_question_mark.rocstill passes; the hosted callee stays specialized at its declared row, which is what the adapter existed to guarantee).Pros
my_thing()?— that now Just Works, for closed→wider-closed and closed→open rows alike.[Negative]in error messages, editor hover, or docs.dangerousSetVarRedirectinventory gets smaller; the checker no longer touches the callee's instantiated type at?sites, so callees always specialize at their declared types.?sites. Polarity can still be added later if the non-?cases prove painful; the reverse migration (removing polarity) would break code.?operator (viaFrom-conversion), not in the type system's variance rules — and that placement also leaves room to grow?into genuine error conversions later.Cons / gaps vs polarity
Only helps where a
?appears. Polarity widens output-position rows everywhere; re-raise doesn't cover:g = |x| if x > 100 Err(TooBig) else f(x)is still a type error (idiomatic fix:Ok(f(x)?), one character of ceremony).[f(1), h(2)]with two different closed error rows still needs open annotations on the producers.Arguably some of this is a feature — a closed annotation is an exhaustiveness promise, and polarity weakens it globally to fix what is overwhelmingly a
?-shaped complaint — but it is the real trade.Check-time resolution. The widening applies when the operand's row is resolved closed at the point the node is checked. A row that is still flexible there shares the var (today's behavior). Defs are checked in order, so in practice annotated/inferred callees are resolved by then, but this is order-sensitive in a way polarity is not. (Declared in the design.md rule; could be moved to a deferred constraint like literal defaulting if it ever bites.)
A runtime coercion exists. When the rows differ, the re-raise compiles to a re-tag match on the error path. It's the identity when rows are equal, and error paths are cold, but polarity-with-specialization would instead compile the callee at the wider row (paying compile-time/code-size there instead).
Testing
src/check/test/type_checking_integration.zig: issue ? rejects closed error union when returning into open error union #9798 flipped to accepted; accepted pins for closed→wider-closed (theNegative/TooBigexample above),?on a stored binding, a generic callee with a ground closed row, and thelhs ? handlerbinop widening the handler's closed row; rejected pins for a tag the return row cannot absorb and for shallow widening (a closed payload row does not widen).src/eval/test/eval_issue_tests.zig: three runtime cases —[Negative]→[Negative, TooBig](different representations, exercises the real re-tag on all executors), a generic callee specialized at its declared row with the re-tag at the use site, and payload-carrying re-raise into an open row.test/fx-open/: hosted accepted side (issue_9963_hosted_try_question_mark.roc) re-run viarun-test-clion interpreter + dev backends, green; hosted rejected side (hosted_try_question_not_included.roc) still a type error. Fixture comments updated to cite the new rule.zig build run-test-zig(full unit suite) andzig build run-test-eval(multi-backend, exercises the re-tag on the interpreter and compiled backends) green locally, except three failures that also fail on plainmainon this machine (json decoder allocation-count, fx boxed-erased host boundary x2 — unrelated to?; CI should arbitrate).Constants.CACHE_VERSIONand the checked-artifactserialized_layout_versionare bumped with fresh golden hashes.🤖 Generated with Claude Code
https://claude.ai/code/session_019T2TVkkH3TScCj9tV1GB2m