From 1c1496126b34333e1c15466d7690c9e205a9d8d5 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Mon, 15 Jun 2026 19:23:19 -0700 Subject: [PATCH] Fix #661/#658 (inferred class method return propagation) and #593 (class-instance iterability) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #661/#658 — inferred class method return types now reach call sites. CheckClassBody resolves a method's inferred (un-annotated) return type during the body pass and writes it to the mutable class, but the class was already frozen with placeholders before that pass, so `new C().m()` read (~any): ordinary methods skipped assignability checks and a generator method's result was rejected as non-iterable by spread/for...of/yield*. After the body pass, when any inferred method return was resolved, the frozen Class/GenericClass (and the TypeMap the compiler reads) are now rebuilt from the resolved mutable state and re-published; the structural compatibility cache is cleared so results computed against the placeholder (keyed by the stable DeclarationId) are not reused. New MutableClass.ResetFrozenCache() drops the cached frozen forms for the rebuild. - `class C { *m() { yield 1; } }` → `[...new C().m()]` type-checks (was "got ''"). - `class C { name() { return "hi"; } }` → `const n: number = new C().name()` is now TS2322. Covers static/async/private methods and recursion. #593 — class-instance iterability. A class is iterable only via an inherited [Symbol.iterator]; `extends Array` resolved Array to a name-only placeholder that dropped the element type, so for...of bound `any` on a plain instance (under-rejecting) and yield* threw a spurious TS2488 on an `extends Array` instance (over-rejecting). Now `extends Array/Set/Map/String/TypedArray` records the element type as an @@iterator on the placeholder, which the structural iterable probe reads by walking the hierarchy directly (the placeholder superclass is a name-only MutableClass the normal member walk skips), so for...of/yield*/spread bind the real element type. A plain class instance with no inherited [Symbol.iterator] is now TS2488 — matching tsc and the runtime, which already throws "for...of requires an iterable". #487/#532 were already fixed by 533d0cfb; added explicit Generator/AsyncGenerator 3-arg lib-spelling regression tests (#487) — #532's nested-generator tests already exist. Updated MethodCompletionValueTests to probe the undefined sentinel through `as any`, since an off-the-end method now correctly infers `void` and `void + 10` is the TS2365 tsc reports. Filed #685 (array destructuring desugars to index access, fails over non-indexable iterables) and #692 (compiled static generator method lowering) for pre-existing gaps surfaced here. Full suite 12714/0; TSConformance 31/31 (no baseline change). Changes are type-checker-only, so Test262 (runs .js untyped by default) is unaffected. --- .../SharedTests/MethodCompletionValueTests.cs | 7 +- .../DedicatedContainerTypeReferenceTests.cs | 34 +++++ .../GeneratorYieldInferenceTests.cs | 106 +++++++++++++++- .../StructuralIterableTypingTests.cs | 117 ++++++++++++++++++ TypeSystem/TypeChecker.Iterables.cs | 46 ++++++- TypeSystem/TypeChecker.Statements.Classes.cs | 84 ++++++++++++- TypeSystem/TypeInfo.cs | 14 +++ 7 files changed, 397 insertions(+), 11 deletions(-) diff --git a/SharpTS.Tests/SharedTests/MethodCompletionValueTests.cs b/SharpTS.Tests/SharedTests/MethodCompletionValueTests.cs index 33fa71d4..e1f6f698 100644 --- a/SharpTS.Tests/SharedTests/MethodCompletionValueTests.cs +++ b/SharpTS.Tests/SharedTests/MethodCompletionValueTests.cs @@ -67,10 +67,13 @@ class C { inst() {} } public void OffEndInstanceMethod_InArithmetic_IsNaN(ExecutionMode mode) { // undefined + 10 === NaN, but null + 10 === 10 — this distinguishes the - // undefined sentinel from CLR null beyond the typeof check. + // undefined sentinel from CLR null beyond the typeof check. The `as any` is + // required because `inst()`'s return type now correctly infers `void` (#661/#658) + // and `void + 10` is a type error (TS2365, matching tsc); the cast keeps this a + // pure runtime-sentinel probe. var source = """ class C { inst() {} } - console.log(new C().inst() + 10); + console.log((new C().inst() as any) + 10); """; Assert.Equal("NaN\n", TestHarness.Run(source, mode)); } diff --git a/SharpTS.Tests/TypeCheckerTests/DedicatedContainerTypeReferenceTests.cs b/SharpTS.Tests/TypeCheckerTests/DedicatedContainerTypeReferenceTests.cs index 4adf9b8f..baa48df2 100644 --- a/SharpTS.Tests/TypeCheckerTests/DedicatedContainerTypeReferenceTests.cs +++ b/SharpTS.Tests/TypeCheckerTests/DedicatedContainerTypeReferenceTests.cs @@ -113,6 +113,40 @@ public void AsyncGenerator_SelfAssignment_Accepted() TestHarness.RunInterpreted(source); } + [Fact] + public void Generator_ThreeTypeArgs_LibSpelling_Accepted() + { + // lib.d.ts is Generator; SharpTS keeps only the yield type but + // must accept (and ignore) the optional TReturn/TNext rather than rejecting "requires exactly 1" + // (#487). This is the issue's exact repro. + var source = """ + function* gen(): Generator { yield 1; } + console.log([...gen()]); + """; + TestHarness.RunInterpreted(source); + } + + [Fact] + public void Generator_TooManyTypeArgs_Rejected() + { + // Beyond the defaulted range (T, TReturn, TNext) is the range error TS2707, not the exact-count + // TS2314 (#487) — mirrors the Iterator arm. + var source = "function* gen(): Generator { yield 1; }"; + var ex = Assert.ThrowsAny(() => TestHarness.RunInterpreted(source)); + Assert.Equal("TS2707", ex.Diagnostic.TsCode); + } + + [Fact] + public void AsyncGenerator_ThreeTypeArgs_LibSpelling_Accepted() + { + // AsyncGenerator — same defaulted range as Generator (#487). + var source = """ + async function* gen(): AsyncGenerator { yield 1; } + let g: AsyncGenerator = gen(); + """; + TestHarness.RunInterpreted(source); + } + [Fact] public void Generator_AssignableToIterableIterator_Accepted() { diff --git a/SharpTS.Tests/TypeCheckerTests/GeneratorYieldInferenceTests.cs b/SharpTS.Tests/TypeCheckerTests/GeneratorYieldInferenceTests.cs index e3860b02..97982e93 100644 --- a/SharpTS.Tests/TypeCheckerTests/GeneratorYieldInferenceTests.cs +++ b/SharpTS.Tests/TypeCheckerTests/GeneratorYieldInferenceTests.cs @@ -165,9 +165,105 @@ function outer(): number[] { TestHarness.RunInterpreted(source); } - // NOTE: generator METHODS with an inferred return type now also compute Generator rather - // than Generator (the same fix in CheckClassBody), but the result is not yet observable at a call - // site: `new C().values()` resolves an inferred method's return to the unresolved `` - // placeholder regardless of generator-ness — a pre-existing method-return-inference propagation gap - // (#658). A test here would exercise that unrelated bug, so it is intentionally omitted. + // ---- #661/#658: a class method's inferred return type is now observable at the call site ---- + // CheckClassBody computes the inferred (un-annotated) method return type during the body pass and + // re-publishes the frozen class afterwards, so `new C().m()` no longer reads the `` placeholder. + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void GeneratorMethod_InferredYield_IsIterable_RunsInBothModes(ExecutionMode mode) + { + // The #661 headline: spreading a generator method with no explicit Generator return type used to + // fail "must be an iterable type ... got ''". The inferred Generator is now visible. + var source = """ + class C { *m() { yield 1; yield 2; } } + console.log([...new C().m()][0]); + """; + Assert.Equal("1\n", TestHarness.Run(source, mode)); + } + + [Fact] + public void GeneratorMethod_InferredYield_WrongElementUse_Rejected() + { + // The inferred method element is number, so binding it to string is a genuine TS2322 (not a vacuous + // pass through the old `` ~ any placeholder). + var source = """ + class C { *m() { yield 1; } } + const s: string = [...new C().m()][0]; + """; + var ex = Assert.ThrowsAny(() => TestHarness.RunInterpreted(source)); + Assert.Equal("TS2322", ex.Diagnostic.TsCode); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void GeneratorMethod_InferredYield_ForOf_RunsInBothModes(ExecutionMode mode) + { + var source = """ + class C { *m() { yield 10; yield 20; } } + let total = 0; + for (const v of new C().m()) { total += v; } + console.log(total); + """; + Assert.Equal("30\n", TestHarness.Run(source, mode)); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void AsyncGeneratorMethod_InferredYield_RunsInBothModes(ExecutionMode mode) + { + // The inferred async generator method computes AsyncGenerator (its yield type), so + // `for await...of` binds number and the compiled state machine emits a valid element type. + var source = """ + class C { async *m() { yield 1; yield 2; } } + async function run() { + let total = 0; + for await (const v of new C().m()) { total += v; } + console.log(total); + } + run(); + """; + Assert.Equal("3\n", TestHarness.Run(source, mode)); + } + + [Fact] + public void StaticGeneratorMethod_InferredYield_Interpreted() + { + // The inferred return type propagates for a STATIC generator method too (CheckClassBody writes the + // resolved type into StaticMethods). Interpreter-only: the compiler does not yet lower a static + // generator method's body ("Yield not supported in this context"), a pre-existing limitation + // independent of return-type inference — it fails the same way with an explicit return type (#692). + var source = """ + class C { static *gen() { yield 7; } } + console.log([...C.gen()][0]); + """; + Assert.Equal("7\n", TestHarness.RunInterpreted(source)); + } + + [Fact] + public void OrdinaryMethod_InferredReturn_WrongAssignment_Rejected() + { + // #658 (the non-generator face of the same propagation gap): an ordinary inferred method's return + // type now reaches the call site, so a bad assignment is the TS2322 tsc reports rather than a + // vacuous pass through `` ~ any. + var source = """ + class C { name() { return "hi"; } } + const n: number = new C().name(); + """; + var ex = Assert.ThrowsAny(() => TestHarness.RunInterpreted(source)); + Assert.Equal("TS2322", ex.Diagnostic.TsCode); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void OrdinaryMethod_InferredReturn_CorrectUse_RunsInBothModes(ExecutionMode mode) + { + // The same propagation must not over-reject: a correctly-typed consumer of the inferred return runs. + var source = """ + class C { name() { return "hi"; } } + const s: string = new C().name(); + console.log(s); + """; + Assert.Equal("hi\n", TestHarness.Run(source, mode)); + } } diff --git a/SharpTS.Tests/TypeCheckerTests/StructuralIterableTypingTests.cs b/SharpTS.Tests/TypeCheckerTests/StructuralIterableTypingTests.cs index 8a3bcd24..197d7e9a 100644 --- a/SharpTS.Tests/TypeCheckerTests/StructuralIterableTypingTests.cs +++ b/SharpTS.Tests/TypeCheckerTests/StructuralIterableTypingTests.cs @@ -432,4 +432,121 @@ public void StructuralIterable_IteratesAndSpreads(ExecutionMode mode) """; Assert.Equal("30\n3\n", TestHarness.Run(source, mode)); } + + // ---- #593: class-instance iterability (plain instance non-iterable; `extends Array` instance iterable) ---- + // A class is iterable only via an inherited [Symbol.iterator]. SharpTS previously bound `any` for a + // plain instance in for...of (under-rejecting) and threw a spurious TS2488 for an `extends Array` + // instance in yield* (over-rejecting); both now match tsc. + + [Fact] + public void PlainClassInstance_ForOf_RejectedTS2488() + { + // tsc: TS2488 — a plain class instance has no [Symbol.iterator]. SharpTS used to bind `any`. + var source = """ + class P { x = 1; } + function f(p: P) { for (const v of p) { console.log(v); } } + """; + var ex = Assert.ThrowsAny(() => TestHarness.RunInterpreted(source)); + Assert.Equal("TS2488", ex.Diagnostic.TsCode); + } + + [Fact] + public void PlainClassInstance_Spread_RejectedTS2488() + { + var source = """ + class P { x = 1; } + function f(p: P) { return [...p]; } + """; + var ex = Assert.ThrowsAny(() => TestHarness.RunInterpreted(source)); + Assert.Equal("TS2488", ex.Diagnostic.TsCode); + } + + [Fact] + public void PlainClassInstance_YieldStar_RejectedTS2488() + { + var source = """ + class P { x = 1; } + function* g(p: P) { yield* p; } + """; + var ex = Assert.ThrowsAny(() => TestHarness.RunInterpreted(source)); + Assert.Equal("TS2488", ex.Diagnostic.TsCode); + } + + [Fact] + public void ClassWithNextButNoSymbolIterator_ForOf_RejectedTS2488() + { + // Having a next() method does not make a class iterable in tsc (needs [Symbol.iterator]); the + // SharpTS runtime agrees (for...of over such an instance throws), so the static TS2488 matches both. + var source = """ + class Counter { next() { return { value: 1, done: false }; } } + function f(c: Counter) { for (const v of c) { console.log(v); } } + """; + var ex = Assert.ThrowsAny(() => TestHarness.RunInterpreted(source)); + Assert.Equal("TS2488", ex.Diagnostic.TsCode); + } + + [Fact] + public void ExtendsArray_YieldStar_Accepted() + { + // tsc: OK — C inherits Array's [Symbol.iterator]. SharpTS used to throw a spurious TS2488. + var source = """ + class C extends Array {} + function* g(x: C) { yield* x; } + """; + TestHarness.RunInterpreted(source); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void ExtendsArray_ForOf_BindsElementType_RunsInBothModes(ExecutionMode mode) + { + // The inherited element type is number (recovered from the dropped `extends Array` arg), and + // the runtime supports iterating an Array subclass. + var source = """ + class C extends Array {} + const c = new C(); + c.push(10); c.push(20); + let total = 0; + for (const v of c) { total += v; } + console.log(total); + """; + Assert.Equal("30\n", TestHarness.Run(source, mode)); + } + + [Fact] + public void ExtendsArray_ForOf_ElementIsNumberNotAny_WrongUseRejected() + { + // The bound element is genuinely number (not `any`), so misusing it as string is a TS2322. + var source = """ + class C extends Array {} + function f(c: C) { for (const v of c) { const s: string = v; } } + """; + var ex = Assert.ThrowsAny(() => TestHarness.RunInterpreted(source)); + Assert.Equal("TS2322", ex.Diagnostic.TsCode); + } + + [Fact] + public void ExtendsArray_ThroughUserSubclass_IsIterable() + { + // Iterability is inherited transitively: B → A → Array. + var source = """ + class A extends Array {} + class B extends A {} + function f(b: B) { for (const v of b) { const n: number = v; } } + """; + TestHarness.RunInterpreted(source); + } + + [Fact] + public void ExtendsNonIterableBuiltin_ForOf_RejectedTS2488() + { + // `extends Error` is not iterable in tsc; the enumeration of iterable bases excludes it, so the + // instance is correctly reported non-iterable rather than binding `any`. + var source = """ + class MyErr extends Error {} + function f(e: MyErr) { for (const v of e) {} } + """; + var ex = Assert.ThrowsAny(() => TestHarness.RunInterpreted(source)); + Assert.Equal("TS2488", ex.Diagnostic.TsCode); + } } diff --git a/TypeSystem/TypeChecker.Iterables.cs b/TypeSystem/TypeChecker.Iterables.cs index 3e4f345e..cdac556e 100644 --- a/TypeSystem/TypeChecker.Iterables.cs +++ b/TypeSystem/TypeChecker.Iterables.cs @@ -85,7 +85,14 @@ private bool TryGetStructuralIterableElement(TypeInfo source, out TypeInfo eleme TypeInfo? iteratorFactory = GetMemberType(source, "@@iterator"); if (!IsCallableMember(iteratorFactory) && source is TypeInfo.Record { SymbolIndexType: { } symIndex }) iteratorFactory = symIndex; // object-literal [Symbol.iterator]() lands in the symbol index - if (!IsCallableMember(iteratorFactory)) return false; + if (!IsCallableMember(iteratorFactory)) + { + // A class instance may inherit iterability from a built-in iterable base + // (`class C extends Array`). That placeholder superclass is a name-only MutableClass + // the normal member walk skips, so probe the hierarchy directly (#593). + return source is TypeInfo.Instance instance + && TryGetInstanceExtendsBuiltInIterableElement(instance, out elementType); + } TypeInfo? iterator = GetCallableReturnType(iteratorFactory); if (iterator is null) { elementType = new TypeInfo.Any(); return true; } @@ -93,6 +100,34 @@ private bool TryGetStructuralIterableElement(TypeInfo source, out TypeInfo eleme return TryGetIteratorElementFromReturn(iterator, out elementType); } + /// + /// Element type of a class instance that (transitively) extends a built-in iterable, read from the + /// @@iterator the extends Array<T>/Set<T>/… placeholder records (see + /// ). That placeholder superclass is a name-only + /// which ClassInfoAccessor's chain walk skips, so the + /// hierarchy is walked directly here — mirroring ExtendsBuiltInError. Returns false for an + /// instance with no built-in-iterable base, leaving it to be reported non-iterable (#593). + /// + private bool TryGetInstanceExtendsBuiltInIterableElement(TypeInfo.Instance instance, out TypeInfo elementType) + { + elementType = null!; + TypeInfo? current = instance.ResolvedClassType; + for (int guard = 0; current != null && guard < 64; guard++) + { + TypeInfo? iteratorFactory = current is TypeInfo.MutableClass mc + ? (mc.Methods.TryGetValue("@@iterator", out var m) ? m : null) + : GetMethods(current)?.GetValueOrDefault("@@iterator"); + if (IsCallableMember(iteratorFactory)) + { + TypeInfo? iterator = GetCallableReturnType(iteratorFactory); + if (iterator is null) { elementType = new TypeInfo.Any(); return true; } + return TryGetIteratorElementFromReturn(iterator, out elementType); + } + current = GetSuperclass(current); + } + return false; + } + /// /// Element of the iterator value returned by [Symbol.iterator](): a dedicated iterator/generator /// record carries it directly, otherwise it is read structurally from the iterator's next(). An @@ -129,14 +164,19 @@ private bool TryGetIteratorElementFromReturn(TypeInfo iterator, out TypeInfo ele /// @@iterator member (see TypeChecker.Statements.Interfaces) — so an interface carrying a /// number index might be iterable-via-Array and is spared to avoid a false TS2488. A genuine /// [Symbol.iterator]() interface member is caught earlier by the structural probe. + /// Class instance: a class is iterable only via an inherited [Symbol.iterator]. + /// An extends Array/Set/Map/... instance carries one (recovered by the structural probe via + /// ) and is caught earlier; a class cannot + /// declare its own [Symbol.iterator]() yet (#592), so any instance reaching here is genuinely + /// non-iterable, matching tsc's TS2488 instead of binding any (#593). When #592 lands, a + /// user [Symbol.iterator] becomes a real member the structural probe finds first. /// - /// Class instances are intentionally excluded: a class may extends Array (iterable) yet that - /// base is a name-only placeholder here, so non-iterability can't be proven — tracked separately. /// private static bool IsProvablyNonIterableStructuralObject(TypeInfo source) => source switch { TypeInfo.Record => true, TypeInfo.Interface { NumberIndexType: null } => true, + TypeInfo.Instance => true, _ => false }; diff --git a/TypeSystem/TypeChecker.Statements.Classes.cs b/TypeSystem/TypeChecker.Statements.Classes.cs index 28a14d98..23a784ff 100644 --- a/TypeSystem/TypeChecker.Statements.Classes.cs +++ b/TypeSystem/TypeChecker.Statements.Classes.cs @@ -1,5 +1,6 @@ using System.Collections.Frozen; using SharpTS.Parsing; +using SharpTS.Runtime.BuiltIns; using SharpTS.TypeSystem.Exceptions; namespace SharpTS.TypeSystem; @@ -551,6 +552,11 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) _environment = classEnv; _currentClass = classTypeForBody; + // Set when a method's inferred (un-annotated) return type is resolved during the body + // pass below. The class was frozen with placeholders before this pass, so + // when this is true the frozen class is rebuilt and re-published afterwards (#658/#661). + bool anyInferredMethodReturnResolved = false; + try { // Check instance field initializers (e.g. `x = 5`, `r = () => { ... }`) within the @@ -714,6 +720,9 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) if (method.IsStatic) mutableClass.StaticMethods[mName] = updatedMethodType; else mutableClass.Methods[mName] = updatedMethodType; } + // The frozen class call sites read still holds the placeholder for + // this method; flag a rebuild after the body pass (#658/#661). + anyInferredMethodReturnResolved = true; } } finally @@ -814,6 +823,34 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) _environment = prevEnv; _currentClass = prevClass; } + + // Publish method return types inferred during the body pass. The class was frozen with + // placeholders before the body could be checked, so the frozen Class/GenericClass + // that call sites read (and the TypeMap the compiler reads) still hold the placeholder for + // every un-annotated method. Rebuild the frozen forms from the now-resolved mutable state and + // re-register them. Without this, `new C().m()` on an inferred method reads (~any): + // ordinary methods silently skip assignability checks and a generator method's result is + // rejected as non-iterable by spread/for...of/yield* (#658/#661). `_environment` here is the + // outer scope the class was originally defined in (the body pass restored it above). + if (anyInferredMethodReturnResolved) + { + mutableClass.ResetFrozenCache(); + if (classTypeParams != null && classTypeParams.Count > 0) + { + _environment.Define(classStmt.Name.Lexeme, mutableClass.FreezeGeneric(classTypeParams)); + _typeMap.SetClassType(classStmt.Name.Lexeme, mutableClass.Freeze()); + } + else + { + var refrozen = mutableClass.Freeze(); + _environment.Define(classStmt.Name.Lexeme, refrozen); + _typeMap.SetClassType(classStmt.Name.Lexeme, refrozen); + } + // Structural compatibility results cache on CacheKey() (carries the stable DeclarationId), + // so any comparison made against the placeholder during the body pass must not be reused. + _compatibilityCache = null; + _identityCompatibilityCache = null; + } } /// @@ -874,9 +911,20 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) // Promise, Array). Create a placeholder class so super() // calls and constructor validation type-check correctly // (accept any number of args). - var placeholder = new TypeInfo.MutableClass(Expr.GetSuperclassLeafName(classStmt.SuperclassExpr)!); + var leafName = Expr.GetSuperclassLeafName(classStmt.SuperclassExpr)!; + var placeholder = new TypeInfo.MutableClass(leafName); placeholder.Methods["constructor"] = new TypeInfo.Function( [new TypeInfo.Any()], new TypeInfo.Void(), RequiredParams: 0, HasRestParam: true); + // When the base is a built-in iterable (`class C extends Array`), record its + // element type as an @@iterator member. The global resolves to Any in value position so + // the type argument is otherwise dropped; recovering it lets an instance's for...of / + // yield* / spread bind the real element type, and lets a genuinely non-iterable instance + // be told apart from an iterable one (#593). + if (TryGetBuiltInIterableElement(leafName, classStmt.SuperclassTypeArgs, out var iterableElement)) + { + placeholder.Methods["@@iterator"] = new TypeInfo.Function( + [], new TypeInfo.Iterator(iterableElement), RequiredParams: 0, HasRestParam: false); + } superclass = placeholder; } else @@ -885,6 +933,40 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) return superclass; } + /// + /// Element type of a built-in iterable used as a superclass (class C extends Array<number>), + /// recovered from the extends type-argument names that + /// otherwise drops (the global resolves to Any in value position). Array/Set/typed-array element + /// is the sole argument; Map yields the [K, V] tuple; String yields string; the bigint + /// typed arrays yield bigint and the rest number. Returns false for a non-iterable + /// base (Error, Date, Promise, WeakMap, …), so such a subclass instance stays correctly non-iterable + /// (#593). A missing/omitted type argument degrades to any, matching class C extends Array. + /// + private bool TryGetBuiltInIterableElement(string baseName, List? typeArgs, out TypeInfo element) + { + TypeInfo Arg(int i) => typeArgs != null && i < typeArgs.Count ? ToTypeInfo(typeArgs[i]) : new TypeInfo.Any(); + switch (baseName) + { + case "Array" or "ReadonlyArray" or "Set" or "ReadonlySet": + element = Arg(0); + return true; + case "Map" or "ReadonlyMap": + element = TypeInfo.Tuple.FromTypes([Arg(0), Arg(1)], 2); + return true; + case "String": + element = new TypeInfo.String(); + return true; + case var n when BuiltInNames.IsTypedArrayName(n): + element = n is BuiltInNames.BigInt64Array or BuiltInNames.BigUint64Array + ? new TypeInfo.BigInt() + : new TypeInfo.Primitive(TokenType.TYPE_NUMBER); + return true; + default: + element = null!; + return false; + } + } + /// /// Type checks a declare class (ambient declaration). /// For declare classes, we only validate signatures without requiring implementations. diff --git a/TypeSystem/TypeInfo.cs b/TypeSystem/TypeInfo.cs index c3949782..26cbd213 100644 --- a/TypeSystem/TypeInfo.cs +++ b/TypeSystem/TypeInfo.cs @@ -391,6 +391,20 @@ public TypeInfo.GenericClass FreezeGeneric(List typeParams) /// Gets the frozen class if available, otherwise null. public TypeInfo.Class? Frozen => _frozen; + /// + /// Drops the cached frozen core/class so the next / + /// rebuilds from the current mutable state. + /// Used after a class body is type-checked to publish method return types that + /// were inferred during the body pass — the early freeze captured the + /// <inferred> placeholders (#658/#661). The + /// is preserved, so frozen-class identity-by-declaration (cache keys) is stable. + /// + public void ResetFrozenCache() + { + _frozenCore = null; + _frozen = null; + } + public override string ToString() => IsAbstract ? $"abstract class {Name}" : $"class {Name}"; }