Skip to content

Fix compiler crash on partial application of method with literal-call default#5343

Draft
SeanTAllen wants to merge 2 commits into
mainfrom
fix-2480-literal-default-partial-app
Draft

Fix compiler crash on partial application of method with literal-call default#5343
SeanTAllen wants to merge 2 commits into
mainfrom
fix-2480-literal-default-partial-app

Conversation

@SeanTAllen
Copy link
Copy Markdown
Member

@SeanTAllen SeanTAllen commented May 16, 2026

Summary

Partially applying a method whose default desugars to a single call on a literal (e.g. USize = -1(1).neg()) crashed the compiler with an assertion in lookup_base. The duplicated default got re-walked after pass flags were reset on the lifted lambda's anonymous class, and make_literal_type clobbered the previously-coerced concrete type. Fix is a guard inside make_literal_type so it doesn't overwrite an already-set type (except TK_INFERTYPE, which is the explicit "please re-type me" marker that lookup's on-demand default-arg typing uses).

This is a targeted fix at the symptom site rather than a principled fix at the ast_resetpass-then-rewalk cycle that creates the inconsistent state. The first commit's message explains the trade-off. The follow-up commit centralizes the guard (it now covers all five callers of make_literal_type, not just the TK_INT/TK_FLOAT case in pass_expr) and adds pointer comments at the two sites that create the hazard.

Closes #2480
Closes #3727

Related

The fix doesn't cover all sibling crashes. Nested literal-method-call defaults like USize = (-1).abs() still crash with a different assertion (uifset:484). Filed as #5342.

… default

Partially applying a method whose default desugars to a single call on a
literal (the canonical example: `USize = -1`, which sugar rewrites as
`(1).neg()`) crashed the compiler with an internal assertion in
`lookup_base`. The mechanics:

`partial_application` duplicates the unsupplied parameter's default into
a synthesized lambda. `expr_object` then lifts that lambda to an
anonymous class and calls `ast_resetpass(def, PASS_SUGAR)`, wiping the
pass flags on the whole subtree — including the dup'd default, whose
literal had already been coerced to a concrete type. The expr pass
re-walks the literal and `make_literal_type` blindly overwrites the
coerced type back to `TK_LITERAL`. The surrounding nodes' types don't
get reset because pass_expr handlers short-circuit when a type is
already set. Verify pass later calls `lookup` on the funref's now-
literal-typed receiver and asserts.

I'm fixing this at the symptom: guard `make_literal_type` so it only
fires when the literal has no type yet. I don't think that's the most
principled fix — the real problem is the `ast_resetpass` + re-walk
cycle, and a cleaner approach would be to either not reset already-
processed dup'd subtrees, or audit every pass_expr handler so re-running
on coerced AST is genuinely idempotent. I picked the targeted fix
because chasing the principled one looks like a quagmire, and I'd
rather close the assertion now than dig a deeper hole trying to fix it
right.

Test is a full-program test rather than libponyc — the libponyc test
framework hits a pre-existing issue with `neg` lookup on `USize` during
on-demand default-arg typing, which is unrelated to #2480 but blocks a
clean unit test.

A nested literal-method-call default like `USize = (-1).abs()` still
crashes, in `uifset` rather than `lookup_base`. Same bug class, different
assertion site. Tracked in #5342.

Closes #2480
Closes #3727
@SeanTAllen SeanTAllen added the changelog - fixed Automatically add "Fixed" CHANGELOG entry on merge label May 16, 2026
@ponylang-main ponylang-main added the discuss during sync Should be discussed during an upcoming sync label May 16, 2026
The earlier fix put the "don't overwrite a coerced type" guard at one
of make_literal_type's five callers. Same hazard applies to the other
four — anywhere the dup'd-then-re-walked subtree happens to be an array
literal, tuple, recover block, or operator-literal call. Moving the
guard inside make_literal_type covers all of them in one place. The
guard still honours TK_INFERTYPE, which `lookup`'s on-demand default-arg
typing sets when it wants a re-type.

Also added pointer comments at the two sites that create the hazard —
`ast_resetpass` in expr_object and the `TREE(p_default)` dup in
partial_application. A maintainer changing either of those should not
have to grep for the consequence in expr/literal.c.
@SeanTAllen
Copy link
Copy Markdown
Member Author

Opening this PR was an exploration — I wanted to see what a targeted fix looked like before committing to it. With #5342 surfacing as a sibling crash this approach doesn't address, I'm reading it as a strong signal we're playing whack-a-mole. Each new symptom in this bug class will need its own guard, and the underlying re-walk-on-dup'd-AST problem stays put.

I think we need to talk about a more principled fix. Probably looks like restructuring the partial-application path so it doesn't re-walk subtrees that came from already-processed code — partial_application in expr/call.c, expr_lambda / expr_object in expr/lambda.c, and the on-demand expr typing in lookup_nominal all assume each other's re-walks are safe, and they aren't. Not small surgery, but bounded.

Holding this PR pending that conversation.

@SeanTAllen SeanTAllen added the do not merge This PR should not be merged at this time label May 16, 2026
@SeanTAllen SeanTAllen marked this pull request as draft May 16, 2026 18:50
@SeanTAllen SeanTAllen removed the discuss during sync Should be discussed during an upcoming sync label May 27, 2026
@jemc
Copy link
Copy Markdown
Member

jemc commented May 27, 2026

I think the right fix here would be to update the lexer/parser area to handle negative numbers as a parse-time evaluation rather than sugaring to (1).neg()

@ponylang-main ponylang-main added the discuss during sync Should be discussed during an upcoming sync label May 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog - fixed Automatically add "Fixed" CHANGELOG entry on merge discuss during sync Should be discussed during an upcoming sync do not merge This PR should not be merged at this time

Projects

None yet

3 participants