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
33 changes: 22 additions & 11 deletions Execution/Interpreter.Statements.cs
Original file line number Diff line number Diff line change
Expand Up @@ -961,20 +961,31 @@ 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, typed arrays and buffers pass through unchanged
/// (fast path); any other source — including <b>strings</b> — is materialized into a
/// <see cref="SharpTSArray"/> via <see cref="GetIterableElements"/> (the same routine spread uses)
/// so the index access reads the iterated elements. Strings are intentionally materialized (not on
/// the fast path) so a rest element binds a fresh array of characters rather than the trailing
/// substring (<c>const [a, ...rest] = "hi"</c> → <c>rest = ["i"]</c>), matching ECMA-262 (#753);
/// non-rest character values are identical either way. A genuinely non-iterable source throws "is
/// not iterable", matching JS; the type checker already rejects those statically except behind
/// <c>any</c>.
/// correct for index-addressable sources. Plain arrays pass through unchanged (fast path). Typed
/// arrays and buffers are index-addressable but NOT iterable via <see cref="GetIterableElements"/>,
/// so they are materialized element-by-element into a fresh <see cref="SharpTSArray"/>; any other
/// source — including <b>strings</b> — is materialized via <see cref="GetIterableElements"/> (the
/// same routine spread uses) so the index access reads the iterated elements. Strings (#753) and
/// typed arrays/buffers (#781) are intentionally materialized (not on the fast path) so a rest
/// element binds a fresh <c>Array</c> rather than the trailing substring / typed-array slice
/// (<c>const [a, ...rest] = "hi"</c> → <c>rest = ["i"]</c>; <c>const [a, ...rest] = u8</c> →
/// <c>rest</c> is a real Array), matching ECMA-262; non-rest element values are identical either
/// way. 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 SharpTSTypedArray or SharpTSBuffer)
return value;
switch (value)
{
case SharpTSArray:
return value;
// Typed arrays / buffers expose index access but no [Symbol.iterator] in this runtime, so
// GetIterableElements would throw; read their elements directly into a fresh Array (#781).
case SharpTSTypedArray typed:
return typed.ToArray();
case SharpTSBuffer buffer:
return new SharpTSArray(buffer.Data.Select(b => (object?)(double)b).ToList());
}

return new SharpTSArray(GetIterableElements(value).ToList());
}
Expand Down
21 changes: 18 additions & 3 deletions Parsing/AST.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,18 @@ public record Comma(Expr Left, Expr Right) : Expr;
/// <c>Assignments</c>, then yield <c>ResultValue</c>. <c>Assignments</c> contains only synthesized
/// <see cref="Stmt.Var"/> and <see cref="Stmt.Expression"/> statements (no control flow), so it is
/// safe in any expression position.
/// </summary>
public record DestructuringAssign(List<Stmt> Assignments, Expr ResultValue) : Expr;
/// <para><paramref name="RawTarget"/>/<paramref name="RawDefault"/> retain the un-lowered pattern and
/// default RHS so that when this node was eager-parsed as a nested element WITH a default
/// (<c>[[a] = []]</c>, <c>{p: {x} = {}}</c>) the outer pattern walk can re-lower the inner pattern
/// against the defaulted access instead of the pre-built (wrong-source) statements (#779). Both are
/// null for a top-level assignment-destructuring, where only <c>Assignments</c>/<c>ResultValue</c> are
/// used; backends never read them.</para>
/// </summary>
public record DestructuringAssign(
List<Stmt> Assignments,
Expr ResultValue,
Expr? RawTarget = null,
Expr? RawDefault = null) : Expr;
public record Binary(Expr Left, Token Operator, Expr Right) : Expr;
public record Logical(Expr Left, Token Operator, Expr Right) : Expr;
public record NullishCoalescing(Expr Left, Expr Right) : Expr;
Expand Down Expand Up @@ -189,12 +199,17 @@ public enum ObjectPropertyKind { Value, Getter, Setter, Method }
/// <param name="IsSpread">Whether this is a spread property (...obj)</param>
/// <param name="Kind">The kind of property (value, getter, setter, method)</param>
/// <param name="SetterParam">The setter parameter (for Kind=Setter only)</param>
/// <param name="IsShorthandDefault">True for the cover-grammar form <c>{ a = 5 }</c> (an ES
/// CoverInitializedName). The value is stored as <c>Expr.Assign(a, 5)</c> so it round-trips to the
/// #754 assignment-destructuring lowering; this flag distinguishes it from the legal expression
/// <c>{ a: a = 5 }</c> so a pure-expression object literal can be rejected as tsc does (#780).</param>
public record Property(
PropertyKey? Key,
Expr Value,
bool IsSpread = false,
ObjectPropertyKind Kind = ObjectPropertyKind.Value,
Stmt.Parameter? SetterParam = null);
Stmt.Parameter? SetterParam = null,
bool IsShorthandDefault = false);
public record GetIndex(Expr Object, Expr Index, bool Optional = false) : Expr;
public record SetIndex(Expr Object, Expr Index, Expr Value) : Expr;
public record Super(Token Keyword, Token? Method) : Expr; // Method is null for super() constructor calls
Expand Down
51 changes: 36 additions & 15 deletions Parsing/Parser.Destructuring.cs
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,10 @@ [new Expr.Literal((double)index)]

if (element.Pattern is IdentifierPattern id)
{
// Apply default value if present
// Apply default value if present (undefined-only, #784)
if (id.DefaultValue != null)
{
accessExpr = new Expr.NullishCoalescing(accessExpr, id.DefaultValue);
accessExpr = DefaultIfUndefined(accessExpr, id.DefaultValue, statements, pattern.Line);
}
statements.Add(new Stmt.Var(id.Name, null, accessExpr));
}
Expand Down Expand Up @@ -247,11 +247,11 @@ [new Expr.Variable(temp), excludeKeysExpr]

if (prop.Value is IdentifierPattern id)
{
// Apply default value if present
// Apply default value if present (undefined-only, #784)
Expr? defaultVal = prop.DefaultValue ?? id.DefaultValue;
if (defaultVal != null)
{
accessExpr = new Expr.NullishCoalescing(accessExpr, defaultVal);
accessExpr = DefaultIfUndefined(accessExpr, defaultVal, statements, pattern.Line);
}
statements.Add(new Stmt.Var(id.Name, null, accessExpr));
}
Expand Down Expand Up @@ -290,7 +290,9 @@ private Expr BuildDestructuringAssignment(Expr pattern, Expr value, int line)
Token rhsTemp = GenerateTempVar(line);
stmts.Add(new Stmt.Var(rhsTemp, null, value));
LowerAssignmentTarget(pattern, new Expr.Variable(rhsTemp), stmts, line);
return new Expr.DestructuringAssign(stmts, new Expr.Variable(rhsTemp));
// Retain the raw (pattern, default) so this node can be re-lowered if it turns out to be a nested
// element WITH a default (`[[a] = []]`) — see LowerElementWithDefault (#779).
return new Expr.DestructuringAssign(stmts, new Expr.Variable(rhsTemp), RawTarget: pattern, RawDefault: value);
}

private void LowerAssignmentTarget(Expr target, Expr source, List<Stmt> stmts, int line)
Expand Down Expand Up @@ -352,38 +354,57 @@ private void LowerObjectAssignmentTarget(Expr.ObjectLiteral obj, Expr source, Li

// Lowers one pattern element / property value to an assignment, peeling off a default (`= expr`)
// that the eager parser captured as an Assign (variable target) or Set/SetIndex/SetPrivate (member
// target). `?? default` applies the default when the read is null/undefined (matching the existing
// declaration desugaring; ECMA-262 is undefined-only — tracked separately).
// target). The default is applied only when the read is `undefined` (ECMA-262 AssignmentElement),
// via the shared DefaultIfUndefined helper (#784).
private void LowerElementWithDefault(Expr element, Expr access, List<Stmt> stmts, int line)
{
switch (Unwrap(element))
{
case Expr.Assign def:
LowerAssignmentTarget(new Expr.Variable(def.Name),
new Expr.NullishCoalescing(access, def.Value), stmts, line);
DefaultIfUndefined(access, def.Value, stmts, line), stmts, line);
break;
case Expr.Set def:
LowerAssignmentTarget(new Expr.Get(def.Object, def.Name),
new Expr.NullishCoalescing(access, def.Value), stmts, line);
DefaultIfUndefined(access, def.Value, stmts, line), stmts, line);
break;
case Expr.SetIndex def:
LowerAssignmentTarget(new Expr.GetIndex(def.Object, def.Index),
new Expr.NullishCoalescing(access, def.Value), stmts, line);
DefaultIfUndefined(access, def.Value, stmts, line), stmts, line);
break;
case Expr.SetPrivate def:
LowerAssignmentTarget(new Expr.GetPrivate(def.Object, def.Name),
new Expr.NullishCoalescing(access, def.Value), stmts, line);
DefaultIfUndefined(access, def.Value, stmts, line), stmts, line);
break;
case Expr.DestructuringAssign { RawTarget: { } rawTarget, RawDefault: { } rawDefault }:
// A nested pattern WITH a default (`[[a] = []]`, `{p: {x} = {}}`): the eager parser lowered
// the inner `[a] = []` to a DestructuringAssign but kept its raw (target, default), so
// re-lower the inner pattern against the defaulted access — discarding the inner node's
// pre-built statements, which bound against the wrong (inner-rhs) source (#779).
LowerAssignmentTarget(rawTarget, DefaultIfUndefined(access, rawDefault, stmts, line), stmts, line);
break;
case Expr.DestructuringAssign:
// A nested pattern WITH a default (`[[a] = []]`, `{p: {x} = {}}`) was eagerly lowered,
// losing its raw target; recovering it needs cover-grammar reparsing. Rare — see #779.
throw new Exception("Nested destructuring with a default is not supported in assignment destructuring.");
default:
LowerAssignmentTarget(element, access, stmts, line);
break;
}
}

// Destructuring defaults are applied ONLY when the matched value is `undefined`, never `null`
// (ECMA-262 ArrayBindingPattern / ObjectBindingPattern / AssignmentElement) — unlike `??`, which
// also replaces null. The access is spilled into a temp so it is evaluated exactly once (a property
// read may invoke a getter) and the ternary's repeated operand is a side-effect-free temp read.
// Shared by both the declaration desugaring and the #754 assignment-destructuring lowering. #784
private Expr DefaultIfUndefined(Expr access, Expr defaultValue, List<Stmt> stmts, int line)
{
Token valueTemp = GenerateTempVar(line);
stmts.Add(new Stmt.Var(valueTemp, null, access));
Expr isUndefined = new Expr.Binary(
new Expr.Variable(valueTemp),
new Token(TokenType.EQUAL_EQUAL_EQUAL, "===", null, line),
new Expr.Literal(SharpTS.Runtime.Types.SharpTSUndefined.Instance));
return new Expr.Ternary(isUndefined, defaultValue, new Expr.Variable(valueTemp));
}

private static Expr MakeAssignmentExpr(Expr target, Expr value) => Unwrap(target) switch
{
Expr.Variable v => new Expr.Assign(v.Name, value),
Expand Down
13 changes: 12 additions & 1 deletion Parsing/Parser.Expressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -954,6 +954,7 @@ private Expr Primary()
// Parse regular identifier (including 'get' and 'set' as property names)
Token name = ConsumePropertyName("Expect property name.");
Expr value;
bool isShorthandDefault = false;

if (Check(TokenType.LEFT_PAREN))
{
Expand All @@ -965,13 +966,23 @@ private Expr Primary()
// Explicit property: { x: value }
value = Expression();
}
else if (Match(TokenType.EQUAL))
{
// Cover-grammar shorthand-with-default: `{ x = 5 }` (an ES CoverInitializedName).
// Only valid as an object DESTRUCTURING pattern; stored as `{ x: (x = 5) }` so the
// #754 assignment-destructuring lowering recovers the `(target, default)`. A
// pure-expression `{ x = 5 }` is rejected in CheckObject via IsShorthandDefault (#780).
value = new Expr.Assign(name, Expression());
isShorthandDefault = true;
}
else
{
// Shorthand property: { x } -> { x: x }
value = new Expr.Variable(name);
}

properties.Add(new Expr.Property(new Expr.IdentifierKey(name), value));
properties.Add(new Expr.Property(new Expr.IdentifierKey(name), value,
IsShorthandDefault: isShorthandDefault));
} while (Match(TokenType.COMMA));
}
Consume(TokenType.RIGHT_BRACE, "Expect '}' after object literal.");
Expand Down
Loading
Loading