Skip to content

Add Queue.isEmpty/poll refinement#1603

Open
nhioe wants to merge 4 commits intoeisop:masterfrom
nhioe:queue-is-empty-poll-refinement
Open

Add Queue.isEmpty/poll refinement#1603
nhioe wants to merge 4 commits intoeisop:masterfrom
nhioe:queue-is-empty-poll-refinement

Conversation

@nhioe
Copy link
Copy Markdown

@nhioe nhioe commented Mar 23, 2026

Fixes false positives in Queue.poll() by refining return type based on isEmpty checks.

Checker should know that the polled element is @NonNull if:

  • isEmpty returns false
  • The Queue element type is @NonNull

Existing test case in isEmptyPoll.java:

// should no longer raise any errors
  void mNonNull(Queue<String> q) {
      while (!q.isEmpty()) {
          @NonNull String firstNode = q.poll();
      }
  }

@nhioe nhioe marked this pull request as draft March 23, 2026 16:15
@nhioe nhioe force-pushed the queue-is-empty-poll-refinement branch from 78742c6 to 1ccfb9d Compare March 23, 2026 17:55
@nhioe nhioe marked this pull request as ready for review March 23, 2026 19:23
Copy link
Copy Markdown
Member

@wmdietl wmdietl left a comment

Choose a reason for hiding this comment

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

This looks great!
Please also add a note to the changelog.


void mNonNull(Queue<String> q) {
while (!q.isEmpty()) {
@NonNull String firstNode = q.poll();
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.

Would it make sense to add a second statement in the loop body: firstNode = q.poll()? The second call should fail, right?

Maybe in a separate test, have while(...) { q.clear(); firstNode = q.poll(); } or something like that, to test other side-effecting methods. I'm a bit afraid that setting the receiver to NONNULL will not be cleared out by such an operation.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thank you for catching these. You're right, currently the implementation only accommodates the simplest path and will fail on those test cases, as NONNULL will persist across mutations.

I plan to clear the NONNULL on known side-effecting methods that may invalidate our guarantee (clear, remove, poll, etc). However, I imagine this could get complicated with arbitrary methods (e.g. some foo is called that mutates the queue)?

Is it reasonable to make the fix for known side-effecting methods and document this limitation?

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.

I think we should look for a solution that works with the existing handling of side-effects.
As extra test, please add something with two separate queues, to ensure that while (!q1.isEmpty()) { q2.clear(); firstNode = q1.poll(); } also works correctly.

}
}

// Handle Collection.isEmpty(), mark receiver as non-empty in the false branch
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.

Suggested change
// Handle Collection.isEmpty(), mark receiver as non-empty in the false branch
// Handle Collection.isEmpty(): mark receiver as non-empty in the false branch.

The sentence read a bit odd. Documentation comments should still end with a period.

You need to explain somewhere how you mark the receiver as non-empty. The body adds NONNULL, which seems a bit odd - the receiver for any method invocation should already be non-null. So how is this conveying any information you can test for below?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Semantically, marking it NONNULL doesn't map perfectly to collection state, but it acts as a signal to poll() about what we learned from isEmpty, so we can refine the return type. Is a custom annotation for non-empty worth exploring?

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.

Could you add something about poll() to the Store? Check the conditions here and set the return type of poll() to non-null here? Hopefully such a fact would automatically removed on side effects.

Comment on lines +497 to +498
// be non-empty
// and Queue element type is @NonNull
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.

Suggested change
// be non-empty
// and Queue element type is @NonNull
// be non-empty, and the Queue element type is @NonNull.

I like the Oxford comma.

Copy link
Copy Markdown
Member

@wmdietl wmdietl left a comment

Choose a reason for hiding this comment

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

Thanks! I just looked at the expanded test and they look great! One comment about aliasing that you should think about and possibly fix. I'll look at the code soon.


void unrelatedClear(Queue<String> q1, Queue<String> q2) {
while (!q1.isEmpty()) {
q2.clear();
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.

You do not know whether q1 == q2 holds or not. So to be conservative, we need to drop the refinement.
This is related to the missing warning in test aliasClearBeforePoll.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I see. I assume I'll need to conservatively drop all queue non-empty refinements queue-related refinements in the store whenever a side-effecting method is called.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants