Skip to content

Align type params across declarations in module-self types and superclass validation - #3067

Merged
soutaro merged 4 commits into
masterfrom
claude/module-self-type-param-alignment-2wpbja
Aug 7, 2026
Merged

Align type params across declarations in module-self types and superclass validation#3067
soutaro merged 4 commits into
masterfrom
claude/module-self-type-param-alignment-2wpbja

Conversation

@soutaro

@soutaro soutaro commented Aug 6, 2026

Copy link
Copy Markdown
Member

When a module/class has multiple declarations with different (but compatible) type parameter names, the variables written in a non-primary declaration leaked through as free variables: module M[A] : _Foo[A] + module M[B] : _Foo[B] made ModuleEntry#self_types return [_Foo[A], _Foo[B]], and class C[A] < Base[A] + class C[B] < Base[B] raised a false SuperclassMismatchError. This broke Steep's module self type check for every class including Enumerable, because rbs 4.1.2 renamed the core Enumerable's type param Elem to E while sig/shims/enumerable.rbs still uses Elem (soutaro/steep#2256).

This PR renames the variables to the primary declaration's type parameter names in both places — the same alignment that methods, instance variables, and mixin arguments already receive — keeping the original locations. The substitution is extracted into ModuleEntry#align_params / ClassEntry#align_params, shared by all five call sites, and returns nil when the declaration already uses the entry's names. Also, Module::Self#hash no longer includes location.hash, matching ==, so .uniq deduplicates equal self types across files.

claude added 3 commits August 6, 2026 02:10
…_types

When a module has multiple declarations with different (but compatible)
type parameter names, `Environment::ModuleEntry#self_types` collected the
self type constraints of each declaration as-is. The type variables from
non-primary declarations were left as free variables that are not bound
to any type parameter of the module, so downstream tools (e.g. Steep's
module self type check) could never satisfy the constraint:

    # a.rbs
    module M[out A] : _Foo[A]
    end

    # b.rbs
    module M[out B] : _Foo[B]
    end

    entry.self_types  # => [_Foo[A], _Foo[B]]  (B is unbound)

Mixin members already get this alignment via `align_params` in
`DefinitionBuilder::AncestorBuilder#mixin_ancestors`, but module self
types did not. Fix it in `ModuleEntry#self_types` — the aggregation
point every consumer goes through — by renaming the type variables of
each declaration's self types to the primary declaration's type
parameters, using the same substitution as `mixin_ancestors`. The
`location` of substituted self types keeps pointing to the original
declaration, so error locations (NoSelfTypeFoundError,
InvalidTypeApplicationError) are unchanged.

Also drop `location` from `AST::Declarations::Module::Self#hash` to make
it consistent with `#==`/`#eql?`, which only compare `name` and `args`.
The inconsistency made the `.uniq` in `ModuleEntry#self_types`
ineffective across files, so identical self types from different
declarations were duplicated.

With both fixes, the example above now yields `[_Foo[A]]`.

This is what broke Steep's self check with rbs 4.1.2, where
core/enumerable.rbs renamed `Elem` to `E` while other environments still
declare `module Enumerable[unchecked out Elem] : _Each[Elem]`:
`one_instance_ancestors(::Enumerable).self_types` became
`[_Each[E, void], _Each[Elem, void]]`, failing every class that includes
Enumerable. (soutaro/steep#2256)

Note: `sig/shims/enumerable.rbs` intentionally keeps the `Elem` name —
this repository's own `steep check` runs on rbs 3.9 whose core still
uses `Elem`, and renaming the shim to `E` makes the self check fail
there. With the alignment fix the name difference is harmless.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TBY8ct4HpkVkZNPdsNHEDE
The superclass comparison across multiple declarations compared the
superclass args as written, so declarations that declare the same
superclass with different type parameter names (`class C[A] < Base[A]`
and `class C[B] < Base[B]`) raised a false SuperclassMismatchError.
Align the args to the entry's type parameter names before comparing,
like ModuleEntry#self_types and mixin_ancestors do.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TBY8ct4HpkVkZNPdsNHEDE
…arams

The substitution that renames a declaration's type parameters to the
entry's type parameters was built inline in five places: MethodBuilder,
DefinitionBuilder#define_instance, AncestorBuilder#mixin_ancestors,
ModuleEntry#self_types, and AncestorBuilder#validate_super_class!.
Define it once as ModuleEntry#align_params / ClassEntry#align_params and
use it from all of them. The method returns nil when the declaration
already uses the entry's type parameter names, so the callers can skip
the substitution in the common case.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TBY8ct4HpkVkZNPdsNHEDE
Type params validation runs before the alignment, so the arity mismatch
raises GenericParameterMismatchError instead of building a broken
substitution.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TBY8ct4HpkVkZNPdsNHEDE
@soutaro soutaro changed the title Align module-self type params across declarations in ModuleEntry#self_types Align type params across declarations in module-self types and superclass validation Aug 7, 2026
@soutaro
soutaro added this pull request to the merge queue Aug 7, 2026
Merged via the queue into master with commit 6c2f00c Aug 7, 2026
48 checks passed
@soutaro
soutaro deleted the claude/module-self-type-param-alignment-2wpbja branch August 7, 2026 02:05
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