Register base classes before derived classes with a topological sort - #53
Open
cobriensr wants to merge 1 commit into
Open
Register base classes before derived classes with a topological sort#53cobriensr wants to merge 1 commit into
cobriensr wants to merge 1 commit into
Conversation
The entry-point generator ordered registered classes so that base classes register before derived ones, but the ordering was a declaration-order-dependent heuristic (it built the type map incrementally and only accumulated a count for base types already seen), not a topological sort. When classes were declared derived-first, a class could be emitted before its base. Because registration is eager -- GodotRegistry.RegisterClassCore calls classdb_register_extension_class with the parent name immediately -- the engine then rejects the class for an unknown parent and the whole C# extension fails to load, based on nothing but source/file order. Replace the heuristic with a real topological order: build the complete name->spec map first, then order by each class's transitive registered-ancestor depth using a stable sort (so unrelated classes keep their declaration order). A base type always has a strictly lower depth than any type deriving from it, so base classes are always registered first. Add a regression test with an inheritance chain declared derived-first, which the previous heuristic ordered incorrectly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Problem
The entry-point generator orders registered
[GodotClass]types so that base classes register before derived classes and this is necessary because registration is eager:GodotRegistry.RegisterClassCorecallsclassdb_register_extension_class…with the parent's name immediately, so if a class is registered before its base, the engine rejects it (unknown parent) and the whole C# extension fails to load.But the ordering in
EntryPointGeneratorwas not a topological sort, but a declaration-order-dependent heuristic: it built the type map incrementally inside the same loop and only accumulated a "derived count" for base types already seen, thenOrderByDescending'd that count. So the emitted order depends on source/file order.Concrete failure: three classes
Bat : Enemy,Enemy : Mob,Mob : Nodedeclared derived-first (e.g. alphabetical file order) are emitted asEnemy, Mob, Bat->Enemybefore its baseMob-> the engine rejectsEnemy-> the extension doesn't load. Nothing about the code is wrong, only the file naming.Fix
Replace the heuristic with a real topological order: build the complete
name -> specmap first, then order by each type's transitive registered-ancestor depth with a stable sort:A base type always has a strictly lower depth than any type deriving from it, so base classes always come first;
OrderByis stable, so unrelated (equal-depth) classes keep their declaration order. (C# forbids inheritance cycles, so the walk always terminates.)Verification
EntryPointGeneratorTests): an inheritance chain declared derived-first. Fail-before: the old generator emittedEnemy, Mob, Bat(derived before base). Pass-after:Mob, Enemy, Bat.MainWithInheritancesnapshot's order changed slightly (a sibling moved so equal-depth types are now in declaration order) — still every base before its derived; regenerated to match.Godot.SourceGeneration.Tests: 22/22.I found this issue while auditing the source generator for adoption-blocking bugs and this issue was not previously filed.