Skip to content

fix Cannot infer set type from usage #2693#2785

Open
asukaminato0721 wants to merge 1 commit intofacebook:mainfrom
asukaminato0721:2693
Open

fix Cannot infer set type from usage #2693#2785
asukaminato0721 wants to merge 1 commit intofacebook:mainfrom
asukaminato0721:2693

Conversation

@asukaminato0721
Copy link
Contributor

Summary

Fixes #2693

Adjusted first-use binding so an initial narrowing read no longer permanently disables deferred inference. In practice, if key not in seen: no longer forces seen off the first-use path, so the later seen.add(key) can infer set[str] as intended.

Test Plan

add test

@meta-cla meta-cla bot added the cla signed label Mar 12, 2026
@asukaminato0721 asukaminato0721 marked this pull request as ready for review March 12, 2026 18:32
Copilot AI review requested due to automatic review settings March 12, 2026 18:32
@github-actions

This comment has been minimized.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes a delayed/first-use inference edge case where an initial narrowing-style read (e.g. if key not in seen:) incorrectly disabled later inference from mutating uses (e.g. seen.add(key)), preventing set() from being inferred as set[str] in common dedup patterns.

Changes:

  • Adjusted deferred bound-name finalization so narrowing reads no longer consume/disable first-use inference.
  • Added a regression test covering set() inference inside a loop with an in membership check before .add(...).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
pyrefly/lib/binding/bindings.rs Updates first-use/deferred inference side-effects for narrowing reads during deferred bound-name finalization.
pyrefly/lib/test/delayed_inference.rs Adds a regression testcase ensuring seen = set() infers set[str] in the reported loop pattern.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines 1301 to 1305
} else {
// Narrowing or other reads: mark as DoesNotPin if still undetermined.
if matches!(first_use, FirstUse::Undetermined) {
self.mark_does_not_pin_if_first_use(def_idx);
}
// Narrowing reads should not pin partial types, but they also should not
// consume first-use inference. A later ordinary read may still determine the
// type, such as `if key not in seen: seen.add(key)`.
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

This else branch is intentionally a no-op but is an empty block (comment-only). If cargo clippy is run with clippy::all, this can trigger clippy::empty_else warnings. Consider removing the else block and moving the comment to a standalone comment after the if/else if chain (or otherwise ensuring the branch isn’t syntactically empty).

Copilot uses AI. Check for mistakes.
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions
Copy link

Diff from mypy_primer, showing the effect of this PR on open source code:

ibis (https://github.com/ibis-project/ibis)
+ ERROR ibis/expr/types/relations.py:514:12-18: Returned type `dict[object, ibis.expr.operations.core.Value[Unknown]]` is not assignable to declared return type `Mapping[str, ibis.expr.types.generic.Value]` [bad-return]

pwndbg (https://github.com/pwndbg/pwndbg)
- ERROR pwndbg/commands/vmmap.py:326:77-87: Object of class `NoneType` has no attribute `vaddr` [missing-attribute]
- ERROR pwndbg/commands/vmmap.py:328:29-39: Object of class `NoneType` has no attribute `vaddr` [missing-attribute]

apprise (https://github.com/caronc/apprise)
+ ERROR apprise/cli.py:1040:17-44: Object of class `int` has no attribute `append`
+ Object of class `str` has no attribute `append` [missing-attribute]
+ ERROR apprise/cli.py:1089:30-45: Type `int` is not iterable [not-iterable]
+ ERROR apprise/cli.py:1090:27-36: Object of class `str` has no attribute `url` [missing-attribute]
+ ERROR apprise/cli.py:1102:24-34: Object of class `str` has no attribute `tags` [missing-attribute]
+ ERROR apprise/cli.py:1104:66-78: No matching overload found for function `str.join` called with arguments: (object | set[Unknown] | Unknown) [no-matching-overload]
+ ERROR apprise/config/base.py:788:43-53: Argument `dict[object, set[Unknown]]` is not assignable to parameter `group_tags` with type `dict[str, set[str]]` in function `ConfigBase.__normalize_tag_groups` [bad-argument-type]
+ ERROR apprise/config/base.py:1300:43-53: Argument `dict[object | Unknown, set[object] | set[Unknown]]` is not assignable to parameter `group_tags` with type `dict[str, set[str]]` in function `ConfigBase.__normalize_tag_groups` [bad-argument-type]

cloud-init (https://github.com/canonical/cloud-init)
+ ERROR cloudinit/net/eni.py:442:9-40: Object of class `str` has no attribute `append` [missing-attribute]
+ ERROR cloudinit/netinfo.py:355:16-20: Returned type `dict[Unknown, dict[str, Unknown]] | dict[Unknown, dict[str, bool | list[Unknown] | str]] | dict[Unknown, Unknown] | Unknown` is not assignable to declared return type `dict[str, dict[str, Interface]]` [bad-return]
+ ERROR cloudinit/netinfo.py:376:12-16: Returned type `dict[Unknown, dict[str, Unknown]] | dict[Unknown, dict[str, bool | list[Unknown] | str]] | dict[Unknown, Unknown] | Unknown` is not assignable to declared return type `dict[str, dict[str, Interface]]` [bad-return]

rotki (https://github.com/rotki/rotki)
+ ERROR rotkehlchen/data_import/utils.py:75:21-53: Object of class `bool` has no attribute `append` [missing-attribute]

hydpy (https://github.com/hydpy-dev/hydpy)
+ ERROR hydpy/models/kinw/kinw_model.py:3505:38-40: Argument `list[Parameter | float | Unknown]` is not assignable to parameter `hvector` with type `Iterable[float]` in function `BaseModelProfile.calculate_qgvector` [bad-argument-type]

prefect (https://github.com/PrefectHQ/prefect)
+ ERROR src/prefect/server/models/block_schemas.py:575:21-85: Object of class `dict` has no attribute `append` [missing-attribute]
+ ERROR src/prefect/server/models/block_schemas.py:584:81-587:22: Cannot set item in `dict[str | None, dict[str, str]]` [unsupported-operation]

@github-actions
Copy link

Primer Diff Classification

❌ 6 regression(s) | ✅ 1 improvement(s) | 7 project(s) total | +14, -2 errors

6 regression(s) across ibis, apprise, cloud-init, rotki, hydpy, prefect. error kinds: missing-attribute, bad-return, bad-argument-type. caused by process_partial_type_read(). 1 improvement(s) across pwndbg.

Project Verdict Changes Error Kinds Root Cause
ibis ❌ Regression +1 bad-return pyrefly/lib/binding/bindings.rs
pwndbg ✅ Improvement -2 missing-attribute pyrefly/lib/binding/bindings.rs
apprise ❌ Regression +6 bad-argument-type, missing-attribute process_partial_type_read()
cloud-init ❌ Regression +3 bad-return, missing-attribute The change to bindings.rs that allows narrowing reads t...
rotki ❌ Regression +1 missing-attribute pyrefly/lib/binding/bindings.rs
hydpy ❌ Regression +1 bad-argument-type pyrefly/lib/binding/bindings.rs
prefect ❌ Regression +2 missing-attribute, unsupported-operation The change to bindings.rs that removed the `mark_does_n...
Detailed analysis

❌ Regression (6)

ibis (+1)

This is a regression. The code is correct - it builds a dict with string keys (from node.name) and ir.Value values (from unwrap_alias(node)), which is properly assignable to the declared return type Mapping[str, ir.Value]. The PR's changes to deferred type inference have caused pyrefly to incorrectly infer dict[object, Value[Unknown]] instead of the correct dict[str, Value]. This is a pyrefly-only false positive.
Attribution: The change in pyrefly/lib/binding/bindings.rs that removed the mark_does_not_pin_if_first_use call for narrowing reads appears to have affected dictionary type inference, causing pyrefly to lose track of types in the unwrap_aliases function

apprise (+6)

The PR aimed to fix set type inference but appears to have broken dictionary type inference in certain patterns. The code at line 1089 iterates over meta["plugins"] which was populated as a list at lines 1032/1040, but pyrefly now incorrectly infers it as int. Similarly, entry should be a plugin object with url() and tags attributes, but pyrefly infers it as str. These are false positives - regression.
Attribution: The change to BindingsBuilder::[process_partial_type_read()](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/binding/bindings.rs) in pyrefly/lib/binding/bindings.rs removed the call to mark_does_not_pin_if_first_use() for narrowing reads, which appears to have broken type inference for dictionaries/sets that are populated conditionally inside loops

cloud-init (+3)

These appear to be regressions. The first two errors show pyrefly inferring overly complex union types with Unknown instead of the declared return types. This suggests the PR's change to first-use binding has made type inference less precise in some cases. The third error about str.append() needs source investigation but appears to be a false positive since the code likely initializes the variable as a list elsewhere. The PR was meant to fix set inference but seems to have degraded inference quality for other patterns.
Attribution: The change to bindings.rs that allows narrowing reads to not consume first-use inference likely caused pyrefly to infer different (more complex) types in these cases, particularly for the netinfo.py errors involving dict[Unknown, ...] unions

rotki (+1)

This is a regression. The code is correct - grouped_msgs[msg]['rows'] is initialized as a list [row] when the key is first added (line 71), so it will always be a list when accessed in the else branch (line 75). Pyrefly incorrectly infers it as bool, likely due to the PR's changes to how narrowing reads (if msg not in grouped_msgs:) affect type inference. This is a false positive that neither mypy nor pyright report.
Attribution: The changes to narrowing read handling in pyrefly/lib/binding/bindings.rs (specifically removing the mark_does_not_pin_if_first_use call for narrowing reads) likely caused pyrefly to lose type information for dictionary values accessed after if key not in dict: checks

hydpy (+1)

This is a regression. The error is pyrefly-only and appears to be a false positive. The Parameter type from hydpy is likely float-compatible (probably has __float__ or numeric protocols), and the presence of Unknown in the union suggests incomplete type inference. Since mypy and pyright don't flag this, and the code likely works at runtime, pyrefly is being overly strict here.
Attribution: The change to preserve first-use inference through narrowing reads in pyrefly/lib/binding/bindings.rs is causing pyrefly to maintain stricter type information about list contents, leading to this new error

prefect (+2)

This is a regression. The code is correct - it implements a common pattern where dictionary values start as single items and get converted to lists when multiple items need to be stored. Pyrefly is incorrectly inferring the dictionary type as dict[str | None, dict[str, str]] instead of recognizing that values can be either dict[str, str] or list[dict[str, str]]. The PR's change to preserve first-use inference through narrowing reads has broken this type inference pattern.
Attribution: The change to bindings.rs that removed the mark_does_not_pin_if_first_use call for narrowing reads broke type inference for this dictionary mutation pattern. The narrowing check if name not in ... is no longer allowing proper inference of the dictionary's value type.

✅ Improvement (1)

pwndbg (-2)

These were false positive errors. The code has a None check on line 309 (if page is not None and page.in_darwin_shared_cache and not expand_shared_cache) that ensures page is not None before the loop continues. Due to the continue statement on line 314, execution only reaches lines 326/328 when page is not None. The PR fixed pyrefly's handling of narrowing reads in type inference, allowing it to correctly understand that page cannot be None at the attribute access locations.
Attribution: The change to narrowing read handling in pyrefly/lib/binding/bindings.rs fixed the type inference. Previously, the narrowing check if page is not None was preventing pyrefly from properly inferring that page cannot be None in the subsequent code paths.

Suggested fixes

Summary: The PR's change to preserve first-use inference through narrowing reads broke dictionary type inference for conditional mutation patterns.

1. In process_partial_type_read() in pyrefly/lib/binding/bindings.rs, restore the call to mark_does_not_pin_if_first_use() for narrowing reads when the container is being mutated in conditional branches. Add a check: if the narrowing read is followed by a mutation (like dict[key] = value or set.add()), then mark it as DoesNotPin to allow proper type inference.

Files: pyrefly/lib/binding/bindings.rs
Confidence: high
Affected projects: ibis, apprise, cloud-init, rotki, hydpy, prefect
Fixes: bad-assignment, bad-attribute, bad-argument
The removed mark_does_not_pin_if_first_use() call was necessary for proper type inference in conditional mutation patterns. Without it, pyrefly maintains overly strict/incorrect types for dictionary values and set contents when they're populated inside if-not-in checks. Expected outcome: eliminates 14 errors across 6 projects


Was this helpful? React with 👍 or 👎

Classification by primer-classifier (7 LLM)

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cannot infer set type from usage

2 participants