Summary
When a function's error union solves to closed empty — every fallible call inside it is handled with match, so it infers Try(x, []) — and that function is the tail expression of a caller whose annotated error union is fatter, the TYPE MISMATCH diagnostic is very hard to act on:
- it highlights the caller's entire body (~40 lines in the real case) rather than the tail call that fails to unify;
- it prints
Try({}, []) with no explanation that [] is a closed empty union, or that closedness is what blocks unification with the fatter union;
- it gives no hint toward either fix (call the infallible function with
? / return Ok(...) explicitly, or stop returning Try from the infallible function at all).
The behavior appears correct under the rigid-extension-variable semantics — this issue is only about diagnostic quality.
Real-world error
From a ~500-line app (main! annotated Try({}, _); sweep_loop! annotated Try({}, _) whose body's errors are all match-handled, called as main!'s tail expression):
┌───────────────┐
│ TYPE MISMATCH ├─ This expression is used in an unexpected way. ─────────────┐
└┬──────────────┘ │
│ │
│ main! = |_args| { │
│ ... (the diagnostic reproduces main!'s entire ~40-line body) ... │
│ } │
│ │
└──────────────────────────────────────────────────────── main.roc:75:17 ┘
It has the type:
Try({}, [])
But the annotation says it should be:
Try({}, [Exit(d), MissingEnv(Str),
ReadTeammatesFailed([NoSuchField(Str), SqliteErr(Sqlite.ErrCode, Str),
UnexpectedType([Bytes, Integer, Null, Real, String, ..]), ..]),
StderrErr(IOErr), ..])
where [d.from_numeral : Numeral -> Try(d, [InvalidNumeral(Str)])]
Working out that the story was "sweep_loop!'s union solved to closed [], and a closed union can't widen into the annotation's union at the tail position" took substantial digging; the eventual fix was one line (sweep_loop!(...)? followed by Ok({}) — and ultimately dropping Try from the infallible function's signature entirely).
Repro
Full reproduction (roc release-fast-c9147c28):
- Check out
ESRogs/zulr at commit 69a9217 (needs the basic-cli clone the app's platform path points at)
- In
roc/dispatcher/main.roc, replace the two lines sweep_loop!({ bots, statuses, cooldowns: [], sweep: 1 })? / Ok({}) with the single tail expression sweep_loop!({ bots, statuses, cooldowns: [], sweep: 1 })
roc check roc/dispatcher/main.roc → the error above
Note on minimization: small reproductions of the apparent shape do not reproduce it — a self-recursive effectful f : U64 => Try({}, _) with all errors handled, called as the tail of an annotated main!, checks cleanly (the union stays flexible and widens fine), as does a chain of two _-annotated effectful functions joined by ?. Whatever closes the union to [] needs more context than those shapes — which may itself be a useful clue.
Suggested improvements
Any of these would have shortened the investigation from hours to seconds:
- Anchor the highlight on the tail expression whose type failed to unify, not the whole function body.
- When the mismatched side is a closed union, say so: "this union is closed (it lists every tag it can be), so it cannot pick up the extra tags the annotation allows."
- When the closed union is empty, consider a dedicated hint: "this expression can never fail; if the surrounding function is annotated to return errors, consider
expr? followed by Ok(...), or removing Try from the callee's type."
Staged in this fork for review before filing upstream (roc-lang/roc).
Summary
When a function's error union solves to closed empty — every fallible call inside it is handled with
match, so it infersTry(x, [])— and that function is the tail expression of a caller whose annotated error union is fatter, the TYPE MISMATCH diagnostic is very hard to act on:Try({}, [])with no explanation that[]is a closed empty union, or that closedness is what blocks unification with the fatter union;?/ returnOk(...)explicitly, or stop returningTryfrom the infallible function at all).The behavior appears correct under the rigid-extension-variable semantics — this issue is only about diagnostic quality.
Real-world error
From a ~500-line app (
main!annotatedTry({}, _);sweep_loop!annotatedTry({}, _)whose body's errors are all match-handled, called asmain!'s tail expression):Working out that the story was "
sweep_loop!'s union solved to closed[], and a closed union can't widen into the annotation's union at the tail position" took substantial digging; the eventual fix was one line (sweep_loop!(...)?followed byOk({})— and ultimately droppingTryfrom the infallible function's signature entirely).Repro
Full reproduction (roc
release-fast-c9147c28):ESRogs/zulrat commit69a9217(needs the basic-cli clone the app's platform path points at)roc/dispatcher/main.roc, replace the two linessweep_loop!({ bots, statuses, cooldowns: [], sweep: 1 })?/Ok({})with the single tail expressionsweep_loop!({ bots, statuses, cooldowns: [], sweep: 1 })roc check roc/dispatcher/main.roc→ the error aboveNote on minimization: small reproductions of the apparent shape do not reproduce it — a self-recursive effectful
f : U64 => Try({}, _)with all errors handled, called as the tail of an annotatedmain!, checks cleanly (the union stays flexible and widens fine), as does a chain of two_-annotated effectful functions joined by?. Whatever closes the union to[]needs more context than those shapes — which may itself be a useful clue.Suggested improvements
Any of these would have shortened the investigation from hours to seconds:
expr?followed byOk(...), or removingTryfrom the callee's type."Staged in this fork for review before filing upstream (roc-lang/roc).