Skip to content

Fix with tuple only processing first binding in build_with_dispose#5077

Open
cuiweixie wants to merge 2 commits intoponylang:mainfrom
cuiweixie:fix/with-tuple-dispose-all-bindings
Open

Fix with tuple only processing first binding in build_with_dispose#5077
cuiweixie wants to merge 2 commits intoponylang:mainfrom
cuiweixie:fix/with-tuple-dispose-all-bindings

Conversation

@cuiweixie
Copy link
Copy Markdown
Contributor

Summary

The loop in build_with_dispose returned on the first recursive call, so only the first tuple element was processed. Later _ patterns were not diagnosed as errors, and dispose calls were only added for the first binding.

Changes

  • Fix the tuple branch to iterate all elements and propagate failure.
  • Add WithTest.DontcareInTupleSecondBindingIsRejected in test/libponyc/with.cc.

Merge commit message

Fix with tuple only processing first binding in build_with_dispose

The loop returned on the first recursive call, so later tuple elements
were skipped: `_` in a later position was not rejected, and dispose
calls were only emitted for the first binding.

Walk every element and propagate failure. Add a libponyc regression test.

The loop returned on the first recursive call, so later tuple elements
were skipped: `_` in a later position was not rejected, and dispose
calls were only emitted for the first binding.

Walk every element and propagate failure. Add a libponyc regression test.
@ponylang-main ponylang-main added the discuss during sync Should be discussed during an upcoming sync label Mar 27, 2026
@SeanTAllen SeanTAllen added the changelog - fixed Automatically add "Fixed" CHANGELOG entry on merge label Mar 27, 2026
@ponylang-main
Copy link
Copy Markdown
Contributor

Hi @cuiweixie,

The changelog - fixed label was added to this pull request; all PRs with a changelog label need to have release notes included as part of the PR. If you haven't added release notes already, please do.

Release notes are added by creating a uniquely named file in the .release-notes directory. We suggest you call the file 5077.md to match the number of this pull request.

The basic format of the release notes (using markdown) should be:

## Title

End user description of changes, why it's important,
problems it solves etc.

If a breaking change, make sure to include 1 or more
examples what code would look like prior to this change
and how to update it to work after this change.

Thanks.

@SeanTAllen
Copy link
Copy Markdown
Member

SeanTAllen commented Mar 27, 2026

The test covers the dontcare diagnostic, but the other half of the bug (dispose calls only emitted for the first binding) doesn't have coverage. A compile-success test with a valid two-element tuple with block would close that gap. Something like with (a, b) = (D.create(), D.create()) do None end compiling cleanly proves the loop runs all the way through on the happy path too.

@SeanTAllen SeanTAllen removed the discuss during sync Should be discussed during an upcoming sync label Mar 27, 2026
- Use single-statement if style in build_with_dispose tuple loop
- Add TwoElementTupleWithCompiles compile-success regression test
- Add .release-notes/5077.md for changelog
Comment on lines +1 to +5
## Fix `with` tuple: process every binding in dispose generation

The compiler walked only the first element of a tuple pattern in `with` when building dispose calls and checking for invalid `_` bindings. Later elements were skipped, so some programs compiled incorrectly (missing dispose for later bindings, or no error for `_` in a later position).

Tuple patterns now walk every element. Programs using multi-binding `with` get correct cleanup and diagnostics.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you update this to show an example of pony code that failed due to the bug? we like to show an example of what previously didn't work but does now in our release notes. thanks.

ast_t* let = ast_child(p);
return build_with_dispose(opt, dispose_clause, let);
if(!build_with_dispose(opt, dispose_clause, let))
return false;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I didn't catch this in my first pass. This should use the error accumulation pattern instead of early return. There's a precedent in sugar_match_capture around line 690 in this same file:

bool r = true;
for(ast_t* p = ast_child(idseq); p != NULL; p = ast_sibling(p))
{
  pony_assert(ast_id(p) == TK_SEQ);
  ast_t* let = ast_child(p);
  if(!build_with_dispose(opt, dispose_clause, let))
    r = false;
}
return r;

With early return, with (_, _) = ... only reports the first error. The user has to fix, recompile, discover the next one. The accumulation pattern reports all of them at once. Same fix, better UX.

" new create(env: Env) =>\n"
" with (a, _) = (D.create(), D.create()) do\n"
" None\n"
" end";
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you switch to the accumulation pattern, add a test for (_, _) that expects two errors. That's the case where early return hides the second diagnostic.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants