[typer] error reporting vs call arguments#12935
Conversation
Documents several error-reporting issues when a call argument (often a function literal) fails to type. The .stderr files hold the expected (correct) output, so the still-broken cases are red until fixed and the fixed cases stay green. Failing (known issues, expected output is the tentative correct one): - Swallow: a real error that is a side effect of typing an argument (module load) is silently dropped when the argument is skipped to a later optional parameter. - Cascade*: a block-bodied function literal with an inner error reports the real error plus a spurious "Void should be Int" unification error (array/var/return variants reproduce on development; the call-argument variant on this branch). - MaskStruct*: when an argument is skipped past an earlier optional and then fails against the intended one, only the first (irrelevant) mismatch is reported, hiding the real "Object requires field y". Reproduces on development. Passing (guard behavior fixed on this branch): - MaskBody*: a non-return unification error inside the body, fully masked on development, is now reported with the real "String should be Int". - DuplicateReport: an error reported twice on development (skip + re-type) is now reported once. See #5837, #10634.
…tion (#10634, #7924) When typing a call argument, errors that occur inside the body of a function literal were attributed to the enclosing call argument ("For function argument 'x'"), and in diagnostics mode an unresolved identifier produced a false positive instead of letting optional-argument skipping / overload resolution recover. Four related changes fix this: - type_local_function resets in_call_args while typing the body, so body errors are reported in place instead of being re-raised and wrapped by arg_error (kept during overload resolution, where a body error must reject the candidate). - type_block recovers a failed value-position expression as a fresh monomorph instead of dropping it to Void, avoiding a spurious cascade ("Void should be Int") against the expected type. - type_ident raises for an unresolved identifier while in_call_args even in diagnostics mode, so the skip / overload machinery can recover (no false positive). - unify_call_args rolls back the function-literal body errors of an argument that is skipped to a later parameter (re-emitted when it binds), so they are not reported twice, while preserving once-only side-effect errors (e.g. module load).
…ents (#12934) A block-bodied function literal recovers body errors per-statement inside type_block, but an expression body (e.g. `() -> x` / `function() x`) let the error propagate out of type_function. When the function literal is a call argument, that propagated error was caught as a signature failure, which then reported a stale skipped-argument error and masked the real one: foo(0, TInt, function() x) // wrongly: "Unknown identifier : TInt" for ?v Now a call-argument function literal recovers its body error in place (committed while in_call_args is reset), so the argument binds normally and the real error is reported at its own position (here the unresolved `x`), letting `TInt` resolve against the later Type.ValueType parameter.
…ion-literal tests Record the rollback set as the message prefix newer than messages_before, bailing out if that boundary is no longer a tail (a nested call may have rolled messages back via List.filter) so we never roll back unrelated messages. Tests cover multiple function-literal arguments and a nested skipping call: - MultiBind: two function-literal args, both bodies error -> both reported in place. - NestedSkip: a function-literal body containing a skipping call whose argument body errors -> each error reported once, in place. - MultiNoSkip: no skip room, the first function literal cannot fit ?n:Int.
…after a body error When a function literal whose body already reported error(s) in place is passed to a non-function parameter (e.g. `() -> err` against `?n:Int`), it can never fit, so the trailing signature mismatch is pure noise. Suppress it and recover (default the parameter) so later arguments are still checked. A function-vs-function mismatch is a real signature error and is kept (see SignatureKept). Updates MultiNoSkip (now reports both body errors) and adds SignatureKept.
|
Also fixes #12910 ! |
|
The analysis sounds good, but having How about we generalize |
Replace g.call_arg_body_messages and f.in_overload_call_args with a single g.call_arg_context option, Some only while unify_call_args resolves a call. Both moved fields share that exact lifetime, so the manual save/restore and cross-file raw mutation collapse into enter_call_args plus a few accessors. in_call_args stays a separate bool: it is additionally toggled off while typing function-literal bodies and display sub-expressions, a finer lifetime than the call_arg_context. delayed_display is left untouched.
d4c6f74 should make it better The reviewer's instinct is half right The smell is real for one of these values: But the proposed consolidation fights the actual lifetimesThe suggestion assumes
In other words, there are genuinely two concepts here, not one:
Merging across that boundary is what makes it awkward. What I'd actually doThe cleanup that does pay off, and stays within the spirit of the review, is narrower: group the g-level accumulator(s) into a record that's I'd leave So my reply to the reviewer would be: agree on the smell, but the right move is to encapsulate the call-resolution accumulator lifetime (helpers + a small record on |
|
Looks better indeed, but I have to push back on this because one of us is confused about something here:
The call to
But there is also Please evaluate this part again and clarify these points. |
|
Trying that introduced some regression (CI run -- ignore the 5522 failure). The reviewer is right that my original justification was wrong — the restores at But folding
So the two values belong in different places because they have different scoping:
|
Committing an error sets the sticky part_scope.has_error flag in addition to appending the message. When a function-literal call argument is skipped to a later parameter, we roll back the body errors from the abandoned attempt by filtering them out of part_scope.messages — but has_error stayed set, so the compiler exited with an error status and nothing to print (#5522). Add Common.rollback_messages, which drops the messages and recomputes has_error from the survivors, and use it in unify_call_args.
|
The problem with global values like this is that the typing order is undefined. For example, while typing a call argument we might trigger the resolution of a To be honest I'm having a hard time following the reasoning here. It already starts with the mention of "sub-context" because I'm not sure what is meant by that. Looking at the test for #9067, the real problem appears to be that we cannot distinguish in which cases we should and shouldn't show the "For function argument" extra message. Surely the answer to that is that we should only show this message for a direct unification failure of the provided expression type against the expected argument type. Another concern I have is that this PR ignores overloads completely, which have exactly the same problems as optional arguments with regards to our "type this - actually never mind, ignore all these errors and type this instead" approach. A solution to this should be general enough to cover this as well. Sadly, I can't see a clean way of handling this as long as |
Better error reporting for function-body / call-argument errors
When a call argument fails to type — especially a function literal — errors were frequently reported in the wrong place or with the wrong message:
... For function argument 'x') instead of being shown at its own position;Void should be Intunification error on top of the real one;The fix
Four related changes in the typer:
type_local_functionresetsin_call_argswhile typing the body, so body errors are reported in place instead of being re-raised and wrapped byarg_error. It is kept set during overload resolution, where a body error must reject the candidate.type_local_functionadditionally recovers an expression body error of a call-argument function literal (e.g.() -> x/function() x). A block body already recovers per-statement insidetype_block, but an expression body would otherwise let the error propagate out and be mistaken for a signature failure of the enclosing call argument — which then reported a stale skipped-argument error and masked the real one (Invalid error with optional parameter #12934).type_blockrecovers a failed value-position expression as a fresh monomorph instead of dropping it toVoid, avoiding a spurious cascade (Void should be Int) against the expected type.type_identraises for an unresolved identifier whilein_call_argseven in diagnostics mode, so the skip / overload machinery can recover (no false positive).unify_call_argsrolls back the function-literal body errors of an argument that is skipped to a later parameter (they are re-emitted against the parameter it actually binds to), so they are not reported twice, while preserving once-only side-effect errors such as module loading.Examples
Test coverage
tests/misc/eval/projects/Issue10634/— call-argument / optional-skip scenarios (unknown ident, enum member, abstract cast, array access, var unify, optional skipping in both orders).tests/misc/eval/projects/Issue12934/— enum-value skip with a later function literal whose expression body has an error.tests/misc/eval/projects/function-body-error-reporting/— see below.tests/serverIssue10634,Issue7924(no false-positive diagnostics).Two existing tests lost the now-misleading
... For function argumentsuffix and had their expected output updated (improvements):Issue11450,Issue8488.function-body-error-reporting scenarios
Error swallowed by optional-argument skipping
Swallow: a real error that is a side effect of typing an argument (loading theDupmodule) must still be reported when the argument is skipped to a later optional parameter. Only function-literal body errors are rolled back on skip (they are re-emitted against the parameter the argument binds to); once-only side-effect errors like this one are preserved.Closes #5837
Spurious "Void should be Int" cascade
CascadeArray/CascadeVar/CascadeReturn/CascadeCallArg: a function literal whose body has an inner error must report only that error, not an additional unification error on the function's type. A failed value-position expression now recovers as a fresh monomorph instead of collapsing the body toVoid, so it no longer cascades against the expected type.Real error masked by the wrong skipped parameter (FAILS, pre-existing)
MaskStructReturn/MaskStructValue: the argument fails against an earlier optional parameter (?n:Int) and against the intended one (?f/?r), butunify_call_argsreports the first stored (irrelevant) mismatch and discards the real one — so only... should be Null<Int>is shown, hidingObject requires field y. Choosing the right parameter to blame depends on which one the argument best fits and cannot be done cleanly here (reporting the current parameter instead would regressIssue8283, where the skipped parameter is the intended one).MaskStructReturnadditionally suffers from an object-declaration double error. Reproduces ondevelopment; untouched by this fix.Those tests are added as disabled tests.
Non-return body error masked
MaskBodyVar/MaskBodyCall: a unification error inside the body, on a parameter inferred from the intended later parameter, used to be fully masked (shown as... should be Null<Int>for the earlier?n). It is now reported as the realString should be Intat its own position.Duplicated error on skip+retype
DuplicateReport: a body error used to be reported twice because the argument is typed once per skip candidate. The body errors of a skipped attempt are now rolled back and re-emitted only once, against the parameter the argument binds to.Multiple / nested function-literal arguments
MultiBind/NestedSkip/MultiNoSkipexercise more than one function literal in the same call:MultiBind(take(?f, ?g), both bodies error): both errors are reported at their own positions. Previously only the first was shown (wrapped asFor optional f) and the second was dropped.NestedSkip(a function-literal body containing a skipping call whose argument body errors): each error is reported once, in place, even though the inner argument is skipped to a later parameter.MultiNoSkip(take(?n:Int, ?f)called with two function literals): there is no room to skip, so the first literal cannot fit?n:Int. Because a function literal can never be anInt, the resulting signature mismatch is pure noise on top of the body error, so it is suppressed and typing recovers (defaulting the parameter) — so both body errors are reported in place.SignatureKept((x:Int) -> ...against?f:String->Void): a real function-vs-function signature mismatch is kept and reported alongside the body error; only the impossible function-vs-non-function case above is suppressed.Closes #5522
Closes #7197
Closes #7924
Closes #10634
Closes #12910
Closes #12934