Skip to content

fix map is confused by string literals #2854#2856

Draft
asukaminato0721 wants to merge 1 commit intofacebook:mainfrom
asukaminato0721:2854
Draft

fix map is confused by string literals #2854#2856
asukaminato0721 wants to merge 1 commit intofacebook:mainfrom
asukaminato0721:2854

Conversation

@asukaminato0721
Copy link
Contributor

Summary

Fixes #2854

starred list/set elements now infer their iterable type without a contextual hint first, and only retry with the hint when the unhinted result still contains partial placeholders.

That stops Iterable[LiteralString] from over-constraining nested calls like map(str, range(n)) while preserving empty-container inference.

Test Plan

add test

@meta-cla meta-cla bot added the cla signed label Mar 23, 2026
@github-actions

This comment has been minimized.

@asukaminato0721 asukaminato0721 marked this pull request as ready for review March 23, 2026 05:42
Copilot AI review requested due to automatic review settings March 23, 2026 05:42
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 an inference bug where LiteralString context from str.join(...) could incorrectly over-constrain nested starred iterables (e.g., *map(str, ...)) during list/set element inference.

Changes:

  • Update list/set starred-element inference to infer without a contextual hint first, and only retry with the hint when the unhinted result contains partial (placeholder) types.
  • Add a regression test reproducing issue #2854 involving ",".join([*genexpr, *map(str, range(n))]).

Reviewed changes

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

File Description
pyrefly/lib/alt/expr.rs Adjusts starred element inference in list/set literals to avoid premature contextual-hint over-constraint while preserving empty-container inference via a retry.
pyrefly/lib/test/literal.rs Adds a regression testcase ensuring the join + starred + map(str, ...) pattern no longer produces overload/type errors.

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

Comment on lines +1846 to +1851
let retry_with_hint = star_hint.as_ref().is_some()
&& unpacked_ty.any(|ty| self.solver().is_partial(ty));
if retry_with_hint {
unpacked_ty = self.expr_infer_with_hint_promote(
value,
star_hint.as_ref().map(|hint| hint.as_ref()),
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

star_hint is wrapped in a LazyCell, but star_hint.as_ref().is_some() forces it to be computed even when you don't retry with the hint. Consider checking elt_hint.is_some() (or similar) to decide whether a hint exists, and only evaluating star_hint when the retry path is actually taken, so the derived Iterable[...] hint construction stays lazy.

Suggested change
let retry_with_hint = star_hint.as_ref().is_some()
&& unpacked_ty.any(|ty| self.solver().is_partial(ty));
if retry_with_hint {
unpacked_ty = self.expr_infer_with_hint_promote(
value,
star_hint.as_ref().map(|hint| hint.as_ref()),
let retry_with_hint =
elt_hint.is_some() && unpacked_ty.any(|ty| self.solver().is_partial(ty));
if retry_with_hint {
let star_hint_ref = star_hint.force();
unpacked_ty = self.expr_infer_with_hint_promote(
value,
star_hint_ref.as_ref().map(|hint| hint.as_ref()),

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

This comment has been minimized.

@asukaminato0721 asukaminato0721 marked this pull request as draft March 23, 2026 06:56
@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/backends/sql/compilers/snowflake.py:220:35-81: Argument `str` is not assignable to parameter `object` with type `Literal['COMMENT = \'{comment}\'', 'CREATE OR REPLACE TEMPORARY FUNCTION {name}({signature})', 'IMMUTABLE', 'LANGUAGE PYTHON', 'RETURNS {return_type}', 'RUNTIME_VERSION = \'{version}\'']` in function `list.append` [bad-argument-type]
+ ERROR ibis/backends/sql/compilers/snowflake.py:225:31-55: Argument `str` is not assignable to parameter `object` with type `Literal['COMMENT = \'{comment}\'', 'CREATE OR REPLACE TEMPORARY FUNCTION {name}({signature})', 'IMMUTABLE', 'LANGUAGE PYTHON', 'RETURNS {return_type}', 'RUNTIME_VERSION = \'{version}\'']` in function `list.append` [bad-argument-type]

openlibrary (https://github.com/internetarchive/openlibrary)
- ERROR openlibrary/plugins/worksearch/code.py:320:13-19: Argument `list[tuple[str, SolrRequestLabel] | tuple[str, int | Unknown] | tuple[str, str] | tuple[str, Unknown]]` is not assignable to parameter `cur_solr_params` with type `list[tuple[str, str]]` in function `openlibrary.plugins.worksearch.schemes.SearchScheme.q_to_solr_params` [bad-argument-type]
+ ERROR openlibrary/plugins/worksearch/code.py:320:13-19: Argument `list[tuple[Literal['fq'], str] | tuple[str, SolrRequestLabel] | tuple[str, int | Unknown] | tuple[str, Unknown]]` is not assignable to parameter `cur_solr_params` with type `list[tuple[str, str]]` in function `openlibrary.plugins.worksearch.schemes.SearchScheme.q_to_solr_params` [bad-argument-type]

strawberry (https://github.com/strawberry-graphql/strawberry)
+ ERROR strawberry/codegen/query_codegen.py:887:26-44: `list[GraphQLField]` is not assignable to variable `fields` with type `list[GraphQLField | GraphQLFragmentSpread]` [bad-assignment]

pandas (https://github.com/pandas-dev/pandas)
- ERROR pandas/tests/indexes/multi/test_indexing.py:959:36-41: Argument `range` is not assignable to parameter `iterable` with type `Iterable[_NestedSequence[_SupportsArray[dtype]] | _SupportsArray[dtype]]` in function `list.__init__` [bad-argument-type]

core (https://github.com/home-assistant/core)
+ ERROR homeassistant/components/devolo_home_control/siren.py:54:38-59:10: `list[int]` is not assignable to attribute `_attr_available_tones` with type `dict[int, str] | list[int | str] | None` [bad-assignment]
+ ERROR homeassistant/components/fritz/switch.py:203:20-89: Returned type `list[FritzBoxWifiSwitch]` is not assignable to declared return type `list[Entity]` [bad-return]
+ ERROR homeassistant/components/fritz/switch.py:206:12-211:6: Returned type `list[FritzBoxDeflectionSwitch | FritzBoxPortSwitch | FritzBoxProfileSwitch | FritzBoxWifiSwitch]` is not assignable to declared return type `list[Entity]` [bad-return]
+ ERROR homeassistant/components/home_connect/sensor.py:514:12-532:6: Returned type `list[HomeConnectEventSensor | HomeConnectProgramSensor | HomeConnectSensor]` is not assignable to declared return type `list[HomeConnectEntity]` [bad-return]
+ ERROR homeassistant/components/number/const.py:499:80-614:2: `dict[NumberDeviceClass, set[UnitOfSpeed | UnitOfVolumetricFlux] | set[str | type[StrEnum] | None]]` is not assignable to `dict[NumberDeviceClass, set[str | type[StrEnum] | None]]` [bad-assignment]
+ ERROR homeassistant/components/sensor/const.py:594:80-709:2: `dict[SensorDeviceClass, set[UnitOfSpeed | UnitOfVolumetricFlux] | set[str | type[StrEnum] | None]]` is not assignable to `dict[SensorDeviceClass, set[str | type[StrEnum] | None]]` [bad-assignment]

sphinx (https://github.com/sphinx-doc/sphinx)
- ERROR sphinx/ext/autosummary/__init__.py:607:28-43: Argument `list[str]` is not assignable to parameter `iterable` with type `Iterable[LiteralString]` in function `list.__init__` [bad-argument-type]

django-modern-rest (https://github.com/wemake-services/django-modern-rest)
+ ERROR dmr/metadata.py:365:16-370:10: Returned type `list[type[AsyncAuth] | type[Parser] | type[Renderer] | type[SyncAuth] | type[ComponentParser]]` is not assignable to declared return type `list[type[ResponseSpecProvider]]` [bad-return]

materialize (https://github.com/MaterializeInc/materialize)
+ ERROR misc/python/materialize/parallel_workload/action.py:2191:44-75: `list[Table | View]` is not assignable to `list[DBObject]` [bad-assignment]
+ ERROR misc/python/materialize/parallel_workload/action.py:2220:44-75: `list[Table | View]` is not assignable to `list[DBObject]` [bad-assignment]

jax (https://github.com/google/jax)
+ ERROR jax/experimental/key_reuse/_core.py:389:33-66: `list[Var]` is not assignable to `list[Atom]` [bad-assignment]
- ERROR jax/experimental/mosaic/gpu/fragmented_array.py:4109:50-53: Argument `type[str]` is not assignable to parameter `func` with type `(object) -> LiteralString` in function `map.__new__` [bad-argument-type]

@github-actions
Copy link

Primer Diff Classification

❌ 4 regression(s) | ✅ 3 improvement(s) | ➖ 2 neutral | 9 project(s) total | +14, -4 errors

4 regression(s) across strawberry, core, django-modern-rest, materialize. error kinds: bad-assignment, bad-return: list invariance on starred unpacking, bad-assignment: list invariance on starred unpacking. 3 improvement(s) across ibis, pandas, sphinx.

Project Verdict Changes Error Kinds Root Cause
ibis ✅ Improvement +2 bad-argument-type pyrefly/lib/alt/expr.rs
openlibrary ➖ Neutral +1, -1 bad-argument-type
strawberry ❌ Regression +1 bad-assignment pyrefly/lib/alt/expr.rs
pandas ✅ Improvement -1 bad-argument-type pyrefly/lib/alt/expr.rs
core ❌ Regression +6 bad-return: list invariance on starred unpacking pyrefly/lib/alt/expr.rs
sphinx ✅ Improvement -1 bad-argument-type pyrefly/lib/alt/expr.rs
django-modern-rest ❌ Regression +1 bad-return pyrefly/lib/alt/expr.rs
materialize ❌ Regression +2 bad-assignment pyrefly/lib/alt/expr.rs
jax ➖ Neutral +1, -1 Removed false positive on map(str, ...) pyrefly/lib/alt/expr.rs
Detailed analysis

❌ Regression (4)

strawberry (+1)

This is a false positive introduced by the PR. The PR's optimization to skip contextual hints for starred elements when the unhinted type has no partials is too aggressive — it breaks standard bidirectional type inference for list literals. The expression [*sub_type.fields] should adopt the wider union type from the assignment target. Both mypy and pyright handle this correctly via contextual typing. The code is valid Python with no type error.
Attribution: The change to expr.rs in pyrefly/lib/alt/expr.rs modified starred element inference to first infer without the contextual hint and only retry with the hint when the unhinted result contains partial placeholders. Since sub_type.fields resolves to list[GraphQLField] (no partials), the hint list[GraphQLField | GraphQLFragmentSpread] is never applied, causing the list literal [*sub_type.fields] to be inferred as list[GraphQLField] instead of list[GraphQLField | GraphQLFragmentSpread].

core (+6)

bad-return: list invariance on starred unpacking: Functions returning list[Entity] with [*await func_returning_list_of_subclass()] now get inferred as a list of the specific subclass type(s) instead of list[Entity] because the contextual return type hint is no longer applied to starred elements in list literals. For example, at line 203, [*await _async_wifi_entities_list(...)] is inferred as list[FritzBoxWifiSwitch] instead of list[Entity]. At lines 206-211, the list with four starred unpacks from functions returning different subclass lists gets inferred with those specific subclass types rather than being widened to list[Entity]. Due to list invariance, list[FritzBoxWifiSwitch] is not assignable to list[Entity] even though FritzBoxWifiSwitch is a subclass of Entity. This is a false positive — the code is correct and mypy/pyright accept it.
bad-assignment: list invariance on starred unpacking: Assignments like self._attr_available_tones = [*range(...)] where the target type is dict[int, str] | list[int | str] | None now infer list[int] instead of using the hint to widen to list[int | str]. Due to list invariance, list[int] is not a subtype of list[int | str], so the assignment fails. Previously the contextual type hint would have guided the list literal's type. This is a false positive — the code is correct and mypy/pyright accept it.

Overall: The PR fixed one issue (LiteralString over-constraining map calls) but introduced a regression: starred elements in list literals no longer use the contextual type hint when the unhinted inference produces a concrete type. This breaks the standard pattern where list literals use the target type annotation to determine element types. All 6 errors are pyrefly-only false positives on valid Home Assistant code.

Attribution: The change in pyrefly/lib/alt/expr.rs to the starred element handling in list/set literals. The new code first calls self.expr_infer(value, errors) without the contextual hint, and only retries with the hint if the result contains partial placeholders. For concrete types like range(...) or _async_wifi_entities_list(...), the unhinted inference succeeds with a concrete (but too-narrow) type, so the hint is never applied. This causes the list literal to get a narrower element type than the declared target, triggering invariance violations.

django-modern-rest (+1)

This is a regression caused by the PR's change to starred element inference. The key issue is that list is invariant, so list[type[ComponentParser] | type[Parser] | type[Renderer] | type[SyncAuth] | type[AsyncAuth]] is not assignable to list[type[ResponseSpecProvider]] even if all those classes are subclasses of ResponseSpecProvider. Previously, the contextual hint list[type[ResponseSpecProvider]] guided inference so each starred element was typed as type[ResponseSpecProvider], making the return type match. After the PR, the hint is no longer applied (because the unhinted inference doesn't produce partial types), so the concrete types are inferred, creating an invariance mismatch.

Looking at the source code: self.component_parsers is list[ComponentParserSpec] where ComponentParserSpec = tuple[type['ComponentParser'], tuple[Any, ...]], so spec[0] yields type[ComponentParser]. self.parsers is dict[str, 'Parser'], so type(parser) yields type[Parser]. self.renderers is dict[str, 'Renderer'], so type(renderer) yields type[Renderer]. self.auth is list['SyncAuth | AsyncAuth'] | None, so type(auth) yields type[SyncAuth] | type[AsyncAuth] (or type[SyncAuth | AsyncAuth]).

The method response_spec_providers is declared to return list[type[ResponseSpecProvider]], and the code is clearly intended to return classes that implement the ResponseSpecProvider protocol. Whether ComponentParser, Parser, Renderer, SyncAuth, and AsyncAuth actually inherit from ResponseSpecProvider cannot be verified from the provided source alone, but the method's design strongly implies they do.

Even assuming all these classes are subclasses of ResponseSpecProvider, the error is still expected under strict type theory because list is invariant — list[type[Dog]] is not assignable to list[type[Animal]]. However, when constructing a fresh list literal that is immediately returned, type checkers like mypy and pyright typically use contextual typing to widen element types to match the declared return type, avoiding this spurious error.

This is a false positive because the code is correct — the list is being constructed fresh and immediately returned, so the invariance concern is purely theoretical. The previous behavior of using the contextual hint to widen the element types was producing better, more practical results. The PR broke this contextual typing path for starred elements, causing a spurious error that the user would need to suppress.

Attribution: The change in pyrefly/lib/alt/expr.rs modified how starred list/set elements infer their iterable type. Previously, starred elements always used the contextual hint for inference. Now, they first infer without a hint, and only retry with the hint if the unhinted result contains partial placeholders. This change affects the type inference of the starred expressions *[spec[0] for spec in self.component_parsers], *[type(parser) for parser in self.parsers.values()], etc. on lines 366-369. Without the contextual hint of list[type[ResponseSpecProvider]], each starred expression now infers its concrete element types independently (e.g., type[ComponentParser], type[Parser], etc.), and since none of these contain partial placeholders, the hint is never retried. The resulting union type list[type[AsyncAuth] | type[Parser] | type[Renderer] | type[SyncAuth] | type[ComponentParser]] is then checked against the declared return type list[type[ResponseSpecProvider]], and since list is invariant, this fails.

materialize (+2)

This is a regression. The PR changed starred element inference to first try without a contextual hint. For [*exe.db.tables, *exe.db.views] with annotation list[DBObject], the old behavior used the DBObject hint to infer each starred element as Iterable[DBObject], producing list[DBObject]. The new behavior infers list[Table] and list[View] without the hint, combines them to list[Table | View], and then flags the invariance violation when assigning to list[DBObject]. While list invariance is technically correct per the spec (https://typing.readthedocs.io/en/latest/spec/generics.html#variance), mypy and pyright both handle this by using the annotation to guide list display inference — they don't first infer a narrow type and then check assignability. The old pyrefly behavior was correct and matched mypy/pyright. The new behavior is too strict for this common pattern.
Attribution: The change in pyrefly/lib/alt/expr.rs modifies how starred elements in list/set displays are inferred. Previously, starred elements like *exe.db.tables and *exe.db.views were inferred with the contextual hint (list[DBObject]), which would have made their iterable types resolve to DBObject. After the PR, starred elements are first inferred WITHOUT the hint (producing Table and View respectively), and only retry with the hint if the unhinted result contains partial placeholders. Since list[Table] and list[View] are concrete (no partials), the hint is never applied, and the resulting list type becomes list[Table | View] instead of list[DBObject], triggering the invariance violation.

✅ Improvement (3)

ibis (+2)

This is a regression caused by the PR's change to starred element inference. The code at line 217 does preamble_lines = [*self._UDF_PREAMBLE_LINES] where _UDF_PREAMBLE_LINES is a tuple of 6 literal strings (line 188-195). With the new inference logic, pyrefly infers preamble_lines as list[Literal['COMMENT = ...', ...]] instead of list[str]. Then when preamble_lines.append(f"IMPORTS = ...") (line 220) and preamble_lines.append(f"PACKAGES = ...") (line 225) are called, pyrefly rejects the str argument because it doesn't match the narrow Literal[...] type. This is incorrect — the list was created by unpacking a tuple, and its element type should be widened to str (or at minimum, appending a str to a list[LiteralString] should widen the list). Neither mypy nor pyright flag this. The PR's intent was to stop Iterable[LiteralString] from over-constraining map(str, range(n)), but it introduced a side effect where starred unpacking of literal-string tuples now produces overly narrow list types.
Attribution: The change in pyrefly/lib/alt/expr.rs modified how starred elements in list/set literals are inferred. Previously, starred elements always used the contextual hint for inference. Now, they first infer without a hint, and only retry with the hint if the result contains partial placeholders. This change affects how preamble_lines = [*self._UDF_PREAMBLE_LINES] is inferred. The tuple _UDF_PREAMBLE_LINES contains 6 specific literal strings. When unpacking *self._UDF_PREAMBLE_LINES without a contextual hint, pyrefly now infers the list type as list[Literal['COMMENT = ...', 'CREATE OR REPLACE...', ...]] (the union of the specific literal string values) instead of widening to list[str]. This over-narrow inference then causes the subsequent .append(f"...") calls to fail because str is not assignable to the narrow Literal[...] type.

pandas (-1)

This is a clear improvement. The removed error was a false positive where pyrefly incorrectly rejected range as an argument to list() because an overly-specific contextual type hint from np.array() was being propagated into the starred expression's inference. The code np.array([-1, *list(idces)], dtype=np.intp) is perfectly valid Python. The PR fix in pyrefly/lib/alt/expr.rs correctly changes the inference strategy for starred elements to avoid this over-constraining behavior.
Attribution: The change in pyrefly/lib/alt/expr.rs in the starred element handling within elts.map() is directly responsible. Previously, starred expressions always used expr_infer_with_hint_promote with the contextual star_hint. The PR changes this to first call self.expr_infer(value, errors) without the hint, then only retry with the hint if the result contains partial types (self.solver().is_partial(ty)). This prevents Iterable[LiteralString] or other overly-specific hints from over-constraining nested calls like list(range(n)) or map(str, range(n)).

sphinx (-1)

The removed error was a false positive in how the type checker handled the list() constructor call within starred unpacking in the expression [*list(items[:n_items]), overflow_marker]. The type checker incorrectly inferred that list.__init__ required Iterable[LiteralString] rather than Iterable[str], even though all relevant parameters (items: list[str], overflow_marker: str) are annotated as str. The PR fixes this by adjusting how starred elements are inferred in list literals, preventing the type checker from over-constraining the list() call with an incorrect LiteralString hint.
Attribution: The change to starred element inference in pyrefly/lib/alt/expr.rs (around line 1845) now infers starred elements without the contextual hint first, only retrying with the hint when the unhinted result contains partial placeholders. This prevents Iterable[LiteralString] from over-constraining the list() constructor call when the source is already typed as list[str].

➖ Neutral (2)

openlibrary (+1, -1)

Same errors at same locations with same error kinds — message wording changed, no behavioral impact.

jax (+1, -1)

Removed false positive on map(str, ...): The removed error incorrectly flagged map(str, range(n)) because the LiteralString contextual hint from a join was over-constraining the str callable parameter. The PR fix correctly prevents this over-constraining. This is an improvement.
New false positive on list[Var] -> list[Atom] assignment: The new error flags all_inputs: list[core.Atom] = [*jaxpr.invars, *jaxpr.constvars] where invars and constvars are list[Var] and Var is a subclass of Atom. While list invariance is a real typing rule, constructing a fresh list literal and annotating it with a parent type is universally accepted by mypy and pyright. The PR's change to not use the contextual hint for starred elements means the list literal's element type isn't promoted to Atom. This is pyrefly-only and a regression.

Overall: The analysis is factually correct. The removed error on map(str, range(n)) was indeed a false positive where LiteralString context over-constrained the str callable. The new error on list[Var] -> list[Atom] is correctly identified as a false positive - Var is a subclass of Atom in JAX's core module, and constructing a fresh list literal [*jaxpr.invars, *jaxpr.constvars] with a list[Atom] annotation is standard Python practice accepted by mypy and pyright. The invariance of list applies to reassignment of existing lists, not to fresh list literal construction where the annotation provides the target element type. The assessment of the net impact is accurate.

Attribution: The change in pyrefly/lib/alt/expr.rs in the Expr::Starred handling modified how starred elements infer types. Previously, starred elements always used the contextual hint for inference. Now they first try without the hint and only retry with the hint if the unhinted result contains partial placeholders. This fixed the map(str, range(n)) false positive (the LiteralString hint was over-constraining the str callable). However, the same change caused the new list[Var] vs list[Atom] error - without the hint, the starred unpacking of jaxpr.invars (which is list[Var]) now infers as Var instead of being promoted to Atom by the annotation hint, making the resulting list type list[Var] which is invariantly incompatible with list[Atom].

Suggested fixes

Summary: The PR's optimization to skip contextual hints for starred elements when unhinted inference produces no partials is too aggressive — it breaks standard bidirectional type inference for list literals containing starred unpacking, causing 10 pyrefly-only false positives across 6 projects.

**1. In the starred element handling in elts.map() in pyrefly/lib/alt/expr.rs (around line 1845), the retry condition is too narrow — it only retries with the contextual hint when the unhinted result contains partial types. Instead, also retry with the hint when the unhinted element type is NOT assignable to the hint's element type. This preserves the fix for map(str, range(n)) (where str IS assignable to str, so no retry needed) while restoring contextual typing for cases like [*list_of_subclass] assigned to list[superclass]. Pseudo-code:

let mut unpacked_ty = self.expr_infer(value, errors);
let retry_with_hint = star_hint.[`as_ref()`](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/expr.rs).is_some_and(|hint| {
    let has_partials = unpacked_ty.any(|ty| self.solver().is_partial(ty));
    let iterable_ty = self.unwrap_iterable(&unpacked_ty);
    let hint_ty = hint.[`as_ref()`](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/expr.rs);
    let not_assignable = iterable_ty.map_or(true, |ity| {
        !self.solver().is_subset_of(&ity, hint_ty)
    });
    has_partials || not_assignable
});
if retry_with_hint {
    unpacked_ty = self.expr_infer_with_hint_promote(
        value,
        star_hint.[`as_ref()`](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/expr.rs).map(|hint| hint.[`as_ref()`](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/expr.rs)),
        errors,
    );
}

This way:

  • *map(str, range(n)) with hint LiteralString: unhinted gives Iterator[str], iterable type str IS assignable to str (the widened hint), so NO retry → fixes the original false positive.
  • *exe.db.tables (type list[Table]) with hint DBObject: unhinted iterable type Table IS assignable to DBObject (subclass), so retry IS triggered (since list[Table] is not assignable to the hint due to invariance, but the element type is) — actually this needs more nuance.

A simpler and more correct approach: always use the hint for starred elements (restoring old behavior), but specifically handle the map(str, ...) / LiteralString over-constraining case by checking if the hint would narrow a str to LiteralString and skipping the hint in that case.**

Files: pyrefly/lib/alt/expr.rs
Confidence: medium
Affected projects: strawberry, core, django-modern-rest, materialize, ibis, jax
Fixes: bad-assignment, bad-return
The retry-only-on-partials heuristic is fundamentally wrong for the starred unpacking use case. The core issue is that list invariance means list[Subclass] is not assignable to list[Superclass], so contextual hints are essential for fresh list literal construction. The original bug was specifically about LiteralString over-constraining, not about contextual hints in general.

**2. In the starred element handling in pyrefly/lib/alt/expr.rs (around line 1845), use a more targeted fix: always apply the contextual hint for starred elements (restoring old behavior), but add a special case to prevent LiteralString hints from over-constraining. Specifically, when the star_hint is LiteralString or Iterable[LiteralString], widen it to str / Iterable[str] before passing it as the hint. Pseudo-code:

Expr::Starred(ExprStarred { value, .. }) => {
    // Widen LiteralString hints to str to avoid over-constraining
    let widened_hint = star_hint.[`as_ref()`](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/expr.rs).map(|hint| {
        self.solver().widen_literal_string(hint.clone())
    });
    let unpacked_ty = self.expr_infer_with_hint_promote(
        value,
        widened_hint.[`as_ref()`](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/expr.rs).map(|hint| hint.[`as_ref()`](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/expr.rs)),
        errors,
    );
    // ... rest unchanged
}

This directly addresses the root cause (LiteralString over-constraining map(str, range(n))) without breaking contextual typing for all other starred unpacking cases.**

Files: pyrefly/lib/alt/expr.rs
Confidence: high
Affected projects: strawberry, core, django-modern-rest, materialize, ibis, jax
Fixes: bad-assignment, bad-return
The original problem was specifically that LiteralString contextual hints were over-constraining starred elements. The PR's fix was too broad — it disabled ALL contextual hints for starred elements when the unhinted result has no partials. A targeted fix that only widens LiteralString to str in the hint would fix the original issue (pandas, sphinx improvements) while preserving correct contextual typing for all other cases. This eliminates all 10 regression errors across strawberry, core, django-modern-rest, materialize, ibis, and jax while keeping the 3 improvements.


Was this helpful? React with 👍 or 👎

Classification by primer-classifier (1 heuristic, 8 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.

map is confused by string literals

2 participants