Skip to content

Infer tuple types for array literals against pair interface hints like Hash::_Pair - #2253

Open
sferik wants to merge 1 commit into
soutaro:masterfrom
sferik:pair-interface-tuple-hint
Open

Infer tuple types for array literals against pair interface hints like Hash::_Pair#2253
sferik wants to merge 1 commit into
soutaro:masterfrom
sferik:pair-interface-tuple-hint

Conversation

@sferik

@sferik sferik commented Jul 30, 2026

Copy link
Copy Markdown

RBS 4.1 changed the block types of Array#to_h and Hash#to_h, and the array-of-pairs argument of Hash.[], from tuples to the new Hash::_Pair[K, V] interface (ruby/rbs#2694). Steep only derives tuple hints from hint types that are tuple types themselves, so a pair literal like [key, value] in a to_h block is now inferred as an array and every call of the form to_h { |x| [key, value] } fails with Ruby::BlockBodyTypeMismatch:

Cannot allow block body have type `::Array[(::Symbol | ::Integer)]` because declared as type `::Hash::_Pair[::Symbol, ::Integer]`

Hash::_Pair is not a tuple type but it declares the tuple it accepts through its #to_ary method. When the hint contains no tuple type directly, this change deconstructs interface and intersection hints with try_convert(type, :to_ary) and uses the resulting tuple types as hints. The array literal is then inferred as a tuple and the existing tuple validation and fallback machinery applies unchanged. All three _Pair call sites funnel through the same array-literal hint path, so one change covers them.

The intersection case arises when the block body hint combines a tuple with the interface, for example ([::Symbol, ::Numeric] & ::Hash::_Pair[::Symbol, ::Numeric]). Flattening finds no tuple there because the intersection is opaque to flatten_union, but the shape of the intersection still answers #to_ary with a tuple.

A genuinely mismatched pair body still fails with Ruby::BlockBodyTypeMismatch, and the diagnostic improves, reporting the precise tuple type of the body instead of an array type. A regression test covers this.

rbs 4.1 changed the block types of Array#to_h and Hash#to_h and the
array-of-pairs argument of Hash.[] from tuples to the new Hash::_Pair
interface (ruby/rbs#2694). Steep only derives
tuple hints from hint types that are tuple types themselves, so a pair
literal in a to_h block is now inferred as an array and every call of
the form to_h { |x| [key, value] } fails with
Ruby::BlockBodyTypeMismatch:

    Cannot allow block body have type `::Array[(::Symbol | ::Integer)]`
    because declared as type `::Hash::_Pair[::Symbol, ::Integer]`

Hash::_Pair is not a tuple type, but it declares the tuple it accepts
through its #to_ary method. When the hint contains no tuple type
directly, deconstruct interface and intersection hints with
try_convert(type, :to_ary) and use the resulting tuple types as hints,
so the array literal is inferred as a tuple and the existing tuple
validation and fallback machinery applies unchanged.

The intersection case arises when the block body hint combines a tuple
with the interface, as in ([::Symbol, ::Numeric] & ::Hash::_Pair[::Symbol,
::Numeric]), where flattening finds no tuple because the intersection is
opaque to flatten_union.

A genuinely mismatched pair body still fails with
Ruby::BlockBodyTypeMismatch, and now reports the precise tuple type of
the body instead of an array type.
@sferik
sferik force-pushed the pair-interface-tuple-hint branch from 0ed1b58 to 8b24a47 Compare July 30, 2026 13:14
svyatov added a commit to svyatov/sec_id that referenced this pull request Jul 31, 2026
Gemfile.lock is not tracked, so the routine bumps (Rails 8.1.3.1, RuboCop
1.88.2, YARD 0.9.45, and others) appear only in the resolved lockfile.

simplecov 1.0 deprecates `add_filter` and the `# :nocov:` token, both of
which this repo uses. The spec helper now calls `skip`, and the two
unreachable regions in the ActiveModel adapter use `# simplecov:disable`
and `# simplecov:enable`. The bump also unblocks simplecov-cobertura 4.0,
which requires simplecov ~> 1.0.

rbs is held at 4.0.x. Version 4.1 retypes the block return of
`Array#to_h` from the tuple `[K, V]` to the `Hash::_Pair[K, V]`
interface. Steep 2.0 cannot infer an array literal as a tuple against an
interface hint, so `CFI::AttributeSet#to_h` stops type-checking. See
soutaro/steep#2253.

Three workarounds were rejected. A `#:` type assertion makes `steep
stats` report the file as an error, which trips the fail-closed coverage
gate. `map { ... }.to_h` and `each_with_object` both type-check. RuboCop
reports `Style/MapToHash` and `Style/ReduceToHash` for them, and the fix
each cop offers is the block form Steep rejects.
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.

1 participant