From 97b1b15139650fc825f6413564a8bb1f02d65c2c Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Mon, 15 Jun 2026 23:22:09 -0700 Subject: [PATCH] Fix #685: array destructuring uses the iterator protocol; verify #687 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Array binding patterns (`const [a, b] = src`) desugared at parse time to positional index access, which is correct for arrays/tuples/strings but mis-evaluated (and the type checker rejected) any non-indexable iterable — generators, Set, Map, objects with [Symbol.iterator]. JS array destructuring uses the iterator protocol, so these must work. Mirroring the existing __objectRest intrinsic, DesugarArrayPattern now wraps the source in a new __arrayDestructure(...) helper (so all lowering sites — declarations, for-of, function/arrow params — are covered centrally): - Type checker (NormalizeArrayDestructureSourceType): arrays/tuples/strings/any pass through with their precise type (tuples keep positional element types); any other iterable becomes Array, so `_dest0[i]` reads the element type instead of erroring. Non-iterable, non-indexable sources are left unchanged so the existing index-access diagnostic still fires. - Interpreter (NormalizeArrayDestructureSource) and compiled (ArrayDestructureSource): index-addressable sources pass through (fast path); everything else is materialized via the same routine spread uses (GetIterableElements / IterateToList), so destructuring and spread agree on what is iterable. The compiled path wraps the materialized list in $Array so an out-of-range index (pattern longer than the iterable) yields undefined and a binding default applies, instead of throwing IndexOutOfRange. #687 (spreading a generator method's result rejected as '') was already fixed by the #658/#661 inferred-method-return propagation (PR #695) and is covered by existing tests; this only refreshes a stale comment that claimed the result was still unobservable at the call site, and tags the existing test. 18 new shared-mode destructuring tests (generators, Set, Map entries, nested, rest, holes+defaults, custom [Symbol.iterator], element-type preservation, array fast-path regression). Full suite green (12998). --- .../CallHandlers/ArrayDestructureHandler.cs | 57 ++++++ .../CallHandlers/CallHandlerRegistry.cs | 1 + Compilation/EmittedRuntime.cs | 1 + .../RuntimeEmitter.Objects.Iteration.cs | 56 ++++++ Compilation/RuntimeEmitter.RuntimeClass.cs | 3 + Execution/Interpreter.Statements.cs | 17 ++ Parsing/Parser.Destructuring.cs | 15 +- Runtime/BuiltIns/BuiltInNames.cs | 11 ++ Runtime/BuiltIns/GlobalFunctionHandlers.cs | 13 ++ .../SharedTests/DestructuringTests.cs | 172 ++++++++++++++++++ .../GeneratorYieldInferenceTests.cs | 5 +- TypeSystem/TypeChecker.Calls.cs | 9 + TypeSystem/TypeChecker.Iterables.cs | 28 +++ TypeSystem/TypeChecker.Statements.Classes.cs | 7 +- 14 files changed, 387 insertions(+), 8 deletions(-) create mode 100644 Compilation/CallHandlers/ArrayDestructureHandler.cs diff --git a/Compilation/CallHandlers/ArrayDestructureHandler.cs b/Compilation/CallHandlers/ArrayDestructureHandler.cs new file mode 100644 index 00000000..4150b657 --- /dev/null +++ b/Compilation/CallHandlers/ArrayDestructureHandler.cs @@ -0,0 +1,57 @@ +using System.Reflection.Emit; +using SharpTS.Compilation.Emitters; +using SharpTS.Parsing; +using SharpTS.Runtime.BuiltIns; +using SharpTS.TypeSystem; + +namespace SharpTS.Compilation.CallHandlers; + +/// +/// Handles the internal __arrayDestructure helper that normalizes an array binding-pattern +/// source through the iterator protocol (#685). Index-addressable sources (arrays, tuples, strings) +/// are emitted as-is — the desugared positional index access reads them directly and the type checker +/// assigns them a matching pass-through type, so no runtime work is needed (preserves the fast path +/// and tuple positional element types). Every other source is routed through the emitted +/// ArrayDestructureSource runtime helper, which materializes non-indexable iterables +/// (generators, Set, Map, [Symbol.iterator] objects) into an array and passes everything else +/// through unchanged. +/// +public class ArrayDestructureHandler : ICallHandler +{ + public int Priority => 16; // Internal helper, right after ObjectRest (15) + + public bool TryHandle(IEmitterContext emitter, Expr.Call call) + { + if (call.Callee is not Expr.Variable v || v.Name.Lexeme != BuiltInNames.ArrayDestructure) + return false; + if (call.Arguments.Count != 1) + return false; + + var ctx = emitter.Context; + var il = ctx.IL; + var arg = call.Arguments[0]; + + // Emit the source value (boxed to object). + emitter.EmitExpression(arg); + emitter.EmitBoxIfNeeded(arg); + + // Fast path: a statically index-addressable source needs no normalization. The type + // checker passes these through with their precise type (see + // TypeChecker.NormalizeArrayDestructureSourceType), so the runtime value is already an + // array/tuple/string the index access reads directly — and tuple positional element types + // stay intact. + if (IsIndexAddressable(ctx.TypeMap?.Get(arg))) + return true; + + // Otherwise normalize at runtime: + // ArrayDestructureSource(value, Symbol.iterator, runtimeType) + il.Emit(OpCodes.Ldsfld, ctx.Runtime!.SymbolIterator); + il.Emit(OpCodes.Ldtoken, ctx.Runtime!.RuntimeType); + il.Emit(OpCodes.Call, ctx.Types.GetMethod(ctx.Types.Type, "GetTypeFromHandle")); + il.Emit(OpCodes.Call, ctx.Runtime!.ArrayDestructureSource); + return true; + } + + private static bool IsIndexAddressable(TypeInfo? type) => + type is TypeInfo.Array or TypeInfo.Tuple or TypeInfo.String or TypeInfo.StringLiteral; +} diff --git a/Compilation/CallHandlers/CallHandlerRegistry.cs b/Compilation/CallHandlers/CallHandlerRegistry.cs index e6314ffb..d20d4667 100644 --- a/Compilation/CallHandlers/CallHandlerRegistry.cs +++ b/Compilation/CallHandlers/CallHandlerRegistry.cs @@ -21,6 +21,7 @@ public CallHandlerRegistry() [ new SuperConstructorHandler(), // Priority 10 - super() calls new ObjectRestHandler(), // Priority 15 - Internal helpers first + new ArrayDestructureHandler(), // Priority 16 - Internal helper (#685) new ConsoleMethodHandler(), // Priority 20 - Console methods new StaticTypeHandler(), // Priority 30 - Math, JSON, Object, Array, etc. new GlobalThisChainHandler(), // Priority 32 - globalThis.X.Y() diff --git a/Compilation/EmittedRuntime.cs b/Compilation/EmittedRuntime.cs index 121ba0d6..830e4bf7 100644 --- a/Compilation/EmittedRuntime.cs +++ b/Compilation/EmittedRuntime.cs @@ -765,6 +765,7 @@ public class EmittedRuntime public MethodBuilder InvokeTaggedTemplateWithThis { get; set; } = null!; public MethodBuilder StringRaw { get; set; } = null!; public MethodBuilder ObjectRest { get; set; } = null!; + public MethodBuilder ArrayDestructureSource { get; set; } = null!; // #685: normalize array binding-pattern source via the iterator protocol // JSON methods public MethodBuilder JsonParse { get; set; } = null!; diff --git a/Compilation/RuntimeEmitter.Objects.Iteration.cs b/Compilation/RuntimeEmitter.Objects.Iteration.cs index 2ecb5d46..e2b93a82 100644 --- a/Compilation/RuntimeEmitter.Objects.Iteration.cs +++ b/Compilation/RuntimeEmitter.Objects.Iteration.cs @@ -7,6 +7,62 @@ namespace SharpTS.Compilation; public partial class RuntimeEmitter { + /// + /// Emits ArrayDestructureSource: normalizes an array binding-pattern source through the + /// iterator protocol (#685). Index-addressable sources — strings and any + /// (arrays, $Array, typed lists) — pass through + /// unchanged so the desugared positional index access reads them directly and stays consistent + /// with the matching pass-through type the type checker assigned. Any other iterable (Set, Map, + /// generators, [Symbol.iterator] objects, IEnumerable<object>) is materialized + /// via IterateToList into a List<object> so positional access yields the + /// iterated elements. Non-iterable sources pass through, preserving the existing lenient behavior. + /// Signature: object ArrayDestructureSource(object value, $TSSymbol iteratorSymbol, Type runtimeType) + /// + private void EmitArrayDestructureSource(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "ArrayDestructureSource", + MethodAttributes.Public | MethodAttributes.Static, + _types.Object, + [_types.Object, runtime.TSSymbolType, _types.Type] + ); + runtime.ArrayDestructureSource = method; + + var il = method.GetILGenerator(); + var ilistType = typeof(System.Collections.IList); + + var passThroughLabel = il.DefineLabel(); + + // string → pass through (the source stays typed as string; index access reads chars). + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.String); + il.Emit(OpCodes.Brtrue, passThroughLabel); + + // IList (List, $Array, typed lists) → pass through: already index-addressable, and + // routing a typed list (List/List) through IterateToList would re-box it. + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, ilistType); + il.Emit(OpCodes.Brtrue, passThroughLabel); + + // Everything else → materialize through the iterator protocol via IterateToList (Set, Map, + // generators, [Symbol.iterator] objects, .NET enumerables). This is the same routine spread + // uses, so destructuring and spread agree on what is iterable; it throws for genuinely + // non-iterable values (and null), matching JS's "x is not iterable". The List is + // wrapped in a $Array so the subsequent positional index access has JS array semantics — + // notably an out-of-range index (a pattern longer than the iterable) yields undefined (so a + // binding default applies) instead of throwing IndexOutOfRange off the bare list. + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, runtime.IterateToList); + il.Emit(OpCodes.Newobj, runtime.TSArrayCtor); + il.Emit(OpCodes.Ret); + + il.MarkLabel(passThroughLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ret); + } + private void EmitObjectRest(TypeBuilder typeBuilder, EmittedRuntime runtime) { // Accept object instead of Dictionary to support both object literals and class instances diff --git a/Compilation/RuntimeEmitter.RuntimeClass.cs b/Compilation/RuntimeEmitter.RuntimeClass.cs index e2fb1e68..fd3ef467 100644 --- a/Compilation/RuntimeEmitter.RuntimeClass.cs +++ b/Compilation/RuntimeEmitter.RuntimeClass.cs @@ -943,6 +943,9 @@ void EmitLinkProto(FieldBuilder child) EmitInvokeTaggedTemplate(typeBuilder, runtime); EmitInvokeTaggedTemplateWithThis(typeBuilder, runtime); EmitObjectRest(typeBuilder, runtime); + // #685: array binding-pattern source normalizer — depends on IterateToList / + // GetIteratorFunction (emitted above via EmitIteratorMethodsAdvanced). + EmitArrayDestructureSource(typeBuilder, runtime); // JSON methods — gated on UsesJSON (also implied by UsesHttp). if (_features.UsesJSON) { diff --git a/Execution/Interpreter.Statements.cs b/Execution/Interpreter.Statements.cs index 1060e733..ad6a9944 100644 --- a/Execution/Interpreter.Statements.cs +++ b/Execution/Interpreter.Statements.cs @@ -942,6 +942,23 @@ private ExecutionResult ExecuteForIn(Stmt.ForIn forIn) }; } + /// + /// Normalizes an array binding-pattern source through the iterator protocol (#685). Array + /// destructuring (const [a, b] = src) desugars to positional index access, which is only + /// correct for index-addressable sources. Arrays, strings, typed arrays and buffers pass through + /// unchanged (fast path); any other source is materialized into a via + /// — the same routine spread uses — so the index access reads + /// the iterated elements. A genuinely non-iterable source throws "is not iterable", matching JS; + /// the type checker already rejects those statically except behind any. + /// + internal object? NormalizeArrayDestructureSource(object? value) + { + if (value is SharpTSArray or string or SharpTSTypedArray or SharpTSBuffer) + return value; + + return new SharpTSArray(GetIterableElements(value).ToList()); + } + /// /// Iterates over elements with proper break/continue handling. /// diff --git a/Parsing/Parser.Destructuring.cs b/Parsing/Parser.Destructuring.cs index 94725177..93c606a5 100644 --- a/Parsing/Parser.Destructuring.cs +++ b/Parsing/Parser.Destructuring.cs @@ -144,9 +144,20 @@ private Stmt DesugarArrayPattern(ArrayPattern pattern, Expr initializer) { List statements = []; - // const _dest0 = initializer; + // const _dest0 = __arrayDestructure(initializer); + // JS array destructuring follows the iterator protocol, not positional + // indexing. The wrapper normalizes non-indexable iterables (generators, + // Set, Map, objects with [Symbol.iterator]) into an array so the index + // access below is spec-correct (#685); arrays/tuples/strings pass through + // unchanged to keep the fast index path. Token temp = GenerateTempVar(pattern.Line); - statements.Add(new Stmt.Var(temp, null, initializer)); + Expr normalizedInit = new Expr.Call( + new Expr.Variable(new Token(TokenType.IDENTIFIER, "__arrayDestructure", null, pattern.Line)), + new Token(TokenType.RIGHT_PAREN, ")", null, pattern.Line), + null, + [initializer] + ); + statements.Add(new Stmt.Var(temp, null, normalizedInit)); int index = 0; foreach (var element in pattern.Elements) diff --git a/Runtime/BuiltIns/BuiltInNames.cs b/Runtime/BuiltIns/BuiltInNames.cs index eb35d635..404527c8 100644 --- a/Runtime/BuiltIns/BuiltInNames.cs +++ b/Runtime/BuiltIns/BuiltInNames.cs @@ -216,5 +216,16 @@ public static class BuiltInNames /// public const string ObjectRest = "__objectRest"; + /// + /// Normalizes an array-destructuring source through the iterator protocol. + /// Array binding patterns (const [a, b] = src) desugar to positional + /// index access, which only works for index-addressable sources. This helper + /// wraps the source so non-indexable iterables (generators, Set, Map, objects + /// with [Symbol.iterator]) are materialized into an array first, matching + /// JS's iterator-protocol semantics (#685). Index-addressable sources + /// (arrays, strings, tuples) pass through unchanged to keep the fast path. + /// + public const string ArrayDestructure = "__arrayDestructure"; + #endregion } diff --git a/Runtime/BuiltIns/GlobalFunctionHandlers.cs b/Runtime/BuiltIns/GlobalFunctionHandlers.cs index d49ed2d5..b5cd397e 100644 --- a/Runtime/BuiltIns/GlobalFunctionHandlers.cs +++ b/Runtime/BuiltIns/GlobalFunctionHandlers.cs @@ -52,6 +52,7 @@ public static void RegisterAll(GlobalFunctionRegistry registry) // Internal helper registry.RegisterV2(BuiltInNames.ObjectRest, HandleObjectRest); + registry.RegisterV2(BuiltInNames.ArrayDestructure, HandleArrayDestructure); // Note: Error constructors are handled by SharpTSErrorClass (registered as globals). // Error() without 'new' resolves to SharpTSErrorClass.Call() via the general @@ -373,6 +374,18 @@ private static async ValueTask HandleObjectRest( throw new Exception($"{BuiltInNames.ObjectRest} requires 2 arguments"); } + private static async ValueTask HandleArrayDestructure( + Func> evaluateArg, + IReadOnlyList arguments, + Interpreter interpreter) + { + if (arguments.Count != 1) + throw new Exception($"{BuiltInNames.ArrayDestructure} requires 1 argument"); + + var source = (await evaluateArg(arguments[0])).ToObject(); + return RuntimeValue.FromBoxed(interpreter.NormalizeArrayDestructureSource(source)); + } + /// /// Implements the CommonJS require(specifier) function. Resolves the module relative /// to the calling module and returns its module.exports value. Specifier must be a diff --git a/SharpTS.Tests/SharedTests/DestructuringTests.cs b/SharpTS.Tests/SharedTests/DestructuringTests.cs index a63909a9..360821a5 100644 --- a/SharpTS.Tests/SharedTests/DestructuringTests.cs +++ b/SharpTS.Tests/SharedTests/DestructuringTests.cs @@ -118,6 +118,178 @@ public void ObjectDestructuring_WithDefault(ExecutionMode mode) #endregion + #region Iterable (non-indexable) Sources (#685) + + // Array destructuring follows the iterator protocol, so it must work over any + // iterable — not just index-addressable arrays/strings. Before #685 the parser + // desugared `[a, b] = src` to positional index access, which mis-evaluated (and + // the type checker rejected) generators, Set and Map. + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void ArrayDestructuring_OverGenerator(ExecutionMode mode) + { + var source = """ + function* g() { yield 10; yield 20; } + const [a, b] = g(); + console.log(a); + console.log(b); + """; + + var output = TestHarness.Run(source, mode); + Assert.Equal("10\n20\n", output); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void ArrayDestructuring_OverGeneratorWithRest(ExecutionMode mode) + { + var source = """ + function* g() { yield 1; yield 2; yield 3; yield 4; } + const [first, ...rest] = g(); + console.log(first); + console.log(rest.length); + console.log(rest[0]); + console.log(rest[1]); + console.log(rest[2]); + """; + + var output = TestHarness.Run(source, mode); + Assert.Equal("1\n3\n2\n3\n4\n", output); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void ArrayDestructuring_OverSet(ExecutionMode mode) + { + var source = """ + const s = new Set([1, 2, 3]); + const [x, y] = s; + console.log(x); + console.log(y); + """; + + var output = TestHarness.Run(source, mode); + Assert.Equal("1\n2\n", output); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void ArrayDestructuring_OverSetWithHoleAndDefault(ExecutionMode mode) + { + // Set has three values [10, 20, 30]; index 3 is missing → default applies. + var source = """ + const s = new Set([10, 20, 30]); + const [, second, , fourth = 99] = s; + console.log(second); + console.log(fourth); + """; + + var output = TestHarness.Run(source, mode); + Assert.Equal("20\n99\n", output); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void ArrayDestructuring_OverMapEntry(ExecutionMode mode) + { + // Iterating a Map yields [key, value] entries. + var source = """ + const m = new Map([["k", 1]]); + const [first] = m; + console.log(first[0]); + console.log(first[1]); + """; + + var output = TestHarness.Run(source, mode); + Assert.Equal("k\n1\n", output); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void ArrayDestructuring_NestedOverMapEntries(ExecutionMode mode) + { + // Nested pattern destructures each [key, value] entry positionally. + var source = """ + const m = new Map([["a", 1], ["b", 2]]); + const [[k1, v1], [k2, v2]] = m; + console.log(k1); + console.log(v1); + console.log(k2); + console.log(v2); + """; + + var output = TestHarness.Run(source, mode); + Assert.Equal("a\n1\nb\n2\n", output); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void ArrayDestructuring_OverCustomIterable(ExecutionMode mode) + { + var source = """ + const iterable = { + [Symbol.iterator]() { + let i = 0; + return { + next() { + return i < 3 + ? { value: i++ * 10, done: false } + : { value: undefined, done: true }; + } + }; + } + }; + const [a, b, c] = iterable; + console.log(a); + console.log(b); + console.log(c); + """; + + var output = TestHarness.Run(source, mode); + Assert.Equal("0\n10\n20\n", output); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void ArrayDestructuring_GeneratorElementTypeIsUsable(ExecutionMode mode) + { + // The destructured bindings carry the iterable's element type (number), so + // arithmetic type-checks and runs — exercising the type-checker side of #685. + var source = """ + function* g() { yield 5; yield 7; } + const [a, b] = g(); + console.log(a + b); + console.log(a * b); + """; + + var output = TestHarness.Run(source, mode); + Assert.Equal("12\n35\n", output); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void ArrayDestructuring_FastPathArrayUnchanged(ExecutionMode mode) + { + // Regression guard: the iterator-protocol normalization must not disturb the + // existing array/tuple fast path (arrays pass straight through). + var source = """ + const arr: number[] = [100, 200, 300]; + const [a, ...rest] = arr; + console.log(a); + console.log(rest.length); + const tup: [string, number] = ["x", 7]; + const [s, n] = tup; + console.log(s); + console.log(n + 1); + """; + + var output = TestHarness.Run(source, mode); + Assert.Equal("100\n2\nx\n8\n", output); + } + + #endregion + #region Nested Destructuring [Theory] diff --git a/SharpTS.Tests/TypeCheckerTests/GeneratorYieldInferenceTests.cs b/SharpTS.Tests/TypeCheckerTests/GeneratorYieldInferenceTests.cs index 97982e93..e220244a 100644 --- a/SharpTS.Tests/TypeCheckerTests/GeneratorYieldInferenceTests.cs +++ b/SharpTS.Tests/TypeCheckerTests/GeneratorYieldInferenceTests.cs @@ -173,8 +173,9 @@ function outer(): number[] { [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. + // The #661 headline (also reported as #687): 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 at the call site, so the spread type-checks. var source = """ class C { *m() { yield 1; yield 2; } } console.log([...new C().m()][0]); diff --git a/TypeSystem/TypeChecker.Calls.cs b/TypeSystem/TypeChecker.Calls.cs index 4a0900ad..0b3d96c9 100644 --- a/TypeSystem/TypeChecker.Calls.cs +++ b/TypeSystem/TypeChecker.Calls.cs @@ -376,6 +376,15 @@ iterGet.Object is Expr.Variable iterVar && return new TypeInfo.Any(); // Returns an object with remaining properties } + // Handle __arrayDestructure (internal helper for array binding patterns, #685). + // Normalizes the destructuring source so the desugared positional index + // access types correctly for non-indexable iterables. + if (call.Callee is Expr.Variable arrDestVar && arrDestVar.Name.Lexeme == BuiltInNames.ArrayDestructure) + { + var sourceType = call.Arguments.Count == 1 ? CheckExpr(call.Arguments[0]) : new TypeInfo.Any(); + return NormalizeArrayDestructureSourceType(sourceType); + } + // Invalidate property narrowings for method calls on objects // e.g., obj.mutate() should invalidate narrowings on obj's properties if (call.Callee is Expr.Get methodGet) diff --git a/TypeSystem/TypeChecker.Iterables.cs b/TypeSystem/TypeChecker.Iterables.cs index cdac556e..ce8ca189 100644 --- a/TypeSystem/TypeChecker.Iterables.cs +++ b/TypeSystem/TypeChecker.Iterables.cs @@ -207,6 +207,34 @@ private bool TryGetIterableElementType(TypeInfo type, out TypeInfo elementType) } } + /// + /// Computes the static type produced by the __arrayDestructure helper (#685), which + /// normalizes an array binding-pattern source through the iterator protocol. Index-addressable + /// sources (arrays, tuples, strings, any) pass through with their precise type so the + /// desugared positional index access stays accurate — notably tuples keep their per-position + /// element types. Any other iterable (Set, Map, generators, [Symbol.iterator] objects) + /// becomes Array<element>, so the subsequent _dest0[i] reads the element type + /// instead of erroring. A non-iterable, non-indexable source is returned unchanged so the existing + /// index-access diagnostic still fires. + /// + private TypeInfo NormalizeArrayDestructureSourceType(TypeInfo sourceType) + { + switch (sourceType) + { + case TypeInfo.Array: + case TypeInfo.Tuple: + case TypeInfo.String: + case TypeInfo.StringLiteral: + case TypeInfo.Any: + return sourceType; + } + + if (TryGetIterableElementType(sourceType, out var elementType)) + return new TypeInfo.Array(elementType); + + return sourceType; + } + /// /// The element type of any async-iterable source — the dedicated , /// (= AsyncIterableIterator) and diff --git a/TypeSystem/TypeChecker.Statements.Classes.cs b/TypeSystem/TypeChecker.Statements.Classes.cs index 23a784ff..969ca294 100644 --- a/TypeSystem/TypeChecker.Statements.Classes.cs +++ b/TypeSystem/TypeChecker.Statements.Classes.cs @@ -698,10 +698,9 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) // A generator method's type argument is its YIELD type (#548), not the // `return`-derived inferredReturn; a non-generator async method wraps in Promise. - // (The inferred result is not yet observable at a call site — `new C().m()` reads an - // inferred method return as `` regardless of generator-ness, a separate - // method-return-inference propagation gap, #658 — but computing it correctly here - // keeps the method path consistent with the function path for when that is fixed.) + // The resolved type is re-published below (anyInferredMethodReturnResolved), so + // `new C().m()` reads the real Generator<…>/Promise<…> at the call site rather than + // the `` placeholder (#658/#661; the generator-method face was #687). if (method.IsGenerator) inferredReturn = BuildInferredGeneratorType(_inferredYieldTypes!, method.IsAsync); else if (method.IsAsync && inferredReturn is not TypeInfo.Void)