From 284a57f665874808dcffc71843144d28ad227a09 Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Sun, 26 Apr 2026 23:54:48 +0100 Subject: [PATCH 1/2] Support member expression destructuring targets - Allow destructuring assignments into member expressions - Handle object, computed, and default-value targets --- source/units/Goccia.AST.Expressions.pas | 23 +++ source/units/Goccia.Compiler.Expressions.pas | 30 +++- source/units/Goccia.Evaluator.pas | 23 +++ source/units/Goccia.Parser.pas | 31 ++++ .../member-expression-targets.js | 136 ++++++++++++++++++ 5 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 tests/language/expressions/destructuring/member-expression-targets.js diff --git a/source/units/Goccia.AST.Expressions.pas b/source/units/Goccia.AST.Expressions.pas index 505d328c..486b5fa6 100644 --- a/source/units/Goccia.AST.Expressions.pas +++ b/source/units/Goccia.AST.Expressions.pas @@ -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 @@ -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); @@ -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); diff --git a/source/units/Goccia.Compiler.Expressions.pas b/source/units/Goccia.Compiler.Expressions.pas index af79e29e..077871a1 100644 --- a/source/units/Goccia.Compiler.Expressions.pas +++ b/source/units/Goccia.Compiler.Expressions.pas @@ -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; @@ -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; diff --git a/source/units/Goccia.Evaluator.pas b/source/units/Goccia.Evaluator.pas index ab2e26ab..75055903 100644 --- a/source/units/Goccia.Evaluator.pas +++ b/source/units/Goccia.Evaluator.pas @@ -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 diff --git a/source/units/Goccia.Parser.pas b/source/units/Goccia.Parser.pas index 340e603b..1cde69f2 100644 --- a/source/units/Goccia.Parser.pas +++ b/source/units/Goccia.Parser.pas @@ -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); diff --git a/tests/language/expressions/destructuring/member-expression-targets.js b/tests/language/expressions/destructuring/member-expression-targets.js new file mode 100644 index 00000000..c210032b --- /dev/null +++ b/tests/language/expressions/destructuring/member-expression-targets.js @@ -0,0 +1,136 @@ +/*--- +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); +}); From 6de500c424283727d02e52a381b10de667ec2a7b Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Mon, 27 Apr 2026 11:35:02 +0100 Subject: [PATCH 2/2] Add destructuring tests for member expression defaults - Cover static and computed member targets - Verify default values apply when source values are undefined --- .../destructuring/member-expression-targets.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/language/expressions/destructuring/member-expression-targets.js b/tests/language/expressions/destructuring/member-expression-targets.js index c210032b..36571ff1 100644 --- a/tests/language/expressions/destructuring/member-expression-targets.js +++ b/tests/language/expressions/destructuring/member-expression-targets.js @@ -134,3 +134,19 @@ test("member expression targets with default values", () => { 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); +});