Fix virtual dispatch / type filtering for uninstantiated generic subclasses#16982
Closed
stakach wants to merge 2 commits into
Closed
Fix virtual dispatch / type filtering for uninstantiated generic subclasses#16982stakach wants to merge 2 commits into
stakach wants to merge 2 commits into
Conversation
This was
linked to
issues
May 20, 2026
straight-shoota
requested changes
May 20, 2026
The crystal-lang#16947 codegen spec passes on master, so it is not a real regression test. The full original reproducer cannot be reproduced locally on master (in any build mode), so drop the test rather than keep one that doesn't bisect. The crystal-lang#10831 semantic spec used `assert_type` against `is_a?(...) ? true : false`, which is statically `bool` regardless of the filter result — so the spec passed even with the unsymmetric `common_descendent`. Rewrite as a `run(...).to_b` check that actually exercises runtime narrowing, and correct the misleading comment about `NoReturn` (the expression incorrectly evaluated to `false`).
straight-shoota
requested changes
May 22, 2026
This was referenced May 22, 2026
Contributor
Author
|
Splitting per @straight-shoota's review — see #16998 (virtual metaclass dispatch fix for #11134) and #16999 (symmetric |
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.
Fixes #11134, #10831.
Two open bugs both trace back to the type lattice treating uninstantiated generic subclasses as outside the "virtual" view of their parent class:
Bar.as(Foo.class).foosegfaults. The dispatch table forFoo+.class.foohas no branch forBar.metaclass, so it falls through theunreachable.is_a?(T)can break further type filtering ifTis an uninstantiated generic #10831 (deeper case):x.is_a?(B) && x.is_a?(C)returnsfalse(and the typeof collapses toNoReturnin some shapes) whenclass C(T) < B(T). After the firstis_a?(B),xis narrowed toB— andcommon_descendent(B, C)then doesn't find a path back toCbecause it only walks one side of the generic relationship.The same code paths likely affect #16947 (
Array(Foo)containing aSubGenericFoo(String)), but I couldn't reproduce its segfault on current master in any build mode, so I'm not claiming it as a fixed issue. The codegen change here plausibly helps it, but without a deterministic reproducer there's no regression test, so I'd rather leave it open and let it be closed only when someone can confirm.Root cause
Type#collect_subtypesinsrc/compiler/crystal/types.crfilters outGenericTypefrom a virtual type's subtypes:That's right for instance virtual dispatch —
Bar(T)itself isn't instantiable — but for metaclass dispatch the uninstantiatedBarIS a callable class value. And in the intersection logic, the absence of the relationship in both directions makes chainedis_a?filters collapse.Fix
Three coordinated changes:
Type::VirtualType(types.cr): addsubtypes_including_genericandcollect_subtypes_including_generic. They yield uninstantiatedGenericTypesubclasses only when no instantiations exist — if instantiations exist, those already carry the dispatch, and including the uninstantiated form would trip up class methods that reference the type parameter (e.g.def self.x; T.foo; end, the pattern in spec Error: can't instantiate abstract generic struct EnumType(Test) #9621). This is what kept the regression scope small.VirtualMetaclassType#each_concrete_type(types.cr) andVirtualTypeLookup#lookup_matches(method_lookup.cr): use the new method when iterating subtypes for a virtual metaclass. This adds the missing branch forBar.metaclassinFoo+.class's dispatch table and~matchfunction. Instance virtual dispatch is unchanged.common_descendent(GenericClassType, GenericClassType)(type_intersect.cr): trycommon_descendent_instance_and_genericin both directions, so the relationshipC(T) < B(T)flows in either order through chainedis_a?.Tests
spec/compiler/codegen/class_spec.cr— onerun-based regression for Dispatch over virtual metaclasses ignores uninstantiated generic subclasses #11134 (return value2).spec/compiler/semantic/is_a_spec.cr— onerun(...).to_bregression foris_a?(T)can break further type filtering ifTis an uninstantiated generic #10831 deep case. (Verified to fail on the unpatched compiler and pass on the patched one.)The simple case of #10831 (
x.is_a?(B) && x.is_a?(B(Int32))) was already fixed previously and continues to pass.