Stop dropping union members a module guard could match - #104
Draft
apiology wants to merge 2 commits into
Draft
Conversation
ComplexType#intersect_with keeps a declared union member only when it conforms to a guard type or the guard type conforms to it. A member related to the guard in neither direction falls through both branches of that test and is dropped, so narrowing 'A, B_with_M' by an is_a?(M) guard yields 'B_with_M' alone. That is narrower than reality: an A_with_M satisfies the declared A, passes the guard, and is no longer admitted. The first example asserts that soundness property rather than an exact tag string, so it goes green under any correct answer. The second pins the precise result and stays pending, because the sound answer 'A & M, B_with_M' needs intersection types that master cannot represent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EiauH8LL9NaN6hbx5EaNbB
ComplexType#intersect_with kept a declared union member only when it conformed to the guard type or the guard conformed to it, and dropped it otherwise. Dropping is correct for two classes, which single inheritance proves cannot overlap, and narrowing a union by is_a? depends on it. It is wrong when either side is a module, because a subclass can mix the module in, so no such pair can be ruled out. Narrowing [A, B_with_M] by is_a?(M) therefore inferred B_with_M alone, excluding an A_with_M that satisfies the declared A, passes the guard, and is returned. The mirror case -- a module member against a class guard -- dropped every member and fell back to undefined, silencing the check rather than narrowing it. Both copies now keep the guard's type when neither side conforms and either is a module. That is the closest sound answer expressible here; the exact one is the two intersected, which has no representation yet. ApiMap#module? wraps the existing private get_namespace_type so the conformance code does not reimplement it at the call site. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EiauH8LL9NaN6hbx5EaNbB
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR was written by Claude Code on behalf of @apiology.
Problem: When a guard tests for a module, a union member not already known to include it is dropped — so the inferred type omits a value that can actually occur.
A_with_Msatisfies the declaredA, passes the guard, and is returned, yet the inferred type admits onlyB_with_M. The mirror case — a module member against a class guard — is quieter still: every member drops, the empty result falls back toundefined, and the file reports no problems at all.Solution: Keep the guard's type when the two types are related in neither direction and either side is a module, since a subclass can always mix a module in. Two unrelated classes remain disjoint under single inheritance and are still dropped.
The exact answer is their intersection, which cannot be spelled yet; a pending spec asserts it against castwide#1231.
Test plan:
origin/masterbaseline: 531 problems in 90 of 250 files on both sides, problem lists identical after normalizing paths and line numbers.