From 5d00cb923a2ab3f98eea176ff97aba80e192bcc6 Mon Sep 17 00:00:00 2001 From: jayrulez Date: Sun, 25 Oct 2026 12:23:39 -0400 Subject: [PATCH] Compiler: comptime keeps no method list of a type still declaring them CeMachine::GetTypeInfo caches a type's method instances by its revision. A comptime call can reach a type through a cycle while that type is still declaring its methods (a generator on a derived type reading its base's methods, when declaring one of the base's methods populated the derived type), and the partial list was cached under the type's final revision, so every later GetMethods in the compile saw it. Which types came out short depended on compile order. Such a type is now answered with what it has but not cached, so a read after its methods are declared sees them all. --- IDEHelper/Compiler/CeMachine.cpp | 4 ++- IDEHelper/Tests/src/Comptime.bf | 56 ++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/IDEHelper/Compiler/CeMachine.cpp b/IDEHelper/Compiler/CeMachine.cpp index 51b9fe587..24734b2e6 100644 --- a/IDEHelper/Compiler/CeMachine.cpp +++ b/IDEHelper/Compiler/CeMachine.cpp @@ -11223,7 +11223,9 @@ CeTypeInfo* CeMachine::GetTypeInfo(BfType* type) } mCeModule->PopulateType(typeInstance, BfPopulateType_DataAndMethods); - ceTypeInfo->mRevision = typeInstance->mRevision; + // A type reached through a cycle can still be declaring its methods: answer with what it has + // now, but don't keep that, or every later read this compile sees the partial list + ceTypeInfo->mRevision = (typeInstance->mDefineState >= BfTypeDefineState_DefinedAndMethodsSlotted) ? typeInstance->mRevision : -1; for (auto& methodGroup : typeInstance->mMethodInstanceGroups) { if (methodGroup.mOnDemandKind == BfMethodOnDemandKind_NoDecl_AwaitingReference) diff --git a/IDEHelper/Tests/src/Comptime.bf b/IDEHelper/Tests/src/Comptime.bf index 5e41e7135..b9fd4f827 100644 --- a/IDEHelper/Tests/src/Comptime.bf +++ b/IDEHelper/Tests/src/Comptime.bf @@ -875,5 +875,61 @@ namespace Tests { Test.Assert(cZeroLengthMemOps == 1); } + + /// Reads its base's methods while it is applied, the way a generator asks whether an + /// ancestor already declares a member + [AttributeUsage(.Class)] + struct ReadsBaseMethodsAttribute : Attribute, IComptimeTypeApply + { + [Comptime] + public void ApplyToType(Type type) + { + int count = 0; + for (let m in type.BaseType.GetMethods(.Public | .Instance | .DeclaredOnly)) + count++; + Compiler.EmitTypeBody(type, scope $"public const int cBaseMethodsSeen = {count};\n"); + } + } + + /// Declaring Make populates CycleDerived, whose generator reads this type's methods before + /// they are all declared + class CycleBase + { + public int First => 1; + public CycleDerived Make() => null; + public int Second => 2; + } + + /// CycleBase's members without the cycle + class CycleTwin + { + public int First => 1; + public Object Make() => null; + public int Second => 2; + } + + [ReadsBaseMethods] + class CycleDerived : CycleBase + { + } + + [Comptime] + static int CountDeclaredMethods(Type type) + { + int count = 0; + for (let m in type.GetMethods(.Public | .Instance | .DeclaredOnly)) + count++; + return count; + } + + // A type's methods read while it was still declaring them (through a cycle) were cached as + // they stood, so every later read in the compile saw that partial list + [Test] + public static void TestMethodsReadAfterACycle() + { + Test.Assert(CountDeclaredMethods(typeof(CycleTwin)) >= 3); + Test.Assert(CountDeclaredMethods(typeof(CycleBase)) == CountDeclaredMethods(typeof(CycleTwin))); + Test.Assert(CycleDerived.cBaseMethodsSeen >= 0); + } } }