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
23 changes: 23 additions & 0 deletions source/units/Goccia.AST.Expressions.pas
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,16 @@ TGocciaIdentifierDestructuringPattern = class(TGocciaDestructuringPattern)
property Name: string read FName;
end;

// Member expression pattern: obj.prop, this.x, arr[i], obj[key]
TGocciaMemberExpressionDestructuringPattern = class(TGocciaDestructuringPattern)
private
FExpression: TGocciaMemberExpression;
public
constructor Create(const AExpression: TGocciaMemberExpression; const ALine, AColumn: Integer);
function Evaluate(const AContext: TGocciaEvaluationContext): TGocciaValue; override;
property Expression: TGocciaMemberExpression read FExpression;
end;

// Destructuring assignment expression
TGocciaDestructuringAssignmentExpression = class(TGocciaExpression)
private
Expand Down Expand Up @@ -1148,6 +1158,14 @@ constructor TGocciaIdentifierDestructuringPattern.Create(const AName: string; co
FName := AName;
end;

{ TGocciaMemberExpressionDestructuringPattern }

constructor TGocciaMemberExpressionDestructuringPattern.Create(const AExpression: TGocciaMemberExpression; const ALine, AColumn: Integer);
begin
inherited Create(ALine, AColumn);
FExpression := AExpression;
end;

{ TGocciaDestructuringAssignmentExpression }

constructor TGocciaDestructuringAssignmentExpression.Create(const ALeft: TGocciaDestructuringPattern; const ARight: TGocciaExpression; const ALine, AColumn: Integer);
Expand Down Expand Up @@ -1649,6 +1667,11 @@ function TGocciaIdentifierDestructuringPattern.Evaluate(const AContext: TGocciaE
Result := TGocciaUndefinedLiteralValue.UndefinedValue;
end;

function TGocciaMemberExpressionDestructuringPattern.Evaluate(const AContext: TGocciaEvaluationContext): TGocciaValue;
begin
Result := TGocciaUndefinedLiteralValue.UndefinedValue;
end;

function TGocciaDestructuringAssignmentExpression.Evaluate(const AContext: TGocciaEvaluationContext): TGocciaValue;
begin
Result := EvaluateDestructuringAssignment(Self, AContext);
Expand Down
30 changes: 29 additions & 1 deletion source/units/Goccia.Compiler.Expressions.pas
Original file line number Diff line number Diff line change
Expand Up @@ -691,7 +691,10 @@ procedure CollectDestructuringBindings(const APattern: TGocciaDestructuringPatte
end
else if APattern is TGocciaRestDestructuringPattern then
CollectDestructuringBindings(
TGocciaRestDestructuringPattern(APattern).Argument, AScope, AIsConst);
TGocciaRestDestructuringPattern(APattern).Argument, AScope, AIsConst)
else if APattern is TGocciaMemberExpressionDestructuringPattern then
// Member expression targets assign to existing objects — no bindings to declare
;
end;

procedure EmitDestructuring(const ACtx: TGocciaCompilationContext;
Expand Down Expand Up @@ -826,6 +829,31 @@ procedure EmitDestructuring(const ACtx: TGocciaCompilationContext;
ACtx.CompileExpression(AssignPat.Right, ASrcReg);
PatchJumpTarget(ACtx, JumpIdx);
EmitDestructuring(ACtx, AssignPat.Left, ASrcReg);
end
else if APattern is TGocciaMemberExpressionDestructuringPattern then
begin
DestSlot := ACtx.Scope.AllocateRegister;
ACtx.CompileExpression(
TGocciaMemberExpressionDestructuringPattern(APattern).Expression.ObjectExpr,
DestSlot);
if TGocciaMemberExpressionDestructuringPattern(APattern).Expression.Computed then
begin
IdxReg := ACtx.Scope.AllocateRegister;
ACtx.CompileExpression(
TGocciaMemberExpressionDestructuringPattern(APattern).Expression.PropertyExpression,
IdxReg);
EmitInstruction(ACtx, EncodeABC(OP_ARRAY_SET, DestSlot, IdxReg, ASrcReg));
ACtx.Scope.FreeRegister;
end
else
begin
PropIdx := ACtx.Template.AddConstantString(
TGocciaMemberExpressionDestructuringPattern(APattern).Expression.PropertyName);
if PropIdx > High(UInt8) then
raise Exception.Create('Constant pool overflow: property name index exceeds 255');
EmitInstruction(ACtx, EncodeABC(OP_SET_PROP_CONST, DestSlot, UInt8(PropIdx), ASrcReg));
end;
ACtx.Scope.FreeRegister;
end;
end;

Expand Down
23 changes: 23 additions & 0 deletions source/units/Goccia.Evaluator.pas
Original file line number Diff line number Diff line change
Expand Up @@ -4145,10 +4145,33 @@ procedure AssignVariablePattern(const APattern: TGocciaDestructuringPattern; con
end;
end;

procedure AssignMemberExpressionPattern(const APattern: TGocciaMemberExpressionDestructuringPattern; const AValue: TGocciaValue; const AContext: TGocciaEvaluationContext);
var
Obj, PropValue: TGocciaValue;
MemberExpr: TGocciaMemberExpression;
begin
MemberExpr := APattern.Expression;
Obj := EvaluateExpression(MemberExpr.ObjectExpr, AContext);
if MemberExpr.Computed then
begin
PropValue := EvaluateExpression(MemberExpr.PropertyExpression, AContext);
if (PropValue is TGocciaSymbolValue) and (Obj is TGocciaObjectValue) then
TGocciaObjectValue(Obj).AssignSymbolProperty(TGocciaSymbolValue(PropValue), AValue)
else if (PropValue is TGocciaSymbolValue) and (Obj is TGocciaClassValue) then
TGocciaClassValue(Obj).AssignSymbolProperty(TGocciaSymbolValue(PropValue), AValue)
else
AssignProperty(Obj, PropValue.ToStringLiteral.Value, AValue, AContext.OnError, APattern.Line, APattern.Column);
end
else
AssignProperty(Obj, MemberExpr.PropertyName, AValue, AContext.OnError, APattern.Line, APattern.Column);
end;

procedure AssignPattern(const APattern: TGocciaDestructuringPattern; const AValue: TGocciaValue; const AContext: TGocciaEvaluationContext; const AIsDeclaration: Boolean = False; const ADeclarationType: TGocciaDeclarationType = dtLet);
begin
if APattern is TGocciaIdentifierDestructuringPattern then
AssignIdentifierPattern(TGocciaIdentifierDestructuringPattern(APattern), AValue, AContext, AIsDeclaration, ADeclarationType)
else if APattern is TGocciaMemberExpressionDestructuringPattern then
AssignMemberExpressionPattern(TGocciaMemberExpressionDestructuringPattern(APattern), AValue, AContext)
else if APattern is TGocciaArrayDestructuringPattern then
AssignArrayPattern(TGocciaArrayDestructuringPattern(APattern), AValue, AContext, AIsDeclaration, ADeclarationType)
else if APattern is TGocciaObjectDestructuringPattern then
Expand Down
31 changes: 31 additions & 0 deletions source/units/Goccia.Parser.pas
Original file line number Diff line number Diff line change
Expand Up @@ -5093,6 +5093,37 @@ function TGocciaParser.ConvertToPattern(const AExpr: TGocciaExpression): TGoccia

Result := TGocciaObjectDestructuringPattern.Create(Properties, AExpr.Line, AExpr.Column);
end
else if AExpr is TGocciaMemberExpression then
begin
Result := TGocciaMemberExpressionDestructuringPattern.Create(
TGocciaMemberExpression(AExpr), AExpr.Line, AExpr.Column);
end
else if AExpr is TGocciaPropertyAssignmentExpression then
begin
// obj.prop = default -> member expression pattern with default value
Result := TGocciaAssignmentDestructuringPattern.Create(
TGocciaMemberExpressionDestructuringPattern.Create(
TGocciaMemberExpression.Create(
TGocciaPropertyAssignmentExpression(AExpr).ObjectExpr,
TGocciaPropertyAssignmentExpression(AExpr).PropertyName,
False, AExpr.Line, AExpr.Column),
AExpr.Line, AExpr.Column),
TGocciaPropertyAssignmentExpression(AExpr).Value,
AExpr.Line, AExpr.Column);
end
else if AExpr is TGocciaComputedPropertyAssignmentExpression then
begin
// obj[key] = default -> computed member expression pattern with default value
Result := TGocciaAssignmentDestructuringPattern.Create(
TGocciaMemberExpressionDestructuringPattern.Create(
TGocciaMemberExpression.Create(
TGocciaComputedPropertyAssignmentExpression(AExpr).ObjectExpr,
TGocciaComputedPropertyAssignmentExpression(AExpr).PropertyExpression,
AExpr.Line, AExpr.Column),
AExpr.Line, AExpr.Column),
TGocciaComputedPropertyAssignmentExpression(AExpr).Value,
AExpr.Line, AExpr.Column);
end
else
raise TGocciaSyntaxError.Create('Invalid destructuring target', AExpr.Line, AExpr.Column, FFileName, FSourceLines,
SSuggestDestructuringInvalidTarget);
Expand Down
152 changes: 152 additions & 0 deletions tests/language/expressions/destructuring/member-expression-targets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*---
description: Destructuring assignment into member expression targets
features: [destructuring]
---*/

test("array destructuring into this.x properties", () => {
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
swap() {
[this.x, this.y] = [this.y, this.x];
}
}

const p = new Point(1, 2);
expect(p.x).toBe(1);
expect(p.y).toBe(2);
p.swap();
expect(p.x).toBe(2);
expect(p.y).toBe(1);
});

test("array destructuring into obj.prop targets", () => {
const obj = { a: 0, b: 0 };
[obj.a, obj.b] = [10, 20];
expect(obj.a).toBe(10);
expect(obj.b).toBe(20);
});

test("array destructuring into arr[i] targets", () => {
const arr = [0, 0, 0];
[arr[0], arr[1], arr[2]] = [7, 8, 9];
expect(arr[0]).toBe(7);
expect(arr[1]).toBe(8);
expect(arr[2]).toBe(9);
});

test("array destructuring into computed obj[key] targets", () => {
const obj = {};
const k1 = "x";
const k2 = "y";
[obj[k1], obj[k2]] = [100, 200];
expect(obj.x).toBe(100);
expect(obj.y).toBe(200);
});

test("object destructuring into member expression targets", () => {
const target = {};
const source = { a: 1, b: 2, c: 3 };
({ a: target.a, b: target.b, c: target.c } = source);
expect(target.a).toBe(1);
expect(target.b).toBe(2);
expect(target.c).toBe(3);
});

test("mixed member expressions and identifiers in array pattern", () => {
const obj = {};
let z;
[obj.x, z, obj.y] = [1, 2, 3];
expect(obj.x).toBe(1);
expect(z).toBe(2);
expect(obj.y).toBe(3);
});

test("mixed member expressions and identifiers in object pattern", () => {
const target = {};
let local;
({ a: target.prop, b: local } = { a: 42, b: 99 });
expect(target.prop).toBe(42);
expect(local).toBe(99);
});

test("nested destructuring with member expression leaf targets", () => {
const obj = {};
[obj.a, [obj.b, obj.c]] = [1, [2, 3]];
expect(obj.a).toBe(1);
expect(obj.b).toBe(2);
expect(obj.c).toBe(3);
});

test("rest element with member expression target", () => {
const obj = {};
[obj.first, ...obj.rest] = [1, 2, 3, 4];
expect(obj.first).toBe(1);
expect(obj.rest).toEqual([2, 3, 4]);
});

test("fibonacci iterator using this.a/this.b swap", () => {
class FibIterator {
constructor() {
this.a = 0;
this.b = 1;
}
next() {
const value = this.a;
[this.a, this.b] = [this.b, this.a + this.b];
return { value, done: false };
}
[Symbol.iterator]() {
return this;
}
}

const fib = new FibIterator();
const results = Array.from({ length: 8 }, () => fib.next().value);
expect(results).toEqual([0, 1, 1, 2, 3, 5, 8, 13]);
});

test("evaluation order: RHS evaluated before targets assigned", () => {
const obj = { x: 1, y: 2 };
[obj.x, obj.y] = [obj.y, obj.x];
expect(obj.x).toBe(2);
expect(obj.y).toBe(1);
});

test("computed property with expression evaluated once per target", () => {
const obj = {};
let counter = 0;
const key = () => {
counter++;
return "k" + counter;
};
[obj[key()], obj[key()]] = ["a", "b"];
expect(obj.k1).toBe("a");
expect(obj.k2).toBe("b");
expect(counter).toBe(2);
});

test("member expression targets with default values", () => {
const obj = {};
[obj.a = 10, obj.b = 20] = [1, undefined];
expect(obj.a).toBe(1);
expect(obj.b).toBe(20);
});

test("object member expression target with default values (static key)", () => {
const obj = {};
({ a: obj.a = 10, b: obj.b = 20 } = { a: 1, b: undefined });
expect(obj.a).toBe(1);
expect(obj.b).toBe(20);
});

test("object member expression target with default values (computed key)", () => {
const obj = {};
const k1 = "x";
const k2 = "y";
({ a: obj[k1] = 10, b: obj[k2] = 20 } = { a: 1, b: undefined });
expect(obj.x).toBe(1);
expect(obj.y).toBe(20);
});
Loading