Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions Compilation/CallHandlers/ArrayDestructureHandler.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Handles the internal <c>__arrayDestructure</c> 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
/// <c>ArrayDestructureSource</c> runtime helper, which materializes non-indexable iterables
/// (generators, Set, Map, <c>[Symbol.iterator]</c> objects) into an array and passes everything else
/// through unchanged.
/// </summary>
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;
}
1 change: 1 addition & 0 deletions Compilation/CallHandlers/CallHandlerRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions Compilation/EmittedRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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!;
Expand Down
56 changes: 56 additions & 0 deletions Compilation/RuntimeEmitter.Objects.Iteration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,62 @@ namespace SharpTS.Compilation;

public partial class RuntimeEmitter
{
/// <summary>
/// Emits ArrayDestructureSource: normalizes an array binding-pattern source through the
/// iterator protocol (#685). Index-addressable sources — strings and any
/// <see cref="System.Collections.IList"/> (arrays, <c>$Array</c>, 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, <c>[Symbol.iterator]</c> objects, <c>IEnumerable&lt;object&gt;</c>) is materialized
/// via <c>IterateToList</c> into a <c>List&lt;object&gt;</c> 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)
/// </summary>
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<object>, $Array, typed lists) → pass through: already index-addressable, and
// routing a typed list (List<double>/List<bool>) 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<object> 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
Expand Down
3 changes: 3 additions & 0 deletions Compilation/RuntimeEmitter.RuntimeClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
17 changes: 17 additions & 0 deletions Execution/Interpreter.Statements.cs
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,23 @@ private ExecutionResult ExecuteForIn(Stmt.ForIn forIn)
};
}

/// <summary>
/// Normalizes an array binding-pattern source through the iterator protocol (#685). Array
/// destructuring (<c>const [a, b] = src</c>) 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 <see cref="SharpTSArray"/> via
/// <see cref="GetIterableElements"/> — 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 <c>any</c>.
/// </summary>
internal object? NormalizeArrayDestructureSource(object? value)
{
if (value is SharpTSArray or string or SharpTSTypedArray or SharpTSBuffer)
return value;

return new SharpTSArray(GetIterableElements(value).ToList());
}

/// <summary>
/// Iterates over elements with proper break/continue handling.
/// </summary>
Expand Down
15 changes: 13 additions & 2 deletions Parsing/Parser.Destructuring.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,20 @@ private Stmt DesugarArrayPattern(ArrayPattern pattern, Expr initializer)
{
List<Stmt> 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)
Expand Down
11 changes: 11 additions & 0 deletions Runtime/BuiltIns/BuiltInNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -216,5 +216,16 @@ public static class BuiltInNames
/// </summary>
public const string ObjectRest = "__objectRest";

/// <summary>
/// Normalizes an array-destructuring source through the iterator protocol.
/// Array binding patterns (<c>const [a, b] = src</c>) 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 <c>[Symbol.iterator]</c>) 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.
/// </summary>
public const string ArrayDestructure = "__arrayDestructure";

#endregion
}
13 changes: 13 additions & 0 deletions Runtime/BuiltIns/GlobalFunctionHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -373,6 +374,18 @@ private static async ValueTask<RuntimeValue> HandleObjectRest(
throw new Exception($"{BuiltInNames.ObjectRest} requires 2 arguments");
}

private static async ValueTask<RuntimeValue> HandleArrayDestructure(
Func<Expr, ValueTask<RuntimeValue>> evaluateArg,
IReadOnlyList<Expr> 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));
}

/// <summary>
/// Implements the CommonJS <c>require(specifier)</c> function. Resolves the module relative
/// to the calling module and returns its <c>module.exports</c> value. Specifier must be a
Expand Down
Loading
Loading