From 6d5fe06f5e5f2d5a9eb446b2343dd839dee53139 Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Tue, 19 May 2026 23:35:26 +0100 Subject: [PATCH 1/8] Fix computed class field yield evaluation Resolve computed public class field names during class definition so yield resumes correctly for instance and static fields in both interpreted and bytecode execution. --- source/units/Goccia.AST.Expressions.pas | 1 + source/units/Goccia.AST.Statements.pas | 4 + source/units/Goccia.Bytecode.OpCodeNames.pas | 1 + source/units/Goccia.Bytecode.pas | 5 +- source/units/Goccia.Compiler.Expressions.pas | 7 +- source/units/Goccia.Compiler.Statements.pas | 131 ++++++++++++++++-- source/units/Goccia.Evaluator.pas | 122 +++++++++++++--- source/units/Goccia.Parser.pas | 72 ++++++++-- source/units/Goccia.VM.pas | 41 ++++++ source/units/Goccia.Values.ClassValue.pas | 6 + .../class-computed-field-yield-order.js | 69 +++++++++ .../function-keyword/async-function.js | 9 ++ .../class-computed-field-yield.js | 73 ++++++++++ .../language/objects/basic-object-creation.js | 14 ++ 14 files changed, 510 insertions(+), 45 deletions(-) create mode 100644 tests/language/classes/class-computed-field-yield-order.js create mode 100644 tests/language/function-keyword/class-computed-field-yield.js diff --git a/source/units/Goccia.AST.Expressions.pas b/source/units/Goccia.AST.Expressions.pas index 5ad3af49..05350aa8 100644 --- a/source/units/Goccia.AST.Expressions.pas +++ b/source/units/Goccia.AST.Expressions.pas @@ -313,6 +313,7 @@ TGocciaPropertySourceOrder = record PropertyType: TGocciaPropertySourceType; StaticKey: string; // For static properties, getters, setters ComputedIndex: Integer; // Index into ComputedProperties list + Expression: TGocciaExpression; // Value expression for static properties end; TGocciaObjectExpression = class(TGocciaExpression) diff --git a/source/units/Goccia.AST.Statements.pas b/source/units/Goccia.AST.Statements.pas index 73f14ddc..5724b3a2 100644 --- a/source/units/Goccia.AST.Statements.pas +++ b/source/units/Goccia.AST.Statements.pas @@ -285,6 +285,10 @@ TGocciaClassElement = record TGocciaFieldOrderEntry = record Name: string; IsPrivate: Boolean; + IsComputed: Boolean; + ElementIndex: Integer; + ComputedKeyExpression: TGocciaExpression; + FieldInitializer: TGocciaExpression; end; // Shared class definition structure diff --git a/source/units/Goccia.Bytecode.OpCodeNames.pas b/source/units/Goccia.Bytecode.OpCodeNames.pas index 11010e7a..8ebcc7f5 100644 --- a/source/units/Goccia.Bytecode.OpCodeNames.pas +++ b/source/units/Goccia.Bytecode.OpCodeNames.pas @@ -141,6 +141,7 @@ function OpCodeName(const AOp: UInt8): string; OP_DELETE_GLOBAL: Result := 'OP_DELETE_GLOBAL'; OP_DEFINE_STATIC_PROP_CONST: Result := 'OP_DEFINE_STATIC_PROP_CONST'; OP_DEFINE_STATIC_METHOD_CONST: Result := 'OP_DEFINE_STATIC_METHOD_CONST'; + OP_DEFINE_PROP_DYNAMIC: Result := 'OP_DEFINE_PROP_DYNAMIC'; OP_ADD: Result := 'OP_ADD'; OP_SUB: Result := 'OP_SUB'; OP_MUL: Result := 'OP_MUL'; diff --git a/source/units/Goccia.Bytecode.pas b/source/units/Goccia.Bytecode.pas index 14a1d8c3..772eadc4 100644 --- a/source/units/Goccia.Bytecode.pas +++ b/source/units/Goccia.Bytecode.pas @@ -53,7 +53,9 @@ interface // OP_DEFINE_STATIC_METHOD_CONST so class static fields and // methods define own data properties instead of using ordinary // assignment semantics. - GOCCIA_FORMAT_VERSION = 32; + // v32 -> v33: added OP_DEFINE_PROP_DYNAMIC for computed public + // class fields. + GOCCIA_FORMAT_VERSION = 33; GOCCIA_BINARY_MAGIC: array[0..3] of Byte = (Ord('G'), Ord('B'), Ord('C'), 0); GOCCIA_NULLISH_MATCH_UNDEFINED = 0; GOCCIA_NULLISH_MATCH_NULL = 1; @@ -216,6 +218,7 @@ interface OP_DELETE_GLOBAL = 122, OP_DEFINE_STATIC_PROP_CONST = 123, OP_DEFINE_STATIC_METHOD_CONST = 124, + OP_DEFINE_PROP_DYNAMIC = 125, OP_ADD = 128, OP_SUB = 129, OP_MUL = 130, diff --git a/source/units/Goccia.Compiler.Expressions.pas b/source/units/Goccia.Compiler.Expressions.pas index 0017163e..bff90c7e 100644 --- a/source/units/Goccia.Compiler.Expressions.pas +++ b/source/units/Goccia.Compiler.Expressions.pas @@ -2594,8 +2594,13 @@ procedure CompileObject(const ACtx: TGocciaCompilationContext; Key := Order[I].StaticKey; case Order[I].PropertyType of pstStatic: - if AExpr.Properties.TryGetValue(Key, ValExpr) then + begin + ValExpr := Order[I].Expression; + if not Assigned(ValExpr) then + AExpr.Properties.TryGetValue(Key, ValExpr); + if Assigned(ValExpr) then CompileObjectProperty(ACtx, AExpr, ADest, Key, ValExpr); + end; pstComputed: begin if (Order[I].ComputedIndex >= 0) and diff --git a/source/units/Goccia.Compiler.Statements.pas b/source/units/Goccia.Compiler.Statements.pas index 0f35924b..bea86fcd 100644 --- a/source/units/Goccia.Compiler.Statements.pas +++ b/source/units/Goccia.Compiler.Statements.pas @@ -124,6 +124,13 @@ TPreallocatedUsingDisposeSlot = record ResourceRegistered: Boolean; end; + TComputedFieldKeyLocal = record + ElementIndex: Integer; + Name: string; + end; + + TComputedFieldKeyLocals = array of TComputedFieldKeyLocal; + TPendingFinallyEntry = record FinallyBlock: TGocciaBlockStatement; // Non-nil when this entry represents a using block's disposal. @@ -3609,23 +3616,52 @@ procedure CompileComputedMethodBody(const ACtx: TGocciaCompilationContext; ACtx.Scope.FreeRegister; end; +function FindComputedFieldKeyLocalName( + const ALocals: TComputedFieldKeyLocals; + const AElementIndex: Integer): string; +var + LocalIndex: Integer; +begin + for LocalIndex := 0 to High(ALocals) do + if ALocals[LocalIndex].ElementIndex = AElementIndex then + Exit(ALocals[LocalIndex].Name); + Result := ''; +end; + procedure CompileComputedElements(const ACtx: TGocciaCompilationContext; - const ATargetReg: UInt8; const AClassDef: TGocciaClassDefinition); + const ATargetReg: UInt8; const AClassDef: TGocciaClassDefinition; + var AComputedFieldKeyLocals: TComputedFieldKeyLocals); var I: Integer; Elem: TGocciaClassElement; KeyReg: UInt8; + ComputedKeyName: string; begin + SetLength(AComputedFieldKeyLocals, 0); for I := 0 to High(AClassDef.FElements) do begin Elem := AClassDef.FElements[I]; if not Elem.IsComputed then Continue; + if not (Elem.Kind in [cekGetter, cekSetter, cekMethod, cekField]) then + Continue; - KeyReg := ACtx.Scope.AllocateRegister; + if Elem.Kind = cekField then + begin + SetLength(AComputedFieldKeyLocals, Length(AComputedFieldKeyLocals) + 1); + ComputedKeyName := Format('#computed-field-key:%d', [I]); + AComputedFieldKeyLocals[High(AComputedFieldKeyLocals)].ElementIndex := I; + AComputedFieldKeyLocals[High(AComputedFieldKeyLocals)].Name := + ComputedKeyName; + KeyReg := ACtx.Scope.DeclareLocal(ComputedKeyName, False); + end + else + KeyReg := ACtx.Scope.AllocateRegister; ACtx.CompileExpression(Elem.ComputedKeyExpression, KeyReg); case Elem.Kind of + cekField: + Continue; cekGetter: if Elem.IsStatic then CompileComputedGetterBody(ACtx, ATargetReg, KeyReg, @@ -3662,8 +3698,20 @@ function HasAccessorInitializers( Result := False; end; +function HasComputedInstanceFields( + const AClassDef: TGocciaClassDefinition): Boolean; +var + I: Integer; +begin + for I := 0 to High(AClassDef.FFieldOrder) do + if AClassDef.FFieldOrder[I].IsComputed then + Exit(True); + Result := False; +end; + procedure CompileFieldInitializer(const ACtx: TGocciaCompilationContext; - const AClassReg: UInt8; const AClassDef: TGocciaClassDefinition); + const AClassReg: UInt8; const AClassDef: TGocciaClassDefinition; + const AComputedFieldKeyLocals: TComputedFieldKeyLocals); var OldTemplate: TGocciaFunctionTemplate; OldScope: TGocciaCompilerScope; @@ -3672,12 +3720,13 @@ procedure CompileFieldInitializer(const ACtx: TGocciaCompilationContext; ChildCtx: TGocciaCompilationContext; FuncIdx: UInt16; FnReg: UInt8; - ValReg, ThisReg: UInt8; + ValReg, ThisReg, KeyReg: UInt8; KeyIdx: UInt16; - I: Integer; + I, UpvalueIdx: Integer; Entry: TGocciaExpressionMap.TKeyValuePair; Elem: TGocciaClassElement; FieldExpr: TGocciaExpression; + ComputedKeyName: string; begin OldTemplate := ACtx.Template; OldScope := ACtx.Scope; @@ -3702,7 +3751,27 @@ procedure CompileFieldInitializer(const ACtx: TGocciaCompilationContext; for I := 0 to High(AClassDef.FFieldOrder) do begin ValReg := ChildScope.AllocateRegister; - if AClassDef.FFieldOrder[I].IsPrivate then + if AClassDef.FFieldOrder[I].IsComputed then + begin + if Assigned(AClassDef.FFieldOrder[I].FieldInitializer) then + ACtx.CompileExpression(AClassDef.FFieldOrder[I].FieldInitializer, ValReg) + else + EmitInstruction(ChildCtx, EncodeABx(OP_LOAD_UNDEFINED, ValReg, 0)); + KeyReg := ChildScope.AllocateRegister; + ComputedKeyName := FindComputedFieldKeyLocalName( + AComputedFieldKeyLocals, AClassDef.FFieldOrder[I].ElementIndex); + UpvalueIdx := ChildScope.ResolveUpvalue(ComputedKeyName); + if UpvalueIdx < 0 then + raise Exception.Create('Compiler error: computed class field key was not captured'); + EmitInstruction(ChildCtx, EncodeABx(OP_GET_UPVALUE, KeyReg, + UInt16(UpvalueIdx))); + EmitInstruction(ChildCtx, EncodeABC(OP_DEFINE_PROP_DYNAMIC, ThisReg, + KeyReg, ValReg)); + ChildScope.FreeRegister; + ChildScope.FreeRegister; + Continue; + end + else if AClassDef.FFieldOrder[I].IsPrivate then begin if AClassDef.PrivateInstanceProperties.TryGetValue( AClassDef.FFieldOrder[I].Name, FieldExpr) then @@ -4053,7 +4122,7 @@ procedure CompileClassDeclaration(const ACtx: TGocciaCompilationContext; const AStmt: TGocciaClassDeclaration); var ClassDef: TGocciaClassDefinition; - ClassReg, SuperReg, ValReg: UInt8; + ClassReg, SuperReg, ValReg, KeyReg: UInt8; NameIdx, KeyIdx: UInt16; MethodPair: TGocciaClassMethodMap.TKeyValuePair; GetterPair: TGocciaGetterExpressionMap.TKeyValuePair; @@ -4062,6 +4131,8 @@ procedure CompileClassDeclaration(const ACtx: TGocciaCompilationContext; I, LocalIdx, UpvalIdx: Integer; HasSuper: Boolean; PrivPrefix: string; + ComputedFieldKeyLocals: TComputedFieldKeyLocals; + ComputedKeyName: string; begin ClassDef := AStmt.ClassDefinition; HasSuper := ClassDef.SuperClass <> ''; @@ -4166,10 +4237,13 @@ procedure CompileClassDeclaration(const ACtx: TGocciaCompilationContext; SetterPair.Value, OP_DEFINE_ACCESSOR_CONST, ACCESSOR_FLAG_STATIC or ACCESSOR_FLAG_SETTER); end; + CompileComputedElements(ACtx, ClassReg, ClassDef, ComputedFieldKeyLocals); + if (ClassDef.InstanceProperties.Count > 0) or (ClassDef.PrivateInstanceProperties.Count > 0) or + HasComputedInstanceFields(ClassDef) or HasAccessorInitializers(ClassDef) then - CompileFieldInitializer(ACtx, ClassReg, ClassDef); + CompileFieldInitializer(ACtx, ClassReg, ClassDef, ComputedFieldKeyLocals); // Static fields without FElements entries (legacy / no static blocks) if Length(ClassDef.FElements) = 0 then @@ -4201,8 +4275,6 @@ procedure CompileClassDeclaration(const ACtx: TGocciaCompilationContext; end; end; - CompileComputedElements(ACtx, ClassReg, ClassDef); - // ES2022 §15.7.14: compile static fields and static blocks in source order for I := 0 to High(ClassDef.FElements) do begin @@ -4211,6 +4283,17 @@ procedure CompileClassDeclaration(const ACtx: TGocciaCompilationContext; else if (ClassDef.FElements[I].Kind = cekField) and ClassDef.FElements[I].IsStatic then begin ValReg := ACtx.Scope.AllocateRegister; + if ClassDef.FElements[I].IsComputed then + begin + ComputedKeyName := FindComputedFieldKeyLocalName( + ComputedFieldKeyLocals, I); + LocalIdx := ACtx.Scope.ResolveLocal(ComputedKeyName); + if LocalIdx < 0 then + raise Exception.Create('Compiler error: computed static field key was not captured'); + KeyReg := ACtx.Scope.GetLocal(LocalIdx).Slot; + end + else + KeyReg := 0; if Assigned(ClassDef.FElements[I].FieldInitializer) then ACtx.CompileExpression(ClassDef.FElements[I].FieldInitializer, ValReg) else @@ -4226,6 +4309,9 @@ procedure CompileClassDeclaration(const ACtx: TGocciaCompilationContext; EmitInstruction(ACtx, EncodeABC(OP_SET_PROP_CONST, ClassReg, UInt8(KeyIdx), ValReg)); end + else if ClassDef.FElements[I].IsComputed then + EmitInstruction(ACtx, EncodeABC(OP_DEFINE_PROP_DYNAMIC, + ClassReg, KeyReg, ValReg)) else begin KeyIdx := ACtx.Template.AddConstantString(ClassDef.FElements[I].Name); @@ -4260,7 +4346,7 @@ procedure CompileClassExpression(const ACtx: TGocciaCompilationContext; const AInferredName: string = ''); var ClassDef: TGocciaClassDefinition; - SuperReg, ValReg: UInt8; + SuperReg, ValReg, KeyReg: UInt8; NameIdx, KeyIdx: UInt16; MethodPair: TGocciaClassMethodMap.TKeyValuePair; GetterPair: TGocciaGetterExpressionMap.TKeyValuePair; @@ -4272,6 +4358,8 @@ procedure CompileClassExpression(const ACtx: TGocciaCompilationContext; HasNameBinding: Boolean; ClosedLocals: array[0..0] of UInt8; ClosedCount, I: Integer; + ComputedFieldKeyLocals: TComputedFieldKeyLocals; + ComputedKeyName: string; begin ClassDef := AClassDef; HasSuper := ClassDef.SuperClass <> ''; @@ -4377,10 +4465,13 @@ procedure CompileClassExpression(const ACtx: TGocciaCompilationContext; SetterPair.Value, OP_DEFINE_ACCESSOR_CONST, ACCESSOR_FLAG_STATIC or ACCESSOR_FLAG_SETTER); end; + CompileComputedElements(ACtx, ADest, ClassDef, ComputedFieldKeyLocals); + if (ClassDef.InstanceProperties.Count > 0) or (ClassDef.PrivateInstanceProperties.Count > 0) or + HasComputedInstanceFields(ClassDef) or HasAccessorInitializers(ClassDef) then - CompileFieldInitializer(ACtx, ADest, ClassDef); + CompileFieldInitializer(ACtx, ADest, ClassDef, ComputedFieldKeyLocals); // Static fields without FElements entries (legacy / no static blocks) if Length(ClassDef.FElements) = 0 then @@ -4412,8 +4503,6 @@ procedure CompileClassExpression(const ACtx: TGocciaCompilationContext; end; end; - CompileComputedElements(ACtx, ADest, ClassDef); - // ES2022 §15.7.14: compile static fields and static blocks in source order for I := 0 to High(ClassDef.FElements) do begin @@ -4422,6 +4511,17 @@ procedure CompileClassExpression(const ACtx: TGocciaCompilationContext; else if (ClassDef.FElements[I].Kind = cekField) and ClassDef.FElements[I].IsStatic then begin ValReg := ACtx.Scope.AllocateRegister; + if ClassDef.FElements[I].IsComputed then + begin + ComputedKeyName := FindComputedFieldKeyLocalName( + ComputedFieldKeyLocals, I); + LocalIdx := ACtx.Scope.ResolveLocal(ComputedKeyName); + if LocalIdx < 0 then + raise Exception.Create('Compiler error: computed static field key was not captured'); + KeyReg := ACtx.Scope.GetLocal(LocalIdx).Slot; + end + else + KeyReg := 0; if Assigned(ClassDef.FElements[I].FieldInitializer) then ACtx.CompileExpression(ClassDef.FElements[I].FieldInitializer, ValReg) else @@ -4437,6 +4537,9 @@ procedure CompileClassExpression(const ACtx: TGocciaCompilationContext; EmitInstruction(ACtx, EncodeABC(OP_SET_PROP_CONST, ADest, UInt8(KeyIdx), ValReg)); end + else if ClassDef.FElements[I].IsComputed then + EmitInstruction(ACtx, EncodeABC(OP_DEFINE_PROP_DYNAMIC, + ADest, KeyReg, ValReg)) else begin KeyIdx := ACtx.Template.AddConstantString(ClassDef.FElements[I].Name); diff --git a/source/units/Goccia.Evaluator.pas b/source/units/Goccia.Evaluator.pas index a23477ac..9a90d323 100644 --- a/source/units/Goccia.Evaluator.pas +++ b/source/units/Goccia.Evaluator.pas @@ -1453,7 +1453,10 @@ function EvaluateObject(const AObjectExpression: TGocciaObjectExpression; const begin // Static property: {key: value} PropertyName := AObjectExpression.PropertySourceOrder[I].StaticKey; - if AObjectExpression.Properties.TryGetValue(PropertyName, PropertyExpression) then + PropertyExpression := AObjectExpression.PropertySourceOrder[I].Expression; + if not Assigned(PropertyExpression) then + AObjectExpression.Properties.TryGetValue(PropertyName, PropertyExpression); + if Assigned(PropertyExpression) then begin PropertyValue := EvaluateExpression(PropertyExpression, AContext); if (PropertyExpression is TGocciaMethodExpression) @@ -3510,7 +3513,23 @@ procedure InitializeInstanceProperties(const AInstance: TGocciaInstanceValue; co begin FOEntry := AClassValue.FieldOrderEntry(I); Expr := nil; - if FOEntry.IsPrivate then + if FOEntry.IsComputed then + begin + if Assigned(FOEntry.Initializer) then + PropertyValue := EvaluateExpression(FOEntry.Initializer, LocalContext) + else + PropertyValue := TGocciaUndefinedLiteralValue.UndefinedValue; + if FOEntry.ComputedKey is TGocciaSymbolValue then + AInstance.DefineSymbolProperty( + TGocciaSymbolValue(FOEntry.ComputedKey), + TGocciaPropertyDescriptorData.Create(PropertyValue, + [pfEnumerable, pfConfigurable, pfWritable])) + else if Assigned(FOEntry.ComputedKey) then + AInstance.DefineProperty(FOEntry.ComputedKey.ToStringLiteral.Value, + TGocciaPropertyDescriptorData.Create(PropertyValue, + [pfEnumerable, pfConfigurable, pfWritable])); + end + else if FOEntry.IsPrivate then begin if AClassValue.PrivateInstancePropertyDefs.TryGetValue(FOEntry.Name, Expr) and Assigned(Expr) then begin @@ -3574,7 +3593,23 @@ procedure InitializeObjectInstanceProperties(const AInstance: TGocciaObjectValue begin FOEntry := AClassValue.FieldOrderEntry(I); Expr := nil; - if FOEntry.IsPrivate then + if FOEntry.IsComputed then + begin + if Assigned(FOEntry.Initializer) then + PropertyValue := EvaluateExpression(FOEntry.Initializer, AContext) + else + PropertyValue := TGocciaUndefinedLiteralValue.UndefinedValue; + if FOEntry.ComputedKey is TGocciaSymbolValue then + AInstance.DefineSymbolProperty( + TGocciaSymbolValue(FOEntry.ComputedKey), + TGocciaPropertyDescriptorData.Create(PropertyValue, + [pfEnumerable, pfConfigurable, pfWritable])) + else if Assigned(FOEntry.ComputedKey) then + AInstance.DefineProperty(FOEntry.ComputedKey.ToStringLiteral.Value, + TGocciaPropertyDescriptorData.Create(PropertyValue, + [pfEnumerable, pfConfigurable, pfWritable])); + end + else if FOEntry.IsPrivate then begin if AClassValue.PrivateInstancePropertyDefs.TryGetValue(FOEntry.Name, Expr) and Assigned(Expr) then begin @@ -4133,7 +4168,10 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const ClassName: string; I, J: Integer; HasDecorators: Boolean; + HasReplayedComputedKey: Boolean; FieldOrderEntries: array of TGocciaClassFieldOrderEntry; + ResolvedComputedFieldKeys: array of TGocciaValue; + Continuation: TGocciaGeneratorContinuation; MetadataObject: TGocciaObjectValue; SuperMetadata: TGocciaValue; EvaluatedElementDecorators: array of array of TGocciaValue; @@ -4166,6 +4204,7 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const TGocciaMethodValue(Result).OwningClass := ClassValue; end; begin + Continuation := CurrentGeneratorContinuation; SuperClass := nil; SuperClassValue := nil; MethodSuperClass := nil; @@ -4257,18 +4296,6 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const ClassValue.AddPrivateInstanceProperty(PropertyEntry.Key, PropertyEntry.Value); end; - // Copy field order for source-order initialization - if Length(AClassDef.FFieldOrder) > 0 then - begin - SetLength(FieldOrderEntries, Length(AClassDef.FFieldOrder)); - for I := 0 to High(AClassDef.FFieldOrder) do - begin - FieldOrderEntries[I].Name := AClassDef.FFieldOrder[I].Name; - FieldOrderEntries[I].IsPrivate := AClassDef.FFieldOrder[I].IsPrivate; - end; - ClassValue.SetFieldOrder(FieldOrderEntries); - end; - for MethodPair in AClassDef.PrivateMethods do begin Method := TGocciaMethodValue(EvaluateClassMethod(MethodPair.Value, AContext, MethodSuperClass)); @@ -4314,20 +4341,34 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const ClassValue.AddStaticSetter(SetterPair.Key, SetterFunction); end; - // Handle computed members (methods, getters, setters) in source order via FElements + SetLength(ResolvedComputedFieldKeys, Length(AClassDef.FElements)); + + // Handle computed class element names in source order via FElements. for I := 0 to High(AClassDef.FElements) do begin Elem := AClassDef.FElements[I]; if not Elem.IsComputed then Continue; - if not (Elem.Kind in [cekMethod, cekGetter, cekSetter]) then + if not (Elem.Kind in [cekMethod, cekGetter, cekSetter, cekField]) then Continue; // ES2026 §15.4 ClassDefinitionEvaluation step 6.b for computed names: // evaluate, then ToPropertyKey, dispatching string vs. symbol storage. - ComputedKey := ToPropertyKey(EvaluateExpression(Elem.ComputedKeyExpression, AContext)); + // Generator resumption restarts this class definition evaluation, so + // replay keys already resolved before a later computed name suspended. + HasReplayedComputedKey := False; + if Assigned(Continuation) then + HasReplayedComputedKey := Continuation.TakeExpressionValue( + Elem.ComputedKeyExpression, ComputedKey); + if not HasReplayedComputedKey then + ComputedKey := ToPropertyKey(EvaluateExpression( + Elem.ComputedKeyExpression, AContext)); + if Assigned(Continuation) then + Continuation.SaveExpressionValue(Elem.ComputedKeyExpression, ComputedKey); case Elem.Kind of + cekField: + ResolvedComputedFieldKeys[I] := ComputedKey; cekMethod: begin Method := TGocciaMethodValue(EvaluateClassMethod(Elem.MethodNode, AContext, MethodSuperClass)); @@ -4423,6 +4464,26 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const end; end; + // Copy field order for source-order initialization after computed names are resolved. + if Length(AClassDef.FFieldOrder) > 0 then + begin + SetLength(FieldOrderEntries, Length(AClassDef.FFieldOrder)); + for I := 0 to High(AClassDef.FFieldOrder) do + begin + FieldOrderEntries[I].Name := AClassDef.FFieldOrder[I].Name; + FieldOrderEntries[I].IsPrivate := AClassDef.FFieldOrder[I].IsPrivate; + FieldOrderEntries[I].IsComputed := AClassDef.FFieldOrder[I].IsComputed; + FieldOrderEntries[I].Initializer := AClassDef.FFieldOrder[I].FieldInitializer; + FieldOrderEntries[I].ComputedKey := nil; + if FieldOrderEntries[I].IsComputed and + (AClassDef.FFieldOrder[I].ElementIndex >= 0) and + (AClassDef.FFieldOrder[I].ElementIndex <= High(ResolvedComputedFieldKeys)) then + FieldOrderEntries[I].ComputedKey := + ResolvedComputedFieldKeys[AClassDef.FFieldOrder[I].ElementIndex]; + end; + ClassValue.SetFieldOrder(FieldOrderEntries); + end; + // TC39 proposal-decorators §3.1 ClassDefinitionEvaluation — auto-accessor setup for I := 0 to High(AClassDef.FElements) do begin @@ -4452,6 +4513,24 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const PropertyValue := TGocciaUndefinedLiteralValue.UndefinedValue; if Elem.IsPrivate then ClassValue.AddPrivateStaticProperty(Elem.Name, PropertyValue) + else if Elem.IsComputed then + begin + ComputedKey := nil; + if I <= High(ResolvedComputedFieldKeys) then + ComputedKey := ResolvedComputedFieldKeys[I]; + if not Assigned(ComputedKey) then + ComputedKey := ToPropertyKey(EvaluateExpression( + Elem.ComputedKeyExpression, AContext)); + if ComputedKey is TGocciaSymbolValue then + ClassValue.DefineSymbolProperty( + TGocciaSymbolValue(ComputedKey), + TGocciaPropertyDescriptorData.Create(PropertyValue, + [pfEnumerable, pfConfigurable, pfWritable])) + else + ClassValue.DefineProperty(ComputedKey.ToStringLiteral.Value, + TGocciaPropertyDescriptorData.Create(PropertyValue, + [pfEnumerable, pfConfigurable, pfWritable])); + end else ClassValue.DefineProperty(Elem.Name, TGocciaPropertyDescriptorData.Create(PropertyValue, @@ -4800,6 +4879,13 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const end; end; + if Assigned(Continuation) then + for I := 0 to High(AClassDef.FElements) do + if AClassDef.FElements[I].IsComputed and + (AClassDef.FElements[I].Kind in [cekMethod, cekGetter, cekSetter, cekField]) then + Continuation.ClearExpressionValue( + AClassDef.FElements[I].ComputedKeyExpression); + Result := ClassValue; end; diff --git a/source/units/Goccia.Parser.pas b/source/units/Goccia.Parser.pas index 8aca0150..2d0e5a39 100644 --- a/source/units/Goccia.Parser.pas +++ b/source/units/Goccia.Parser.pas @@ -1759,6 +1759,7 @@ function TGocciaParser.Primary: TGocciaExpression; ArrowBody: TGocciaASTNode; SeparatorPos: Integer; Line, Column: Integer; + AsyncStartLine, AsyncStartColumn: Integer; IsGenerator: Boolean; begin case Peek.TokenType of @@ -1995,7 +1996,9 @@ function TGocciaParser.Primary: TGocciaExpression; end else if (Name = KEYWORD_ASYNC) and not Token.ContainsEscape then begin - case TryConsumeAsyncFunction(Token.Line, Token.Column) of + AsyncStartLine := Token.Line; + AsyncStartColumn := Token.Column; + case TryConsumeAsyncFunction(AsyncStartLine, AsyncStartColumn) of afcNotMatched: Result := TGocciaIdentifierExpression.Create(Name, Token.Line, Token.Column); afcDisabled: @@ -2017,8 +2020,8 @@ function TGocciaParser.Primary: TGocciaExpression; Name := Token.Lexeme; end; CollectGenericParameters; - Result := ParseObjectMethodBody(Token.Line, Token.Column, True, IsGenerator); - TGocciaMethodExpression(Result).SourceText := ExtractSourceRange(Token.Line, Token.Column); + Result := ParseObjectMethodBody(AsyncStartLine, AsyncStartColumn, True, IsGenerator); + TGocciaMethodExpression(Result).SourceText := ExtractSourceRange(AsyncStartLine, AsyncStartColumn); TGocciaMethodExpression(Result).IsAsync := True; TGocciaMethodExpression(Result).IsGenerator := IsGenerator; // ES2026 §15.6: AsyncGeneratorDeclaration / AsyncGeneratorExpression have @@ -2844,6 +2847,7 @@ function TGocciaParser.ObjectLiteral: TGocciaExpression; PropertySourceOrder[SourceOrderCount - 1].PropertyType := pstComputed; PropertySourceOrder[SourceOrderCount - 1].ComputedIndex := ComputedCount - 1; PropertySourceOrder[SourceOrderCount - 1].StaticKey := ''; + PropertySourceOrder[SourceOrderCount - 1].Expression := nil; // For spread expressions, no further processing is needed if not Match(gttComma) then @@ -2890,6 +2894,7 @@ function TGocciaParser.ObjectLiteral: TGocciaExpression; PropertySourceOrder[SourceOrderCount - 1].PropertyType := pstGetter; PropertySourceOrder[SourceOrderCount - 1].StaticKey := Key; PropertySourceOrder[SourceOrderCount - 1].ComputedIndex := -1; + PropertySourceOrder[SourceOrderCount - 1].Expression := nil; end else if IsSetter then begin @@ -2902,6 +2907,7 @@ function TGocciaParser.ObjectLiteral: TGocciaExpression; PropertySourceOrder[SourceOrderCount - 1].PropertyType := pstSetter; PropertySourceOrder[SourceOrderCount - 1].StaticKey := Key; PropertySourceOrder[SourceOrderCount - 1].ComputedIndex := -1; + PropertySourceOrder[SourceOrderCount - 1].Expression := nil; end // Check for method shorthand syntax: methodName() { ... } or [expr]() { ... } else if Check(gttLeftParen) then @@ -2960,6 +2966,7 @@ function TGocciaParser.ObjectLiteral: TGocciaExpression; PropertySourceOrder[SourceOrderCount - 1].PropertyType := pstComputed; PropertySourceOrder[SourceOrderCount - 1].ComputedIndex := ComputedCount - 1; PropertySourceOrder[SourceOrderCount - 1].StaticKey := ''; + PropertySourceOrder[SourceOrderCount - 1].Expression := nil; end else begin @@ -2978,6 +2985,7 @@ function TGocciaParser.ObjectLiteral: TGocciaExpression; PropertySourceOrder[SourceOrderCount - 1].PropertyType := pstStatic; PropertySourceOrder[SourceOrderCount - 1].StaticKey := Key; PropertySourceOrder[SourceOrderCount - 1].ComputedIndex := -1; + PropertySourceOrder[SourceOrderCount - 1].Expression := Value; end; end; @@ -5563,21 +5571,34 @@ function TGocciaParser.ParseClassBody(const AClassName: string): TGocciaClassDef ConsumeSemicolonOrASI('Expected ";" after property', SSuggestAddSemicolon); - if (Length(MemberDecorators) > 0) or IsStatic then + if (Length(MemberDecorators) > 0) or IsStatic or IsComputed then begin SetLength(Elements, Length(Elements) + 1); Elements[High(Elements)].Kind := cekField; Elements[High(Elements)].Name := MemberName; Elements[High(Elements)].IsStatic := IsStatic; Elements[High(Elements)].IsPrivate := IsPrivate; - Elements[High(Elements)].IsComputed := False; - Elements[High(Elements)].ComputedKeyExpression := nil; + Elements[High(Elements)].IsComputed := IsComputed; + Elements[High(Elements)].ComputedKeyExpression := ComputedKeyExpression; Elements[High(Elements)].Decorators := MemberDecorators; Elements[High(Elements)].FieldInitializer := PropertyValue; Elements[High(Elements)].TypeAnnotation := FieldType; end; - if IsPrivate and IsStatic then + if IsComputed then + begin + if not IsStatic then + begin + SetLength(FieldOrder, Length(FieldOrder) + 1); + FieldOrder[High(FieldOrder)].Name := ''; + FieldOrder[High(FieldOrder)].IsPrivate := False; + FieldOrder[High(FieldOrder)].IsComputed := True; + FieldOrder[High(FieldOrder)].ElementIndex := High(Elements); + FieldOrder[High(FieldOrder)].ComputedKeyExpression := ComputedKeyExpression; + FieldOrder[High(FieldOrder)].FieldInitializer := PropertyValue; + end; + end + else if IsPrivate and IsStatic then PrivateStaticProperties.Add(MemberName, PropertyValue) else if IsPrivate then begin @@ -5585,6 +5606,10 @@ function TGocciaParser.ParseClassBody(const AClassName: string): TGocciaClassDef SetLength(FieldOrder, Length(FieldOrder) + 1); FieldOrder[High(FieldOrder)].Name := MemberName; FieldOrder[High(FieldOrder)].IsPrivate := True; + FieldOrder[High(FieldOrder)].IsComputed := False; + FieldOrder[High(FieldOrder)].ElementIndex := -1; + FieldOrder[High(FieldOrder)].ComputedKeyExpression := nil; + FieldOrder[High(FieldOrder)].FieldInitializer := nil; end else if IsStatic then StaticProperties.Add(MemberName, PropertyValue) @@ -5594,6 +5619,10 @@ function TGocciaParser.ParseClassBody(const AClassName: string): TGocciaClassDef SetLength(FieldOrder, Length(FieldOrder) + 1); FieldOrder[High(FieldOrder)].Name := MemberName; FieldOrder[High(FieldOrder)].IsPrivate := False; + FieldOrder[High(FieldOrder)].IsComputed := False; + FieldOrder[High(FieldOrder)].ElementIndex := -1; + FieldOrder[High(FieldOrder)].ComputedKeyExpression := nil; + FieldOrder[High(FieldOrder)].FieldInitializer := nil; if FieldType <> '' then InstancePropertyTypes.Add(MemberName, FieldType); end; @@ -5604,21 +5633,34 @@ function TGocciaParser.ParseClassBody(const AClassName: string): TGocciaClassDef Advance; PropertyValue := TGocciaLiteralExpression.Create(TGocciaUndefinedLiteralValue.UndefinedValue, Peek.Line, Peek.Column); - if (Length(MemberDecorators) > 0) or IsStatic then + if (Length(MemberDecorators) > 0) or IsStatic or IsComputed then begin SetLength(Elements, Length(Elements) + 1); Elements[High(Elements)].Kind := cekField; Elements[High(Elements)].Name := MemberName; Elements[High(Elements)].IsStatic := IsStatic; Elements[High(Elements)].IsPrivate := IsPrivate; - Elements[High(Elements)].IsComputed := False; - Elements[High(Elements)].ComputedKeyExpression := nil; + Elements[High(Elements)].IsComputed := IsComputed; + Elements[High(Elements)].ComputedKeyExpression := ComputedKeyExpression; Elements[High(Elements)].Decorators := MemberDecorators; Elements[High(Elements)].FieldInitializer := PropertyValue; Elements[High(Elements)].TypeAnnotation := FieldType; end; - if IsPrivate and IsStatic then + if IsComputed then + begin + if not IsStatic then + begin + SetLength(FieldOrder, Length(FieldOrder) + 1); + FieldOrder[High(FieldOrder)].Name := ''; + FieldOrder[High(FieldOrder)].IsPrivate := False; + FieldOrder[High(FieldOrder)].IsComputed := True; + FieldOrder[High(FieldOrder)].ElementIndex := High(Elements); + FieldOrder[High(FieldOrder)].ComputedKeyExpression := ComputedKeyExpression; + FieldOrder[High(FieldOrder)].FieldInitializer := PropertyValue; + end; + end + else if IsPrivate and IsStatic then PrivateStaticProperties.Add(MemberName, PropertyValue) else if IsPrivate then begin @@ -5626,6 +5668,10 @@ function TGocciaParser.ParseClassBody(const AClassName: string): TGocciaClassDef SetLength(FieldOrder, Length(FieldOrder) + 1); FieldOrder[High(FieldOrder)].Name := MemberName; FieldOrder[High(FieldOrder)].IsPrivate := True; + FieldOrder[High(FieldOrder)].IsComputed := False; + FieldOrder[High(FieldOrder)].ElementIndex := -1; + FieldOrder[High(FieldOrder)].ComputedKeyExpression := nil; + FieldOrder[High(FieldOrder)].FieldInitializer := nil; end else if IsStatic then StaticProperties.Add(MemberName, PropertyValue) @@ -5635,6 +5681,10 @@ function TGocciaParser.ParseClassBody(const AClassName: string): TGocciaClassDef SetLength(FieldOrder, Length(FieldOrder) + 1); FieldOrder[High(FieldOrder)].Name := MemberName; FieldOrder[High(FieldOrder)].IsPrivate := False; + FieldOrder[High(FieldOrder)].IsComputed := False; + FieldOrder[High(FieldOrder)].ElementIndex := -1; + FieldOrder[High(FieldOrder)].ComputedKeyExpression := nil; + FieldOrder[High(FieldOrder)].FieldInitializer := nil; if FieldType <> '' then InstancePropertyTypes.Add(MemberName, FieldType); end; diff --git a/source/units/Goccia.VM.pas b/source/units/Goccia.VM.pas index 46e8f969..1ffb2dc2 100644 --- a/source/units/Goccia.VM.pas +++ b/source/units/Goccia.VM.pas @@ -8075,6 +8075,47 @@ // ES2022 §15.7.14: execute static block closure with this = class SetPropertyValue(GetRegister(A), GlobalName, RightValue); end; + OP_DEFINE_PROP_DYNAMIC: + begin + RightValue := RegisterToValue(FRegisters[C]); + TargetValue := GetRegister(A); + if (TargetValue is TGocciaClassValue) or + (TargetValue is TGocciaObjectValue) then + SetBytecodeHomeObject(RightValue, TargetValue); + if (TargetValue is TGocciaObjectValue) and + (FRegisters[B].Kind = grkObject) and + (FRegisters[B].ObjectValue is TGocciaSymbolValue) then + TGocciaObjectValue(TargetValue).DefineSymbolProperty( + TGocciaSymbolValue(FRegisters[B].ObjectValue), + TGocciaPropertyDescriptorData.Create( + RightValue, [pfEnumerable, pfConfigurable, pfWritable])) + else if (TargetValue is TGocciaObjectValue) and + TryResolveObjectKey(FRegisters[B], PropKeyValue) then + begin + if PropKeyValue is TGocciaSymbolValue then + TGocciaObjectValue(TargetValue).DefineSymbolProperty( + TGocciaSymbolValue(PropKeyValue), + TGocciaPropertyDescriptorData.Create( + RightValue, [pfEnumerable, pfConfigurable, pfWritable])) + else + TGocciaObjectValue(TargetValue).DefineProperty( + TGocciaStringLiteralValue(PropKeyValue).Value, + TGocciaPropertyDescriptorData.Create( + RightValue, [pfEnumerable, pfConfigurable, pfWritable])); + end + else + begin + GlobalName := KeyToPropertyNameRegister(FRegisters[B]); + if TargetValue is TGocciaObjectValue then + TGocciaObjectValue(TargetValue).DefineProperty( + GlobalName, + TGocciaPropertyDescriptorData.Create( + RightValue, [pfEnumerable, pfConfigurable, pfWritable])) + else + SetPropertyValue(TargetValue, GlobalName, RightValue); + end; + end; + OP_DEFINE_STATIC_METHOD_CONST: begin GlobalName := Template.GetConstantUnchecked(B).StringValue; diff --git a/source/units/Goccia.Values.ClassValue.pas b/source/units/Goccia.Values.ClassValue.pas index 752add63..48527f7f 100644 --- a/source/units/Goccia.Values.ClassValue.pas +++ b/source/units/Goccia.Values.ClassValue.pas @@ -26,6 +26,9 @@ TGocciaInstanceValue = class; TGocciaClassFieldOrderEntry = record Name: string; IsPrivate: Boolean; + IsComputed: Boolean; + ComputedKey: TGocciaValue; + Initializer: TGocciaExpression; end; TGocciaClassValue = class(TGocciaObjectValue) @@ -437,6 +440,9 @@ procedure TGocciaClassValue.MarkReferences; for I := 0 to High(FDecoratorFieldInitializers) do if Assigned(FDecoratorFieldInitializers[I].Initializer) then FDecoratorFieldInitializers[I].Initializer.MarkReferences; + for I := 0 to High(FFieldOrder) do + if Assigned(FFieldOrder[I].ComputedKey) then + FFieldOrder[I].ComputedKey.MarkReferences; end; function TGocciaClassValue.IsCallable: Boolean; diff --git a/tests/language/classes/class-computed-field-yield-order.js b/tests/language/classes/class-computed-field-yield-order.js new file mode 100644 index 00000000..ddbfa4bc --- /dev/null +++ b/tests/language/classes/class-computed-field-yield-order.js @@ -0,0 +1,69 @@ +/*--- +description: Computed public class field names evaluate in source order +features: [computed-property-names, class-fields-public, class-static-fields-public, generators] +---*/ + +test("computed public field names follow source order", () => { + const obj = { + *makeClass() { + let C = class { + static [yield "static-before"] = 7; + [yield "field"] = 42; + }; + let c = new C(); + return [C.staticBefore, c.field]; + }, + }; + + const iter = obj.makeClass(); + + expect(iter.next()).toEqual({ value: "static-before", done: false }); + expect(iter.next("staticBefore")).toEqual({ value: "field", done: false }); + expect(iter.next("field")).toEqual({ + value: [7, 42], + done: true, + }); +}); + +test("mixed computed public field names resume in source order", () => { + const obj = { + *makeClass() { + let C = class { + static [yield "static-before"] = 7; + [yield "field"] = 42; + static [yield "static-after"] = 9; + }; + let c = new C(); + return [C.staticBefore, c.field, C.staticAfter]; + }, + }; + + const iter = obj.makeClass(); + + expect(iter.next()).toEqual({ value: "static-before", done: false }); + expect(iter.next("staticBefore")).toEqual({ value: "field", done: false }); + expect(iter.next("field")).toEqual({ value: "static-after", done: false }); + expect(iter.next("staticAfter")).toEqual({ + value: [7, 42, 9], + done: true, + }); +}); + +test("computed public instance fields define own properties", () => { + let calls = 0; + + class Base { + set value(next) { + calls = calls + next; + } + } + + class Derived extends Base { + ["value"] = 11; + } + + const instance = new Derived(); + + expect(calls).toBe(0); + expect(instance.value).toBe(11); +}); diff --git a/tests/language/function-keyword/async-function.js b/tests/language/function-keyword/async-function.js index 68d2fc27..52472725 100644 --- a/tests/language/function-keyword/async-function.js +++ b/tests/language/function-keyword/async-function.js @@ -19,6 +19,15 @@ test("async function expression", async () => { expect(result).toBe("hello"); }); +test("named async function expression source text starts at async keyword", () => { + const source = (async function getValue(value) { + return value; + }).toString(); + + expect(source.startsWith("async function getValue(value)")).toBe(true); + expect(source.includes("return value")).toBe(true); +}); + test("async function with await", async () => { async function delayed() { const value = await Promise.resolve(10); diff --git a/tests/language/function-keyword/class-computed-field-yield.js b/tests/language/function-keyword/class-computed-field-yield.js new file mode 100644 index 00000000..8eaecd06 --- /dev/null +++ b/tests/language/function-keyword/class-computed-field-yield.js @@ -0,0 +1,73 @@ +/*--- +description: Generator yield resumes computed public class field names +features: [compat-function, computed-property-names, class-fields-public, class-static-fields-public, generators] +---*/ + +test("yield resumes computed public instance and static field names", () => { + function* makeClass() { + let C = class { + [yield "instance-key"] = 9; + static [yield "static-key"] = 10; + }; + let c = new C(); + return [c, C]; + } + + const instanceKey = Symbol("instance"); + const staticKey = Symbol("static"); + const iter = makeClass(); + + expect(iter.next()).toEqual({ value: "instance-key", done: false }); + expect(iter.next(instanceKey)).toEqual({ value: "static-key", done: false }); + + const result = iter.next(staticKey); + expect(result.done).toBe(true); + expect(result.value[0][instanceKey]).toBe(9); + expect(result.value[1][staticKey]).toBe(10); +}); + +test("omitted next values install fields under undefined keys", () => { + let observed = []; + + function* makeClass() { + let C = class { + [yield 9] = 9; + static [yield 9] = 10; + }; + let c = new C(); + observed = [c[undefined], C[undefined], c[9], C[9]]; + } + + const iter = makeClass(); + + expect(iter.next()).toEqual({ value: 9, done: false }); + expect(iter.next()).toEqual({ value: 9, done: false }); + expect(iter.next()).toEqual({ value: undefined, done: true }); + expect(observed).toEqual([9, 10, undefined, undefined]); +}); + +test("computed field initializers from yield remain callable", () => { + function* makeClass() { + let C = class { + [yield 9] = () => 9; + static [yield 9] = () => 10; + }; + let c = new C(); + return [ + c[yield 9](), + C[yield 9](), + c[String(yield 9)](), + C[String(yield 9)](), + ]; + } + + const iter = makeClass(); + + expect(iter.next()).toEqual({ value: 9, done: false }); + expect(iter.next(9)).toEqual({ value: 9, done: false }); + expect(iter.next(9)).toEqual({ value: 9, done: false }); + expect(iter.next(9)).toEqual({ value: 9, done: false }); + expect(iter.next(9)).toEqual({ value: 9, done: false }); + expect(iter.next(9)).toEqual({ value: 9, done: false }); + expect(iter.next(9)).toEqual({ value: [9, 10, 9, 10], done: true }); +}); diff --git a/tests/language/objects/basic-object-creation.js b/tests/language/objects/basic-object-creation.js index 44b5aecd..bd8abfac 100644 --- a/tests/language/objects/basic-object-creation.js +++ b/tests/language/objects/basic-object-creation.js @@ -59,6 +59,20 @@ test("object property enumeration and inspection", () => { expect(Object.hasOwn(obj, "d")).toBe(false); }); +test("duplicate static property initializers evaluate in source order", () => { + const log = []; + const obj = { + a: log.push("first"), + b: log.push("middle"), + a: log.push("last"), + }; + + expect(log).toEqual(["first", "middle", "last"]); + expect(obj.a).toBe(3); + expect(obj.b).toBe(2); + expect(Object.keys(obj)).toEqual(["a", "b"]); +}); + // TODO: We don't have a test to support creating objects with prototypes because we only support arrow functions. test("object creation with arrow function should throw a type error", () => { expect(() => { From 021776fc15080b9518cce593a0e24a87624112bf Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Wed, 20 May 2026 14:39:21 +0100 Subject: [PATCH 2/8] Fix computed field key coercion in bytecode --- source/units/Goccia.Bytecode.OpCodeNames.pas | 1 + source/units/Goccia.Bytecode.pas | 7 ++- source/units/Goccia.Compiler.Statements.pas | 5 +++ source/units/Goccia.VM.pas | 4 ++ .../classes/computed-field-key-coercion.js | 44 +++++++++++++++++++ 5 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 tests/language/classes/computed-field-key-coercion.js diff --git a/source/units/Goccia.Bytecode.OpCodeNames.pas b/source/units/Goccia.Bytecode.OpCodeNames.pas index b3886ade..19dad575 100644 --- a/source/units/Goccia.Bytecode.OpCodeNames.pas +++ b/source/units/Goccia.Bytecode.OpCodeNames.pas @@ -171,6 +171,7 @@ function OpCodeName(const AOp: UInt8): string; OP_CREATE_ARGUMENTS: Result := 'OP_CREATE_ARGUMENTS'; OP_TO_OBJECT: Result := 'OP_TO_OBJECT'; OP_HAS_WITH_BINDING: Result := 'OP_HAS_WITH_BINDING'; + OP_TO_PROPERTY_KEY: Result := 'OP_TO_PROPERTY_KEY'; OP_INC: Result := 'OP_INC'; OP_DEC: Result := 'OP_DEC'; OP_TO_NUMERIC: Result := 'OP_TO_NUMERIC'; diff --git a/source/units/Goccia.Bytecode.pas b/source/units/Goccia.Bytecode.pas index 1245accb..7d321f0b 100644 --- a/source/units/Goccia.Bytecode.pas +++ b/source/units/Goccia.Bytecode.pas @@ -59,7 +59,9 @@ interface // [[HomeObject]] without affecting plain data properties. // v34 -> v35: added OP_DEFINE_PROP_DYNAMIC for computed public // class fields. - GOCCIA_FORMAT_VERSION = 35; + // v35 -> v36: added OP_TO_PROPERTY_KEY so delayed computed class field + // definitions can reuse source-order property keys. + GOCCIA_FORMAT_VERSION = 36; GOCCIA_BINARY_MAGIC: array[0..3] of Byte = (Ord('G'), Ord('B'), Ord('C'), 0); GOCCIA_NULLISH_MATCH_UNDEFINED = 0; GOCCIA_NULLISH_MATCH_NULL = 1; @@ -254,7 +256,8 @@ interface OP_NEW_TARGET = 181, OP_CREATE_ARGUMENTS = 182, OP_TO_OBJECT = 183, - OP_HAS_WITH_BINDING = 184 + OP_HAS_WITH_BINDING = 184, + OP_TO_PROPERTY_KEY = 185 ); function EncodeABC(const AOp: TGocciaOpCode; const A, B, C: UInt8): UInt32; inline; diff --git a/source/units/Goccia.Compiler.Statements.pas b/source/units/Goccia.Compiler.Statements.pas index dac10ef6..62d1afe8 100644 --- a/source/units/Goccia.Compiler.Statements.pas +++ b/source/units/Goccia.Compiler.Statements.pas @@ -3732,7 +3732,12 @@ procedure CompileComputedElements(const ACtx: TGocciaCompilationContext; case Elem.Kind of cekField: + begin + // ES2026 §15.7.10 ClassFieldDefinitionEvaluation evaluates + // ClassElementName, including ToPropertyKey, during class definition. + EmitInstruction(ACtx, EncodeABC(OP_TO_PROPERTY_KEY, KeyReg, KeyReg, 0)); Continue; + end; cekGetter: if Elem.IsStatic then CompileComputedGetterBody(ACtx, ATargetReg, KeyReg, diff --git a/source/units/Goccia.VM.pas b/source/units/Goccia.VM.pas index 4fc0af01..d597d3de 100644 --- a/source/units/Goccia.VM.pas +++ b/source/units/Goccia.VM.pas @@ -7424,6 +7424,10 @@ function TGocciaVM.ExecuteClosureRegistersInternal( OP_TO_OBJECT: SetRegister(A, ToObject(GetRegister(B))); + // ES2026 §7.1.19 ToPropertyKey(argument) + OP_TO_PROPERTY_KEY: + SetRegister(A, ToPropertyKey(RegisterToValue(FRegisters[B]))); + OP_LOAD_INT: FRegisters[A] := RegisterInt(DecodesBx(Instruction)); diff --git a/tests/language/classes/computed-field-key-coercion.js b/tests/language/classes/computed-field-key-coercion.js new file mode 100644 index 00000000..324e295e --- /dev/null +++ b/tests/language/classes/computed-field-key-coercion.js @@ -0,0 +1,44 @@ +/*--- +description: Computed public class field keys are coerced during class definition +features: [computed-property-names, class-fields-public, class-static-fields-public] +---*/ + +test("static computed field coerces key before initializer", () => { + const order = []; + const key = { + toString() { + order.push("key"); + return "value"; + }, + }; + + class C { + static [key] = (order.push("init"), 1); + } + + expect(order).toEqual(["key", "init"]); + expect(C.value).toBe(1); +}); + +test("instance computed field coerces key once at class definition", () => { + const order = []; + const key = { + toString() { + order.push("key"); + return "value"; + }, + }; + + class C { + [key] = (order.push("init"), 1); + } + + expect(order).toEqual(["key"]); + + const first = new C(); + const second = new C(); + + expect(order).toEqual(["key", "init", "init"]); + expect(first.value).toBe(1); + expect(second.value).toBe(1); +}); From 92b4b9e8ef9c9dec2d7651c2899fc75a6bd952ec Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Wed, 20 May 2026 15:12:24 +0100 Subject: [PATCH 3/8] Fix computed field decorator keys --- source/units/Goccia.Compiler.Statements.pas | 44 +++++++--- source/units/Goccia.Evaluator.pas | 26 +++++- source/units/Goccia.VM.pas | 87 ++++++++++++------- source/units/Goccia.Values.ClassValue.pas | 35 +++++++- .../decorators/computed-field-decorator.js | 58 +++++++++++++ 5 files changed, 201 insertions(+), 49 deletions(-) create mode 100644 tests/language/decorators/computed-field-decorator.js diff --git a/source/units/Goccia.Compiler.Statements.pas b/source/units/Goccia.Compiler.Statements.pas index 62d1afe8..2b70a3e0 100644 --- a/source/units/Goccia.Compiler.Statements.pas +++ b/source/units/Goccia.Compiler.Statements.pas @@ -4076,7 +4076,8 @@ procedure CompileAutoAccessors(const ACtx: TGocciaCompilationContext; procedure CompileDecoratorOrchestration( const ACtx: TGocciaCompilationContext; - const AClassReg: UInt8; const AClassDef: TGocciaClassDefinition); + const AClassReg: UInt8; const AClassDef: TGocciaClassDefinition; + const AComputedFieldKeyLocals: TComputedFieldKeyLocals); var I, J: Integer; Elem: TGocciaClassElement; @@ -4086,6 +4087,8 @@ procedure CompileDecoratorOrchestration( Desc: string; PairReg, ExtraReg: UInt8; HasElementDecorators: Boolean; + ComputedKeyName: string; + LocalIdx: Integer; begin HasElementDecorators := False; for I := 0 to High(AClassDef.FElements) do @@ -4134,9 +4137,24 @@ procedure CompileDecoratorOrchestration( PairReg := ACtx.Scope.AllocateRegister; ExtraReg := ACtx.Scope.AllocateRegister; EmitInstruction(ACtx, EncodeABC(OP_MOVE, PairReg, DecoRegs[I][J], 0)); - EmitInstruction(ACtx, EncodeABC(OP_LOAD_UNDEFINED, ExtraReg, 0, 0)); - EmitInstruction(ACtx, EncodeABC(OP_APPLY_ELEMENT_DECORATOR_CONST, PairReg, - 0, UInt8(DescIdx))); + if (Elem.Kind = cekField) and Elem.IsComputed then + begin + ComputedKeyName := FindComputedFieldKeyLocalName( + AComputedFieldKeyLocals, I); + LocalIdx := ACtx.Scope.ResolveLocal(ComputedKeyName); + if LocalIdx < 0 then + raise Exception.Create('Compiler error: computed decorator field key was not captured'); + EmitInstruction(ACtx, EncodeABC(OP_MOVE, ExtraReg, + ACtx.Scope.GetLocal(LocalIdx).Slot, 0)); + EmitInstruction(ACtx, EncodeABC(OP_APPLY_ELEMENT_DECORATOR_CONST, + PairReg, ExtraReg, UInt8(DescIdx))); + end + else + begin + EmitInstruction(ACtx, EncodeABC(OP_LOAD_UNDEFINED, ExtraReg, 0, 0)); + EmitInstruction(ACtx, EncodeABC(OP_APPLY_ELEMENT_DECORATOR_CONST, + PairReg, 0, UInt8(DescIdx))); + end; ACtx.Scope.FreeRegister; ACtx.Scope.FreeRegister; end; @@ -4163,7 +4181,8 @@ procedure CompileDecoratorOrchestration( procedure CompileDecoratorAndAccessorPass( const ACtx: TGocciaCompilationContext; const AClassReg: UInt8; const AClassDef: TGocciaClassDefinition; - const ASuperReg: Integer); + const ASuperReg: Integer; + const AComputedFieldKeyLocals: TComputedFieldKeyLocals); var PairReg, ExtraReg: UInt8; begin @@ -4182,7 +4201,8 @@ procedure CompileDecoratorAndAccessorPass( ACtx.Scope.FreeRegister; CompileAutoAccessors(ACtx, AClassReg, AClassDef); - CompileDecoratorOrchestration(ACtx, AClassReg, AClassDef); + CompileDecoratorOrchestration(ACtx, AClassReg, AClassDef, + AComputedFieldKeyLocals); PairReg := ACtx.Scope.AllocateRegister; ExtraReg := ACtx.Scope.AllocateRegister; @@ -4401,9 +4421,11 @@ procedure CompileClassDeclaration(const ACtx: TGocciaCompilationContext; end; if HasSuper then - CompileDecoratorAndAccessorPass(ACtx, ClassReg, ClassDef, SuperReg) + CompileDecoratorAndAccessorPass(ACtx, ClassReg, ClassDef, SuperReg, + ComputedFieldKeyLocals) else - CompileDecoratorAndAccessorPass(ACtx, ClassReg, ClassDef, -1); + CompileDecoratorAndAccessorPass(ACtx, ClassReg, ClassDef, -1, + ComputedFieldKeyLocals); // Sync cell if the class local was pre-declared and captured by a hoisted // function (see CompileVariableDeclaration for the full explanation) @@ -4630,7 +4652,8 @@ procedure CompileClassExpression(const ACtx: TGocciaCompilationContext; if HasSuper then begin - CompileDecoratorAndAccessorPass(ACtx, ADest, ClassDef, SuperReg); + CompileDecoratorAndAccessorPass(ACtx, ADest, ClassDef, SuperReg, + ComputedFieldKeyLocals); // Only free __super__ manually when there is no name binding scope — // when HasNameBinding is true, __super__ lives inside the inner scope // and EndScope below will free it together with the name binding local. @@ -4638,7 +4661,8 @@ procedure CompileClassExpression(const ACtx: TGocciaCompilationContext; ACtx.Scope.FreeRegister; end else - CompileDecoratorAndAccessorPass(ACtx, ADest, ClassDef, -1); + CompileDecoratorAndAccessorPass(ACtx, ADest, ClassDef, -1, + ComputedFieldKeyLocals); if HasNameBinding then begin diff --git a/source/units/Goccia.Evaluator.pas b/source/units/Goccia.Evaluator.pas index 2ef071a1..7a675d7c 100644 --- a/source/units/Goccia.Evaluator.pas +++ b/source/units/Goccia.Evaluator.pas @@ -4197,6 +4197,7 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const ContextObject, AccessObject, AutoAccessorValue, DecResultObj: TGocciaObjectValue; Elem: TGocciaClassElement; ElementName: string; + ElementKey: TGocciaValue; CurrentMethod, GetterFnValue, SetterFnValue: TGocciaValue; NewGetter, NewSetter, NewInit: TGocciaValue; MethodCollector, FieldCollector, StaticFieldCollector, ClassCollector: TGocciaInitializerCollector; @@ -4621,7 +4622,25 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const cekAccessor: ContextObject.AssignProperty(PROP_KIND, TGocciaStringLiteralValue.Create('accessor')); end; - if Elem.IsPrivate then + ElementName := Elem.Name; + ElementKey := nil; + if (not Elem.IsPrivate) and Elem.IsComputed and + (I <= High(ResolvedComputedFieldKeys)) then + begin + ElementKey := ResolvedComputedFieldKeys[I]; + if ElementKey is TGocciaSymbolValue then + ContextObject.AssignProperty(PROP_NAME, ElementKey) + else if Assigned(ElementKey) then + begin + ElementName := ElementKey.ToStringLiteral.Value; + ContextObject.AssignProperty(PROP_NAME, + TGocciaStringLiteralValue.Create(ElementName)); + end + else + ContextObject.AssignProperty(PROP_NAME, + TGocciaStringLiteralValue.Create(Elem.Name)); + end + else if Elem.IsPrivate then ContextObject.AssignProperty(PROP_NAME, TGocciaStringLiteralValue.Create('#' + Elem.Name)) else ContextObject.AssignProperty(PROP_NAME, TGocciaStringLiteralValue.Create(Elem.Name)); @@ -4637,7 +4656,6 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const ContextObject.AssignProperty(PROP_METADATA, MetadataObject); // Build access object - ElementName := Elem.Name; AccessObject := TGocciaObjectValue.Create; case Elem.Kind of @@ -4780,7 +4798,7 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const begin if not DecoratorResult.IsCallable then AContext.OnError('Field decorator must return a function or undefined', ALine, AColumn); - ClassValue.AddFieldInitializer(ElementName, DecoratorResult, Elem.IsPrivate, Elem.IsStatic); + ClassValue.AddFieldInitializerWithKey(ElementName, ElementKey, DecoratorResult, Elem.IsPrivate, Elem.IsStatic); end; end; @@ -4809,7 +4827,7 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const if Assigned(NewSetter) and not (NewSetter is TGocciaUndefinedLiteralValue) then ClassValue.AddSetter(ElementName, TGocciaFunctionValue(NewSetter)); if Assigned(NewInit) and not (NewInit is TGocciaUndefinedLiteralValue) and NewInit.IsCallable then - ClassValue.AddFieldInitializer(ElementName, NewInit, Elem.IsPrivate, Elem.IsStatic); + ClassValue.AddFieldInitializerWithKey(ElementName, ElementKey, NewInit, Elem.IsPrivate, Elem.IsStatic); end; end; end; diff --git a/source/units/Goccia.VM.pas b/source/units/Goccia.VM.pas index d597d3de..5084d0c7 100644 --- a/source/units/Goccia.VM.pas +++ b/source/units/Goccia.VM.pas @@ -197,7 +197,7 @@ TGocciaVM = class const AArguments: TGocciaRegisterArray): TGocciaValue; procedure BeginDecorators(const AClassValue, ASuperValue: TGocciaValue); procedure ApplyElementDecorator(const ADecoratorFn: TGocciaValue; - const ADescriptor: string); + const ADescriptor: string; const AComputedKey: TGocciaValue = nil); procedure ApplyClassDecorator(const ADecoratorFn: TGocciaValue); function FinishDecorators(const ACurrentValue: TGocciaValue): TGocciaValue; function GetSuperPropertyValue(const ASuperValue, AThisValue: TGocciaValue; @@ -5946,11 +5946,13 @@ procedure TGocciaVM.ApplyClassDecorator(const ADecoratorFn: TGocciaValue); end; procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; - const ADescriptor: string); + const ADescriptor: string; const AComputedKey: TGocciaValue = nil); var Session: TGocciaVMDecoratorSession; Kind: Char; Name: string; + ElementName: string; + ElementKey: TGocciaValue; Flags: Integer; IsStatic, IsPrivate: Boolean; ClassVal, DecoratorResult, ElementValue: TGocciaValue; @@ -5976,6 +5978,15 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; ParseElementDescriptor(ADescriptor, Kind, Name, Flags); IsStatic := (Flags and 1) <> 0; IsPrivate := (Flags and 2) <> 0; + ElementName := Name; + ElementKey := nil; + if (not IsPrivate) and Assigned(AComputedKey) and + not (AComputedKey is TGocciaUndefinedLiteralValue) then + begin + ElementKey := AComputedKey; + if not (ElementKey is TGocciaSymbolValue) then + ElementName := ElementKey.ToStringLiteral.Value; + end; case Kind of 'm': KindStr := 'method'; @@ -5993,6 +6004,11 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; if IsPrivate then ContextObject.AssignProperty(PROP_NAME, TGocciaStringLiteralValue.Create('#' + Name)) + else if ElementKey is TGocciaSymbolValue then + ContextObject.AssignProperty(PROP_NAME, ElementKey) + else if Assigned(ElementKey) then + ContextObject.AssignProperty(PROP_NAME, + TGocciaStringLiteralValue.Create(ElementName)) else ContextObject.AssignProperty(PROP_NAME, TGocciaStringLiteralValue.Create(Name)); @@ -6017,10 +6033,10 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; if IsPrivate then ElementValue := TGocciaClassValue(ClassVal).GetPrivateMethod(Name) else if IsStatic then - ElementValue := TGocciaClassValue(ClassVal).GetProperty(Name) + ElementValue := TGocciaClassValue(ClassVal).GetProperty(ElementName) else - ElementValue := TGocciaClassValue(ClassVal).Prototype.GetProperty(Name); - AccessGetterHelper := TGocciaAccessGetter.Create(ElementValue, Name); + ElementValue := TGocciaClassValue(ClassVal).Prototype.GetProperty(ElementName); + AccessGetterHelper := TGocciaAccessGetter.Create(ElementValue, ElementName); AccessObject.AssignProperty(PROP_GET, TGocciaNativeFunctionValue.CreateWithoutPrototype( AccessGetterHelper.Get, PROP_GET, 0)); @@ -6028,8 +6044,8 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; end; 'f', 'a': begin - AccessGetterHelper := TGocciaAccessGetter.Create(nil, Name); - AccessSetterHelper := TGocciaAccessSetter.Create(Name); + AccessGetterHelper := TGocciaAccessGetter.Create(nil, ElementName); + AccessSetterHelper := TGocciaAccessSetter.Create(ElementName); AccessObject.AssignProperty(PROP_GET, TGocciaNativeFunctionValue.CreateWithoutPrototype( AccessGetterHelper.Get, PROP_GET, 0)); @@ -6058,18 +6074,18 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; if IsPrivate then ElementValue := TGocciaClassValue(ClassVal).PrivatePropertyGetter[Name] else if IsStatic then - ElementValue := TGocciaClassValue(ClassVal).StaticPropertyGetter[Name] + ElementValue := TGocciaClassValue(ClassVal).StaticPropertyGetter[ElementName] else - ElementValue := TGocciaClassValue(ClassVal).PropertyGetter[Name]; + ElementValue := TGocciaClassValue(ClassVal).PropertyGetter[ElementName]; end; 's': begin if IsPrivate then ElementValue := TGocciaClassValue(ClassVal).PrivatePropertySetter[Name] else if IsStatic then - ElementValue := TGocciaClassValue(ClassVal).StaticPropertySetter[Name] + ElementValue := TGocciaClassValue(ClassVal).StaticPropertySetter[ElementName] else - ElementValue := TGocciaClassValue(ClassVal).PropertySetter[Name]; + ElementValue := TGocciaClassValue(ClassVal).PropertySetter[ElementName]; end; 'f': ElementValue := TGocciaUndefinedLiteralValue.UndefinedValue; @@ -6079,16 +6095,16 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; if IsStatic then begin AutoAccessorValue.AssignProperty(PROP_GET, - TGocciaClassValue(ClassVal).StaticPropertyGetter[Name]); + TGocciaClassValue(ClassVal).StaticPropertyGetter[ElementName]); AutoAccessorValue.AssignProperty(PROP_SET, - TGocciaClassValue(ClassVal).StaticPropertySetter[Name]); + TGocciaClassValue(ClassVal).StaticPropertySetter[ElementName]); end else begin AutoAccessorValue.AssignProperty(PROP_GET, - TGocciaClassValue(ClassVal).PropertyGetter[Name]); + TGocciaClassValue(ClassVal).PropertyGetter[ElementName]); AutoAccessorValue.AssignProperty(PROP_SET, - TGocciaClassValue(ClassVal).PropertySetter[Name]); + TGocciaClassValue(ClassVal).PropertySetter[ElementName]); end; ElementValue := AutoAccessorValue; end; @@ -6117,10 +6133,10 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; TGocciaClassValue(ClassVal).Prototype.AssignProperty( '#' + Name, DecoratorResult) else if IsStatic then - TGocciaClassValue(ClassVal).SetProperty(Name, DecoratorResult) + TGocciaClassValue(ClassVal).SetProperty(ElementName, DecoratorResult) else TGocciaClassValue(ClassVal).Prototype.AssignProperty( - Name, DecoratorResult); + ElementName, DecoratorResult); end; 'g': begin @@ -6128,16 +6144,16 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; ThrowTypeError(SErrorGetterDecoratorReturn, SSuggestDecoratorFunction); if IsStatic then TGocciaClassValue(ClassVal).AddStaticGetter( - Name, TGocciaFunctionValue(DecoratorResult)) + ElementName, TGocciaFunctionValue(DecoratorResult)) else begin ExistingDescriptor := TGocciaClassValue(ClassVal).Prototype - .GetOwnPropertyDescriptor(Name); + .GetOwnPropertyDescriptor(ElementName); SetterValue := nil; if (ExistingDescriptor is TGocciaPropertyDescriptorAccessor) and Assigned(TGocciaPropertyDescriptorAccessor(ExistingDescriptor).Setter) then SetterValue := TGocciaPropertyDescriptorAccessor(ExistingDescriptor).Setter; - TGocciaClassValue(ClassVal).Prototype.DefineProperty(Name, + TGocciaClassValue(ClassVal).Prototype.DefineProperty(ElementName, TGocciaPropertyDescriptorAccessor.Create( DecoratorResult, SetterValue, [pfEnumerable, pfConfigurable, pfWritable])); end; @@ -6148,16 +6164,16 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; ThrowTypeError(SErrorSetterDecoratorReturn, SSuggestDecoratorFunction); if IsStatic then TGocciaClassValue(ClassVal).AddStaticSetter( - Name, TGocciaFunctionValue(DecoratorResult)) + ElementName, TGocciaFunctionValue(DecoratorResult)) else begin ExistingDescriptor := TGocciaClassValue(ClassVal).Prototype - .GetOwnPropertyDescriptor(Name); + .GetOwnPropertyDescriptor(ElementName); GetterValue := nil; if (ExistingDescriptor is TGocciaPropertyDescriptorAccessor) and Assigned(TGocciaPropertyDescriptorAccessor(ExistingDescriptor).Getter) then GetterValue := TGocciaPropertyDescriptorAccessor(ExistingDescriptor).Getter; - TGocciaClassValue(ClassVal).Prototype.DefineProperty(Name, + TGocciaClassValue(ClassVal).Prototype.DefineProperty(ElementName, TGocciaPropertyDescriptorAccessor.Create( GetterValue, DecoratorResult, [pfEnumerable, pfConfigurable, pfWritable])); end; @@ -6166,8 +6182,8 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; begin if not DecoratorResult.IsCallable then ThrowTypeError(SErrorFieldDecoratorReturn, SSuggestDecoratorFunction); - TGocciaClassValue(ClassVal).AddFieldInitializer( - Name, DecoratorResult, IsPrivate, IsStatic); + TGocciaClassValue(ClassVal).AddFieldInitializerWithKey( + ElementName, ElementKey, DecoratorResult, IsPrivate, IsStatic); end; 'a': begin @@ -6182,26 +6198,26 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; begin if IsStatic then TGocciaClassValue(ClassVal).AddStaticGetter( - Name, TGocciaFunctionBase(NewGetter)) + ElementName, TGocciaFunctionBase(NewGetter)) else TGocciaClassValue(ClassVal).AddGetter( - Name, TGocciaFunctionBase(NewGetter)); + ElementName, TGocciaFunctionBase(NewGetter)); end; if Assigned(NewSetter) and not (NewSetter is TGocciaUndefinedLiteralValue) then begin if IsStatic then TGocciaClassValue(ClassVal).AddStaticSetter( - Name, TGocciaFunctionBase(NewSetter)) + ElementName, TGocciaFunctionBase(NewSetter)) else TGocciaClassValue(ClassVal).AddSetter( - Name, TGocciaFunctionBase(NewSetter)); + ElementName, TGocciaFunctionBase(NewSetter)); end; if Assigned(NewInit) and not (NewInit is TGocciaUndefinedLiteralValue) and NewInit.IsCallable then - TGocciaClassValue(ClassVal).AddFieldInitializer( - Name, NewInit, IsPrivate, IsStatic); + TGocciaClassValue(ClassVal).AddFieldInitializerWithKey( + ElementName, ElementKey, NewInit, IsPrivate, IsStatic); end; end; end; @@ -9709,8 +9725,13 @@ // ES2022 §15.7.14: execute static block closure with this = class BeginDecorators(RegisterToValue(FRegisters[A]), RegisterToValue(FRegisters[A + 1])); OP_APPLY_ELEMENT_DECORATOR_CONST: - ApplyElementDecorator(RegisterToValue(FRegisters[A]), - Template.GetConstantUnchecked(C).StringValue); + if B <> 0 then + ApplyElementDecorator(RegisterToValue(FRegisters[A]), + Template.GetConstantUnchecked(C).StringValue, + RegisterToValue(FRegisters[B])) + else + ApplyElementDecorator(RegisterToValue(FRegisters[A]), + Template.GetConstantUnchecked(C).StringValue); OP_APPLY_CLASS_DECORATOR: ApplyClassDecorator(RegisterToValue(FRegisters[A])); diff --git a/source/units/Goccia.Values.ClassValue.pas b/source/units/Goccia.Values.ClassValue.pas index 48527f7f..1dc55b3d 100644 --- a/source/units/Goccia.Values.ClassValue.pas +++ b/source/units/Goccia.Values.ClassValue.pas @@ -54,6 +54,7 @@ TGocciaClassValue = class(TGocciaObjectValue) FFieldInitializers: array of TGocciaValue; FDecoratorFieldInitializers: array of record Name: string; + ComputedKey: TGocciaValue; Initializer: TGocciaValue; IsPrivate: Boolean; IsStatic: Boolean; @@ -142,6 +143,7 @@ TGocciaClassValue = class(TGocciaObjectValue) procedure MarkReferences; override; procedure AddFieldInitializer(const AName: string; const AInitializer: TGocciaValue; const AIsPrivate, AIsStatic: Boolean); + procedure AddFieldInitializerWithKey(const AName: string; const AComputedKey: TGocciaValue; const AInitializer: TGocciaValue; const AIsPrivate, AIsStatic: Boolean); procedure SetMethodInitializers(const AInitializers: array of TGocciaValue); procedure SetFieldInitializers(const AInitializers: array of TGocciaValue); procedure AppendMethodInitializers(const AInitializers: array of TGocciaValue); @@ -438,8 +440,12 @@ procedure TGocciaClassValue.MarkReferences; if Assigned(FFieldInitializers[I]) then FFieldInitializers[I].MarkReferences; for I := 0 to High(FDecoratorFieldInitializers) do + begin if Assigned(FDecoratorFieldInitializers[I].Initializer) then FDecoratorFieldInitializers[I].Initializer.MarkReferences; + if Assigned(FDecoratorFieldInitializers[I].ComputedKey) then + FDecoratorFieldInitializers[I].ComputedKey.MarkReferences; + end; for I := 0 to High(FFieldOrder) do if Assigned(FFieldOrder[I].ComputedKey) then FFieldOrder[I].ComputedKey.MarkReferences; @@ -708,9 +714,15 @@ function TGocciaClassValue.GetStaticPropertySetter(const AName: string): TGoccia end; procedure TGocciaClassValue.AddFieldInitializer(const AName: string; const AInitializer: TGocciaValue; const AIsPrivate, AIsStatic: Boolean); +begin + AddFieldInitializerWithKey(AName, nil, AInitializer, AIsPrivate, AIsStatic); +end; + +procedure TGocciaClassValue.AddFieldInitializerWithKey(const AName: string; const AComputedKey: TGocciaValue; const AInitializer: TGocciaValue; const AIsPrivate, AIsStatic: Boolean); begin SetLength(FDecoratorFieldInitializers, Length(FDecoratorFieldInitializers) + 1); FDecoratorFieldInitializers[High(FDecoratorFieldInitializers)].Name := AName; + FDecoratorFieldInitializers[High(FDecoratorFieldInitializers)].ComputedKey := AComputedKey; FDecoratorFieldInitializers[High(FDecoratorFieldInitializers)].Initializer := AInitializer; FDecoratorFieldInitializers[High(FDecoratorFieldInitializers)].IsPrivate := AIsPrivate; FDecoratorFieldInitializers[High(FDecoratorFieldInitializers)].IsStatic := AIsStatic; @@ -814,6 +826,8 @@ procedure TGocciaClassValue.RunDecoratorFieldInitializers(const AInstance: TGocc Idx: Integer; Args: TGocciaArgumentsCollection; InitResult, OriginalValue: TGocciaValue; + PropertyKey: TGocciaValue; + PropertyName: string; begin for Idx := 0 to High(FDecoratorFieldInitializers) do begin @@ -822,7 +836,18 @@ procedure TGocciaClassValue.RunDecoratorFieldInitializers(const AInstance: TGocc if Assigned(AInstance) and (AInstance is TGocciaObjectValue) then begin - OriginalValue := TGocciaObjectValue(AInstance).GetProperty(FDecoratorFieldInitializers[Idx].Name); + PropertyKey := FDecoratorFieldInitializers[Idx].ComputedKey; + if PropertyKey is TGocciaSymbolValue then + OriginalValue := TGocciaObjectValue(AInstance).GetSymbolProperty( + TGocciaSymbolValue(PropertyKey)) + else + begin + if Assigned(PropertyKey) then + PropertyName := PropertyKey.ToStringLiteral.Value + else + PropertyName := FDecoratorFieldInitializers[Idx].Name; + OriginalValue := TGocciaObjectValue(AInstance).GetProperty(PropertyName); + end; if not Assigned(OriginalValue) then OriginalValue := TGocciaUndefinedLiteralValue.UndefinedValue; @@ -830,7 +855,13 @@ procedure TGocciaClassValue.RunDecoratorFieldInitializers(const AInstance: TGocc try InitResult := TGocciaFunctionBase(FDecoratorFieldInitializers[Idx].Initializer).Call(Args, AInstance); if Assigned(InitResult) and not (InitResult is TGocciaUndefinedLiteralValue) then - TGocciaObjectValue(AInstance).AssignProperty(FDecoratorFieldInitializers[Idx].Name, InitResult); + begin + if PropertyKey is TGocciaSymbolValue then + TGocciaObjectValue(AInstance).AssignSymbolProperty( + TGocciaSymbolValue(PropertyKey), InitResult) + else + TGocciaObjectValue(AInstance).AssignProperty(PropertyName, InitResult); + end; finally Args.Free; end; diff --git a/tests/language/decorators/computed-field-decorator.js b/tests/language/decorators/computed-field-decorator.js new file mode 100644 index 00000000..8f095a5c --- /dev/null +++ b/tests/language/decorators/computed-field-decorator.js @@ -0,0 +1,58 @@ +/*--- +description: Decorated computed fields use the resolved property key in context and initializer wiring +features: [decorators, class-fields, computed-property-names] +---*/ + +describe("computed field decorators", () => { + test("string computed field context and initializer use resolved key", () => { + let receivedName; + const key = "x"; + const decorate = (value, context) => { + receivedName = context.name; + return (initialValue) => initialValue + 1; + }; + + class C { + @decorate + [key] = 41; + } + + const instance = new C(); + expect(receivedName).toBe("x"); + expect(instance.x).toBe(42); + }); + + test("symbol computed field context and initializer preserve symbol key", () => { + let receivedName; + const key = Symbol("field"); + const decorate = (value, context) => { + receivedName = context.name; + return (initialValue) => initialValue + 1; + }; + + class C { + @decorate + [key] = 41; + } + + const instance = new C(); + expect(receivedName === key).toBe(true); + expect(instance[key]).toBe(42); + }); + + test("static computed field context uses resolved key", () => { + let receivedName; + const key = "staticName"; + const decorate = (value, context) => { + receivedName = context.name; + }; + + class C { + @decorate + static [key] = 1; + } + + expect(receivedName).toBe("staticName"); + expect(C.staticName).toBe(1); + }); +}); From 3165c91f0e1ba9259add339514254d6424cca2a2 Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Wed, 20 May 2026 21:52:15 +0100 Subject: [PATCH 4/8] Handle computed keys in decorated class elements - Preserve computed property keys across fields, methods, getters, and setters - Support symbol keys in decorator access helpers - Add regression coverage for computed field decorators --- source/units/Goccia.Compiler.Statements.pas | 19 +- source/units/Goccia.Evaluator.Decorators.pas | 78 +++++- source/units/Goccia.Evaluator.pas | 252 +++++++++++++++--- source/units/Goccia.VM.pas | 249 ++++++++++++----- .../decorators/computed-field-decorator.js | 88 ++++++ 5 files changed, 575 insertions(+), 111 deletions(-) diff --git a/source/units/Goccia.Compiler.Statements.pas b/source/units/Goccia.Compiler.Statements.pas index 2b70a3e0..41fe0de3 100644 --- a/source/units/Goccia.Compiler.Statements.pas +++ b/source/units/Goccia.Compiler.Statements.pas @@ -3707,8 +3707,12 @@ procedure CompileComputedElements(const ACtx: TGocciaCompilationContext; Elem: TGocciaClassElement; KeyReg: UInt8; ComputedKeyName: string; + ClassKeyPrefix: string; + NeedsKeyLocal: Boolean; + KeyIsLocal: Boolean; begin SetLength(AComputedFieldKeyLocals, 0); + ClassKeyPrefix := IntToHex(PtrUInt(AClassDef), SizeOf(PtrUInt) * 2); for I := 0 to High(AClassDef.FElements) do begin Elem := AClassDef.FElements[I]; @@ -3717,14 +3721,18 @@ procedure CompileComputedElements(const ACtx: TGocciaCompilationContext; if not (Elem.Kind in [cekGetter, cekSetter, cekMethod, cekField]) then Continue; - if Elem.Kind = cekField then + NeedsKeyLocal := (Elem.Kind = cekField) or (Length(Elem.Decorators) > 0); + KeyIsLocal := False; + if NeedsKeyLocal then begin SetLength(AComputedFieldKeyLocals, Length(AComputedFieldKeyLocals) + 1); - ComputedKeyName := Format('#computed-field-key:%d', [I]); + ComputedKeyName := Format('#computed-element-key:%s:%d', + [ClassKeyPrefix, I]); AComputedFieldKeyLocals[High(AComputedFieldKeyLocals)].ElementIndex := I; AComputedFieldKeyLocals[High(AComputedFieldKeyLocals)].Name := ComputedKeyName; KeyReg := ACtx.Scope.DeclareLocal(ComputedKeyName, False); + KeyIsLocal := True; end else KeyReg := ACtx.Scope.AllocateRegister; @@ -3758,7 +3766,8 @@ procedure CompileComputedElements(const ACtx: TGocciaCompilationContext; Elem.MethodNode, Elem.IsStatic); end; - ACtx.Scope.FreeRegister; + if not KeyIsLocal then + ACtx.Scope.FreeRegister; end; end; @@ -4137,13 +4146,13 @@ procedure CompileDecoratorOrchestration( PairReg := ACtx.Scope.AllocateRegister; ExtraReg := ACtx.Scope.AllocateRegister; EmitInstruction(ACtx, EncodeABC(OP_MOVE, PairReg, DecoRegs[I][J], 0)); - if (Elem.Kind = cekField) and Elem.IsComputed then + if Elem.IsComputed then begin ComputedKeyName := FindComputedFieldKeyLocalName( AComputedFieldKeyLocals, I); LocalIdx := ACtx.Scope.ResolveLocal(ComputedKeyName); if LocalIdx < 0 then - raise Exception.Create('Compiler error: computed decorator field key was not captured'); + raise Exception.Create('Compiler error: computed decorator element key was not captured'); EmitInstruction(ACtx, EncodeABC(OP_MOVE, ExtraReg, ACtx.Scope.GetLocal(LocalIdx).Slot, 0)); EmitInstruction(ACtx, EncodeABC(OP_APPLY_ELEMENT_DECORATOR_CONST, diff --git a/source/units/Goccia.Evaluator.Decorators.pas b/source/units/Goccia.Evaluator.Decorators.pas index 110cdd64..ada4f7d4 100644 --- a/source/units/Goccia.Evaluator.Decorators.pas +++ b/source/units/Goccia.Evaluator.Decorators.pas @@ -22,23 +22,28 @@ TGocciaAccessGetter = class private FTarget: TGocciaValue; FPropertyName: string; + FPropertyKey: TGocciaValue; public constructor Create(const ATarget: TGocciaValue; const APropertyName: string); + constructor CreateWithKey(const ATarget: TGocciaValue; const APropertyKey: TGocciaValue); function Get(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; end; TGocciaAccessSetter = class private FPropertyName: string; + FPropertyKey: TGocciaValue; public constructor Create(const APropertyName: string); + constructor CreateWithKey(const APropertyKey: TGocciaValue); function SetValue(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; end; implementation uses - Goccia.Values.ObjectValue; + Goccia.Values.ObjectValue, + Goccia.Values.SymbolValue; { TGocciaInitializerCollector } @@ -67,14 +72,40 @@ constructor TGocciaAccessGetter.Create(const ATarget: TGocciaValue; const APrope begin FTarget := ATarget; FPropertyName := APropertyName; + FPropertyKey := nil; +end; + +constructor TGocciaAccessGetter.CreateWithKey(const ATarget: TGocciaValue; const APropertyKey: TGocciaValue); +begin + FTarget := ATarget; + FPropertyKey := APropertyKey; + if Assigned(APropertyKey) and not (APropertyKey is TGocciaSymbolValue) then + FPropertyName := APropertyKey.ToStringLiteral.Value + else + FPropertyName := ''; end; function TGocciaAccessGetter.Get(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; +var + Target: TGocciaValue; begin - if Assigned(AThisValue) and (AThisValue is TGocciaObjectValue) then - Result := AThisValue.GetProperty(FPropertyName) - else if Assigned(FTarget) then - Result := FTarget.GetProperty(FPropertyName) + if AArgs.Length > 0 then + Target := AArgs.GetElement(0) + else if Assigned(AThisValue) and (AThisValue is TGocciaObjectValue) then + Target := AThisValue + else + Target := FTarget; + + if FPropertyKey is TGocciaSymbolValue then + begin + if Target is TGocciaObjectValue then + Result := TGocciaObjectValue(Target).GetSymbolProperty( + TGocciaSymbolValue(FPropertyKey)) + else + Result := TGocciaUndefinedLiteralValue.UndefinedValue; + end + else if Assigned(Target) then + Result := Target.GetProperty(FPropertyName) else Result := TGocciaUndefinedLiteralValue.UndefinedValue; end; @@ -84,12 +115,45 @@ function TGocciaAccessGetter.Get(const AArgs: TGocciaArgumentsCollection; const constructor TGocciaAccessSetter.Create(const APropertyName: string); begin FPropertyName := APropertyName; + FPropertyKey := nil; +end; + +constructor TGocciaAccessSetter.CreateWithKey(const APropertyKey: TGocciaValue); +begin + FPropertyKey := APropertyKey; + if Assigned(APropertyKey) and not (APropertyKey is TGocciaSymbolValue) then + FPropertyName := APropertyKey.ToStringLiteral.Value + else + FPropertyName := ''; end; function TGocciaAccessSetter.SetValue(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; +var + Target: TGocciaValue; + NewValue: TGocciaValue; begin - if Assigned(AThisValue) and (AThisValue is TGocciaObjectValue) then - TGocciaObjectValue(AThisValue).AssignProperty(FPropertyName, AArgs.GetElement(0)); + if AArgs.Length >= 2 then + begin + Target := AArgs.GetElement(0); + NewValue := AArgs.GetElement(1); + end + else + begin + Target := AThisValue; + if AArgs.Length > 0 then + NewValue := AArgs.GetElement(0) + else + NewValue := TGocciaUndefinedLiteralValue.UndefinedValue; + end; + + if Target is TGocciaObjectValue then + begin + if FPropertyKey is TGocciaSymbolValue then + TGocciaObjectValue(Target).AssignSymbolProperty( + TGocciaSymbolValue(FPropertyKey), NewValue) + else + TGocciaObjectValue(Target).AssignProperty(FPropertyName, NewValue); + end; Result := TGocciaUndefinedLiteralValue.UndefinedValue; end; diff --git a/source/units/Goccia.Evaluator.pas b/source/units/Goccia.Evaluator.pas index 7a675d7c..6fc0a5d2 100644 --- a/source/units/Goccia.Evaluator.pas +++ b/source/units/Goccia.Evaluator.pas @@ -4186,7 +4186,7 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const HasDecorators: Boolean; HasReplayedComputedKey: Boolean; FieldOrderEntries: array of TGocciaClassFieldOrderEntry; - ResolvedComputedFieldKeys: array of TGocciaValue; + ResolvedComputedElementKeys: array of TGocciaValue; Continuation: TGocciaGeneratorContinuation; MetadataObject: TGocciaObjectValue; SuperMetadata: TGocciaValue; @@ -4200,6 +4200,7 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const ElementKey: TGocciaValue; CurrentMethod, GetterFnValue, SetterFnValue: TGocciaValue; NewGetter, NewSetter, NewInit: TGocciaValue; + ExistingGetterValue, ExistingSetterValue: TGocciaValue; MethodCollector, FieldCollector, StaticFieldCollector, ClassCollector: TGocciaInitializerCollector; AccessGetterHelper: TGocciaAccessGetter; AccessSetterHelper: TGocciaAccessSetter; @@ -4220,6 +4221,91 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const ASetterExpression, AContext, MethodSuperClass, True)); TGocciaMethodValue(Result).OwningClass := ClassValue; end; + + function GetDecoratedDataProperty(const AIsStatic: Boolean; + const AName: string; const AKey: TGocciaValue): TGocciaValue; + begin + if AKey is TGocciaSymbolValue then + begin + if AIsStatic then + Result := ClassValue.GetSymbolProperty(TGocciaSymbolValue(AKey)) + else + Result := ClassValue.Prototype.GetSymbolProperty( + TGocciaSymbolValue(AKey)); + end + else if AIsStatic then + Result := ClassValue.GetProperty(AName) + else + Result := ClassValue.Prototype.GetProperty(AName); + end; + + procedure DefineDecoratedDataProperty(const AIsStatic: Boolean; + const AName: string; const AKey, AValue: TGocciaValue); + begin + if AKey is TGocciaSymbolValue then + begin + if AIsStatic then + ClassValue.DefineSymbolProperty( + TGocciaSymbolValue(AKey), + TGocciaPropertyDescriptorData.Create( + AValue, [pfConfigurable, pfWritable])) + else + ClassValue.Prototype.DefineSymbolProperty( + TGocciaSymbolValue(AKey), + TGocciaPropertyDescriptorData.Create( + AValue, [pfConfigurable, pfWritable])); + end + else if AIsStatic then + ClassValue.DefineProperty(AName, + TGocciaPropertyDescriptorData.Create( + AValue, [pfConfigurable, pfWritable])) + else + ClassValue.Prototype.AssignProperty(AName, AValue); + end; + + function GetDecoratedAccessorDescriptor(const AIsStatic: Boolean; + const AName: string; const AKey: TGocciaValue): TGocciaPropertyDescriptor; + begin + if AKey is TGocciaSymbolValue then + begin + if AIsStatic then + Result := ClassValue.GetOwnStaticSymbolDescriptor( + TGocciaSymbolValue(AKey)) + else + Result := ClassValue.Prototype.GetOwnSymbolPropertyDescriptor( + TGocciaSymbolValue(AKey)); + end + else if AIsStatic then + Result := ClassValue.GetOwnPropertyDescriptor(AName) + else + Result := ClassValue.Prototype.GetOwnPropertyDescriptor(AName); + end; + + procedure DefineDecoratedAccessorProperty(const AIsStatic: Boolean; + const AName: string; const AKey, AGetter, ASetter: TGocciaValue); + begin + if AKey is TGocciaSymbolValue then + begin + if AIsStatic then + ClassValue.DefineSymbolProperty( + TGocciaSymbolValue(AKey), + TGocciaPropertyDescriptorAccessor.Create( + AGetter, ASetter, [pfConfigurable])) + else + ClassValue.Prototype.DefineSymbolProperty( + TGocciaSymbolValue(AKey), + TGocciaPropertyDescriptorAccessor.Create( + AGetter, ASetter, [pfConfigurable])); + end + else if AIsStatic then + ClassValue.DefineProperty(AName, + TGocciaPropertyDescriptorAccessor.Create( + AGetter, ASetter, [pfConfigurable])) + else + ClassValue.Prototype.DefineProperty(AName, + TGocciaPropertyDescriptorAccessor.Create( + AGetter, ASetter, [pfConfigurable, pfWritable])); + end; begin Continuation := CurrentGeneratorContinuation; SuperClass := nil; @@ -4358,7 +4444,7 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const ClassValue.AddStaticSetter(SetterPair.Key, SetterFunction); end; - SetLength(ResolvedComputedFieldKeys, Length(AClassDef.FElements)); + SetLength(ResolvedComputedElementKeys, Length(AClassDef.FElements)); // Handle computed class element names in source order via FElements. for I := 0 to High(AClassDef.FElements) do @@ -4383,9 +4469,11 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const if Assigned(Continuation) then Continuation.SaveExpressionValue(Elem.ComputedKeyExpression, ComputedKey); + ResolvedComputedElementKeys[I] := ComputedKey; + case Elem.Kind of cekField: - ResolvedComputedFieldKeys[I] := ComputedKey; + ; cekMethod: begin Method := TGocciaMethodValue(EvaluateClassMethod(Elem.MethodNode, AContext, MethodSuperClass)); @@ -4494,9 +4582,9 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const FieldOrderEntries[I].ComputedKey := nil; if FieldOrderEntries[I].IsComputed and (AClassDef.FFieldOrder[I].ElementIndex >= 0) and - (AClassDef.FFieldOrder[I].ElementIndex <= High(ResolvedComputedFieldKeys)) then + (AClassDef.FFieldOrder[I].ElementIndex <= High(ResolvedComputedElementKeys)) then FieldOrderEntries[I].ComputedKey := - ResolvedComputedFieldKeys[AClassDef.FFieldOrder[I].ElementIndex]; + ResolvedComputedElementKeys[AClassDef.FFieldOrder[I].ElementIndex]; end; ClassValue.SetFieldOrder(FieldOrderEntries); end; @@ -4533,8 +4621,8 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const else if Elem.IsComputed then begin ComputedKey := nil; - if I <= High(ResolvedComputedFieldKeys) then - ComputedKey := ResolvedComputedFieldKeys[I]; + if I <= High(ResolvedComputedElementKeys) then + ComputedKey := ResolvedComputedElementKeys[I]; if not Assigned(ComputedKey) then ComputedKey := ToPropertyKey(EvaluateExpression( Elem.ComputedKeyExpression, AContext)); @@ -4625,9 +4713,9 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const ElementName := Elem.Name; ElementKey := nil; if (not Elem.IsPrivate) and Elem.IsComputed and - (I <= High(ResolvedComputedFieldKeys)) then + (I <= High(ResolvedComputedElementKeys)) then begin - ElementKey := ResolvedComputedFieldKeys[I]; + ElementKey := ResolvedComputedElementKeys[I]; if ElementKey is TGocciaSymbolValue then ContextObject.AssignProperty(PROP_NAME, ElementKey) else if Assigned(ElementKey) then @@ -4663,20 +4751,64 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const begin if Elem.IsPrivate then CurrentMethod := ClassValue.GetPrivateMethod(ElementName) - else if Elem.IsStatic then - CurrentMethod := ClassValue.GetProperty(ElementName) else - CurrentMethod := ClassValue.Prototype.GetProperty(ElementName); + CurrentMethod := GetDecoratedDataProperty( + Elem.IsStatic, ElementName, ElementKey); - AccessGetterHelper := TGocciaAccessGetter.Create(CurrentMethod, ElementName); + if ElementKey is TGocciaSymbolValue then + AccessGetterHelper := TGocciaAccessGetter.CreateWithKey( + CurrentMethod, ElementKey) + else + AccessGetterHelper := TGocciaAccessGetter.Create( + CurrentMethod, ElementName); AccessObject.AssignProperty(PROP_GET, TGocciaNativeFunctionValue.CreateWithoutPrototype(AccessGetterHelper.Get, PROP_GET, 0)); ContextObject.AssignProperty(PROP_ACCESS, AccessObject); end; + cekGetter: + begin + if not Elem.IsPrivate then + begin + if ElementKey is TGocciaSymbolValue then + AccessGetterHelper := TGocciaAccessGetter.CreateWithKey( + nil, ElementKey) + else + AccessGetterHelper := TGocciaAccessGetter.Create( + nil, ElementName); + AccessObject.AssignProperty(PROP_GET, + TGocciaNativeFunctionValue.CreateWithoutPrototype(AccessGetterHelper.Get, PROP_GET, 0)); + ContextObject.AssignProperty(PROP_ACCESS, AccessObject); + end; + end; + cekSetter: + begin + if not Elem.IsPrivate then + begin + if ElementKey is TGocciaSymbolValue then + AccessSetterHelper := TGocciaAccessSetter.CreateWithKey( + ElementKey) + else + AccessSetterHelper := TGocciaAccessSetter.Create(ElementName); + AccessObject.AssignProperty(PROP_SET, + TGocciaNativeFunctionValue.CreateWithoutPrototype(AccessSetterHelper.SetValue, PROP_SET, 1)); + ContextObject.AssignProperty(PROP_ACCESS, AccessObject); + end; + end; cekField, cekAccessor: begin - AccessGetterHelper := TGocciaAccessGetter.Create(nil, ElementName); - AccessSetterHelper := TGocciaAccessSetter.Create(ElementName); + if ElementKey is TGocciaSymbolValue then + begin + AccessGetterHelper := TGocciaAccessGetter.CreateWithKey( + nil, ElementKey); + AccessSetterHelper := TGocciaAccessSetter.CreateWithKey( + ElementKey); + end + else + begin + AccessGetterHelper := TGocciaAccessGetter.Create( + nil, ElementName); + AccessSetterHelper := TGocciaAccessSetter.Create(ElementName); + end; AccessObject.AssignProperty(PROP_GET, TGocciaNativeFunctionValue.CreateWithoutPrototype(AccessGetterHelper.Get, PROP_GET, 0)); AccessObject.AssignProperty(PROP_SET, @@ -4704,10 +4836,9 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const begin if Elem.IsPrivate then DecoratorArgs.Add(ClassValue.GetPrivateMethod(ElementName)) - else if Elem.IsStatic then - DecoratorArgs.Add(ClassValue.GetProperty(ElementName)) else - DecoratorArgs.Add(ClassValue.Prototype.GetProperty(ElementName)); + DecoratorArgs.Add(GetDecoratedDataProperty( + Elem.IsStatic, ElementName, ElementKey)); DecoratorArgs.Add(ContextObject); DecoratorResult := TGocciaFunctionBase(DecoratorFn).Call(DecoratorArgs, TGocciaUndefinedLiteralValue.UndefinedValue); @@ -4722,12 +4853,9 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const if DecoratorResult is TGocciaMethodValue then ClassValue.AddPrivateMethod(ElementName, TGocciaMethodValue(DecoratorResult)); end - else if Elem.IsStatic then - ClassValue.DefineProperty(ElementName, - TGocciaPropertyDescriptorData.Create( - DecoratorResult, [pfConfigurable, pfWritable])) else - ClassValue.Prototype.AssignProperty(ElementName, DecoratorResult); + DefineDecoratedDataProperty( + Elem.IsStatic, ElementName, ElementKey, DecoratorResult); end; end; @@ -4735,10 +4863,16 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const begin if Elem.IsPrivate then GetterFnValue := ClassValue.PrivatePropertyGetter[ElementName] - else if Elem.IsStatic then - GetterFnValue := ClassValue.StaticPropertyGetter[ElementName] else - GetterFnValue := ClassValue.PropertyGetter[ElementName]; + begin + ExistingDescriptor := GetDecoratedAccessorDescriptor( + Elem.IsStatic, ElementName, ElementKey); + GetterFnValue := nil; + if (ExistingDescriptor is TGocciaPropertyDescriptorAccessor) and + Assigned(TGocciaPropertyDescriptorAccessor(ExistingDescriptor).Getter) then + GetterFnValue := + TGocciaPropertyDescriptorAccessor(ExistingDescriptor).Getter; + end; DecoratorArgs.Add(GetterFnValue); DecoratorArgs.Add(ContextObject); @@ -4752,10 +4886,19 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const if Elem.IsPrivate then ClassValue.AddPrivateGetter(ElementName, TGocciaFunctionValue(DecoratorResult)) - else if Elem.IsStatic then - ClassValue.AddStaticGetter(ElementName, TGocciaFunctionValue(DecoratorResult)) else - ClassValue.AddGetter(ElementName, TGocciaFunctionValue(DecoratorResult)); + begin + ExistingDescriptor := GetDecoratedAccessorDescriptor( + Elem.IsStatic, ElementName, ElementKey); + ExistingSetterValue := nil; + if (ExistingDescriptor is TGocciaPropertyDescriptorAccessor) and + Assigned(TGocciaPropertyDescriptorAccessor(ExistingDescriptor).Setter) then + ExistingSetterValue := + TGocciaPropertyDescriptorAccessor(ExistingDescriptor).Setter; + DefineDecoratedAccessorProperty( + Elem.IsStatic, ElementName, ElementKey, + DecoratorResult, ExistingSetterValue); + end; end; end; @@ -4763,10 +4906,16 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const begin if Elem.IsPrivate then SetterFnValue := ClassValue.PrivatePropertySetter[ElementName] - else if Elem.IsStatic then - SetterFnValue := ClassValue.StaticPropertySetter[ElementName] else - SetterFnValue := ClassValue.PropertySetter[ElementName]; + begin + ExistingDescriptor := GetDecoratedAccessorDescriptor( + Elem.IsStatic, ElementName, ElementKey); + SetterFnValue := nil; + if (ExistingDescriptor is TGocciaPropertyDescriptorAccessor) and + Assigned(TGocciaPropertyDescriptorAccessor(ExistingDescriptor).Setter) then + SetterFnValue := + TGocciaPropertyDescriptorAccessor(ExistingDescriptor).Setter; + end; DecoratorArgs.Add(SetterFnValue); DecoratorArgs.Add(ContextObject); @@ -4780,10 +4929,19 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const if Elem.IsPrivate then ClassValue.AddPrivateSetter(ElementName, TGocciaFunctionValue(DecoratorResult)) - else if Elem.IsStatic then - ClassValue.AddStaticSetter(ElementName, TGocciaFunctionValue(DecoratorResult)) else - ClassValue.AddSetter(ElementName, TGocciaFunctionValue(DecoratorResult)); + begin + ExistingDescriptor := GetDecoratedAccessorDescriptor( + Elem.IsStatic, ElementName, ElementKey); + ExistingGetterValue := nil; + if (ExistingDescriptor is TGocciaPropertyDescriptorAccessor) and + Assigned(TGocciaPropertyDescriptorAccessor(ExistingDescriptor).Getter) then + ExistingGetterValue := + TGocciaPropertyDescriptorAccessor(ExistingDescriptor).Getter; + DefineDecoratedAccessorProperty( + Elem.IsStatic, ElementName, ElementKey, + ExistingGetterValue, DecoratorResult); + end; end; end; @@ -4805,8 +4963,19 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const cekAccessor: begin AutoAccessorValue := TGocciaObjectValue.Create; - AutoAccessorValue.AssignProperty(PROP_GET, ClassValue.PropertyGetter[ElementName]); - AutoAccessorValue.AssignProperty(PROP_SET, ClassValue.PropertySetter[ElementName]); + ExistingDescriptor := GetDecoratedAccessorDescriptor( + Elem.IsStatic, ElementName, ElementKey); + ExistingGetterValue := nil; + ExistingSetterValue := nil; + if ExistingDescriptor is TGocciaPropertyDescriptorAccessor then + begin + ExistingGetterValue := + TGocciaPropertyDescriptorAccessor(ExistingDescriptor).Getter; + ExistingSetterValue := + TGocciaPropertyDescriptorAccessor(ExistingDescriptor).Setter; + end; + AutoAccessorValue.AssignProperty(PROP_GET, ExistingGetterValue); + AutoAccessorValue.AssignProperty(PROP_SET, ExistingSetterValue); DecoratorArgs.Add(AutoAccessorValue); DecoratorArgs.Add(ContextObject); @@ -4823,9 +4992,14 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const NewInit := DecResultObj.GetProperty(PROP_INIT); if Assigned(NewGetter) and not (NewGetter is TGocciaUndefinedLiteralValue) then - ClassValue.AddGetter(ElementName, TGocciaFunctionValue(NewGetter)); + ExistingGetterValue := NewGetter; if Assigned(NewSetter) and not (NewSetter is TGocciaUndefinedLiteralValue) then - ClassValue.AddSetter(ElementName, TGocciaFunctionValue(NewSetter)); + ExistingSetterValue := NewSetter; + if (Assigned(NewGetter) and not (NewGetter is TGocciaUndefinedLiteralValue)) or + (Assigned(NewSetter) and not (NewSetter is TGocciaUndefinedLiteralValue)) then + DefineDecoratedAccessorProperty( + Elem.IsStatic, ElementName, ElementKey, + ExistingGetterValue, ExistingSetterValue); if Assigned(NewInit) and not (NewInit is TGocciaUndefinedLiteralValue) and NewInit.IsCallable then ClassValue.AddFieldInitializerWithKey(ElementName, ElementKey, NewInit, Elem.IsPrivate, Elem.IsStatic); end; diff --git a/source/units/Goccia.VM.pas b/source/units/Goccia.VM.pas index 5084d0c7..d52ab62f 100644 --- a/source/units/Goccia.VM.pas +++ b/source/units/Goccia.VM.pas @@ -5964,6 +5964,93 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; KindStr: string; ExistingDescriptor: TGocciaPropertyDescriptor; GetterValue, SetterValue, NewGetter, NewSetter, NewInit: TGocciaValue; + + function GetDecoratedDataProperty(const AIsStatic: Boolean; + const AName: string; const AKey: TGocciaValue): TGocciaValue; + begin + if AKey is TGocciaSymbolValue then + begin + if AIsStatic then + Result := TGocciaClassValue(ClassVal).GetSymbolProperty( + TGocciaSymbolValue(AKey)) + else + Result := TGocciaClassValue(ClassVal).Prototype.GetSymbolProperty( + TGocciaSymbolValue(AKey)); + end + else if AIsStatic then + Result := TGocciaClassValue(ClassVal).GetProperty(AName) + else + Result := TGocciaClassValue(ClassVal).Prototype.GetProperty(AName); + end; + + procedure DefineDecoratedDataProperty(const AIsStatic: Boolean; + const AName: string; const AKey, AValue: TGocciaValue); + begin + if AKey is TGocciaSymbolValue then + begin + if AIsStatic then + TGocciaClassValue(ClassVal).DefineSymbolProperty( + TGocciaSymbolValue(AKey), + TGocciaPropertyDescriptorData.Create( + AValue, [pfConfigurable, pfWritable])) + else + TGocciaClassValue(ClassVal).Prototype.DefineSymbolProperty( + TGocciaSymbolValue(AKey), + TGocciaPropertyDescriptorData.Create( + AValue, [pfConfigurable, pfWritable])); + end + else if AIsStatic then + TGocciaClassValue(ClassVal).DefineProperty(AName, + TGocciaPropertyDescriptorData.Create( + AValue, [pfConfigurable, pfWritable])) + else + TGocciaClassValue(ClassVal).Prototype.AssignProperty(AName, AValue); + end; + + function GetDecoratedAccessorDescriptor(const AIsStatic: Boolean; + const AName: string; const AKey: TGocciaValue): TGocciaPropertyDescriptor; + begin + if AKey is TGocciaSymbolValue then + begin + if AIsStatic then + Result := TGocciaClassValue(ClassVal).GetOwnStaticSymbolDescriptor( + TGocciaSymbolValue(AKey)) + else + Result := TGocciaClassValue(ClassVal).Prototype + .GetOwnSymbolPropertyDescriptor(TGocciaSymbolValue(AKey)); + end + else if AIsStatic then + Result := TGocciaClassValue(ClassVal).GetOwnPropertyDescriptor(AName) + else + Result := TGocciaClassValue(ClassVal).Prototype + .GetOwnPropertyDescriptor(AName); + end; + + procedure DefineDecoratedAccessorProperty(const AIsStatic: Boolean; + const AName: string; const AKey, AGetter, ASetter: TGocciaValue); + begin + if AKey is TGocciaSymbolValue then + begin + if AIsStatic then + TGocciaClassValue(ClassVal).DefineSymbolProperty( + TGocciaSymbolValue(AKey), + TGocciaPropertyDescriptorAccessor.Create( + AGetter, ASetter, [pfConfigurable])) + else + TGocciaClassValue(ClassVal).Prototype.DefineSymbolProperty( + TGocciaSymbolValue(AKey), + TGocciaPropertyDescriptorAccessor.Create( + AGetter, ASetter, [pfConfigurable])); + end + else if AIsStatic then + TGocciaClassValue(ClassVal).DefineProperty(AName, + TGocciaPropertyDescriptorAccessor.Create( + AGetter, ASetter, [pfConfigurable])) + else + TGocciaClassValue(ClassVal).Prototype.DefineProperty(AName, + TGocciaPropertyDescriptorAccessor.Create( + AGetter, ASetter, [pfConfigurable, pfWritable])); + end; begin if not Assigned(FActiveDecoratorSession) then Exit; @@ -6032,20 +6119,64 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; begin if IsPrivate then ElementValue := TGocciaClassValue(ClassVal).GetPrivateMethod(Name) - else if IsStatic then - ElementValue := TGocciaClassValue(ClassVal).GetProperty(ElementName) else - ElementValue := TGocciaClassValue(ClassVal).Prototype.GetProperty(ElementName); - AccessGetterHelper := TGocciaAccessGetter.Create(ElementValue, ElementName); + ElementValue := GetDecoratedDataProperty( + IsStatic, ElementName, ElementKey); + if ElementKey is TGocciaSymbolValue then + AccessGetterHelper := TGocciaAccessGetter.CreateWithKey( + ElementValue, ElementKey) + else + AccessGetterHelper := TGocciaAccessGetter.Create( + ElementValue, ElementName); AccessObject.AssignProperty(PROP_GET, TGocciaNativeFunctionValue.CreateWithoutPrototype( AccessGetterHelper.Get, PROP_GET, 0)); ContextObject.AssignProperty(PROP_ACCESS, AccessObject); end; + 'g': + begin + if not IsPrivate then + begin + if ElementKey is TGocciaSymbolValue then + AccessGetterHelper := TGocciaAccessGetter.CreateWithKey( + nil, ElementKey) + else + AccessGetterHelper := TGocciaAccessGetter.Create(nil, ElementName); + AccessObject.AssignProperty(PROP_GET, + TGocciaNativeFunctionValue.CreateWithoutPrototype( + AccessGetterHelper.Get, PROP_GET, 0)); + ContextObject.AssignProperty(PROP_ACCESS, AccessObject); + end; + end; + 's': + begin + if not IsPrivate then + begin + if ElementKey is TGocciaSymbolValue then + AccessSetterHelper := TGocciaAccessSetter.CreateWithKey( + ElementKey) + else + AccessSetterHelper := TGocciaAccessSetter.Create(ElementName); + AccessObject.AssignProperty(PROP_SET, + TGocciaNativeFunctionValue.CreateWithoutPrototype( + AccessSetterHelper.SetValue, PROP_SET, 1)); + ContextObject.AssignProperty(PROP_ACCESS, AccessObject); + end; + end; 'f', 'a': begin - AccessGetterHelper := TGocciaAccessGetter.Create(nil, ElementName); - AccessSetterHelper := TGocciaAccessSetter.Create(ElementName); + if ElementKey is TGocciaSymbolValue then + begin + AccessGetterHelper := TGocciaAccessGetter.CreateWithKey( + nil, ElementKey); + AccessSetterHelper := TGocciaAccessSetter.CreateWithKey( + ElementKey); + end + else + begin + AccessGetterHelper := TGocciaAccessGetter.Create(nil, ElementName); + AccessSetterHelper := TGocciaAccessSetter.Create(ElementName); + end; AccessObject.AssignProperty(PROP_GET, TGocciaNativeFunctionValue.CreateWithoutPrototype( AccessGetterHelper.Get, PROP_GET, 0)); @@ -6073,39 +6204,50 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; begin if IsPrivate then ElementValue := TGocciaClassValue(ClassVal).PrivatePropertyGetter[Name] - else if IsStatic then - ElementValue := TGocciaClassValue(ClassVal).StaticPropertyGetter[ElementName] else - ElementValue := TGocciaClassValue(ClassVal).PropertyGetter[ElementName]; + begin + ExistingDescriptor := GetDecoratedAccessorDescriptor( + IsStatic, ElementName, ElementKey); + ElementValue := nil; + if (ExistingDescriptor is TGocciaPropertyDescriptorAccessor) and + Assigned(TGocciaPropertyDescriptorAccessor(ExistingDescriptor).Getter) then + ElementValue := + TGocciaPropertyDescriptorAccessor(ExistingDescriptor).Getter; + end; end; 's': begin if IsPrivate then ElementValue := TGocciaClassValue(ClassVal).PrivatePropertySetter[Name] - else if IsStatic then - ElementValue := TGocciaClassValue(ClassVal).StaticPropertySetter[ElementName] else - ElementValue := TGocciaClassValue(ClassVal).PropertySetter[ElementName]; + begin + ExistingDescriptor := GetDecoratedAccessorDescriptor( + IsStatic, ElementName, ElementKey); + ElementValue := nil; + if (ExistingDescriptor is TGocciaPropertyDescriptorAccessor) and + Assigned(TGocciaPropertyDescriptorAccessor(ExistingDescriptor).Setter) then + ElementValue := + TGocciaPropertyDescriptorAccessor(ExistingDescriptor).Setter; + end; end; 'f': ElementValue := TGocciaUndefinedLiteralValue.UndefinedValue; 'a': begin AutoAccessorValue := TGocciaObjectValue.Create; - if IsStatic then + ExistingDescriptor := GetDecoratedAccessorDescriptor( + IsStatic, ElementName, ElementKey); + GetterValue := nil; + SetterValue := nil; + if ExistingDescriptor is TGocciaPropertyDescriptorAccessor then begin - AutoAccessorValue.AssignProperty(PROP_GET, - TGocciaClassValue(ClassVal).StaticPropertyGetter[ElementName]); - AutoAccessorValue.AssignProperty(PROP_SET, - TGocciaClassValue(ClassVal).StaticPropertySetter[ElementName]); - end - else - begin - AutoAccessorValue.AssignProperty(PROP_GET, - TGocciaClassValue(ClassVal).PropertyGetter[ElementName]); - AutoAccessorValue.AssignProperty(PROP_SET, - TGocciaClassValue(ClassVal).PropertySetter[ElementName]); + GetterValue := TGocciaPropertyDescriptorAccessor( + ExistingDescriptor).Getter; + SetterValue := TGocciaPropertyDescriptorAccessor( + ExistingDescriptor).Setter; end; + AutoAccessorValue.AssignProperty(PROP_GET, GetterValue); + AutoAccessorValue.AssignProperty(PROP_SET, SetterValue); ElementValue := AutoAccessorValue; end; end; @@ -6132,50 +6274,46 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; if IsPrivate then TGocciaClassValue(ClassVal).Prototype.AssignProperty( '#' + Name, DecoratorResult) - else if IsStatic then - TGocciaClassValue(ClassVal).SetProperty(ElementName, DecoratorResult) else - TGocciaClassValue(ClassVal).Prototype.AssignProperty( - ElementName, DecoratorResult); + DefineDecoratedDataProperty( + IsStatic, ElementName, ElementKey, DecoratorResult); end; 'g': begin if not DecoratorResult.IsCallable then ThrowTypeError(SErrorGetterDecoratorReturn, SSuggestDecoratorFunction); - if IsStatic then - TGocciaClassValue(ClassVal).AddStaticGetter( - ElementName, TGocciaFunctionValue(DecoratorResult)) + if IsPrivate then + TGocciaClassValue(ClassVal).AddPrivateGetter( + Name, TGocciaFunctionBase(DecoratorResult)) else begin - ExistingDescriptor := TGocciaClassValue(ClassVal).Prototype - .GetOwnPropertyDescriptor(ElementName); + ExistingDescriptor := GetDecoratedAccessorDescriptor( + IsStatic, ElementName, ElementKey); SetterValue := nil; if (ExistingDescriptor is TGocciaPropertyDescriptorAccessor) and Assigned(TGocciaPropertyDescriptorAccessor(ExistingDescriptor).Setter) then SetterValue := TGocciaPropertyDescriptorAccessor(ExistingDescriptor).Setter; - TGocciaClassValue(ClassVal).Prototype.DefineProperty(ElementName, - TGocciaPropertyDescriptorAccessor.Create( - DecoratorResult, SetterValue, [pfEnumerable, pfConfigurable, pfWritable])); + DefineDecoratedAccessorProperty( + IsStatic, ElementName, ElementKey, DecoratorResult, SetterValue); end; end; 's': begin if not DecoratorResult.IsCallable then ThrowTypeError(SErrorSetterDecoratorReturn, SSuggestDecoratorFunction); - if IsStatic then - TGocciaClassValue(ClassVal).AddStaticSetter( - ElementName, TGocciaFunctionValue(DecoratorResult)) + if IsPrivate then + TGocciaClassValue(ClassVal).AddPrivateSetter( + Name, TGocciaFunctionBase(DecoratorResult)) else begin - ExistingDescriptor := TGocciaClassValue(ClassVal).Prototype - .GetOwnPropertyDescriptor(ElementName); + ExistingDescriptor := GetDecoratedAccessorDescriptor( + IsStatic, ElementName, ElementKey); GetterValue := nil; if (ExistingDescriptor is TGocciaPropertyDescriptorAccessor) and Assigned(TGocciaPropertyDescriptorAccessor(ExistingDescriptor).Getter) then GetterValue := TGocciaPropertyDescriptorAccessor(ExistingDescriptor).Getter; - TGocciaClassValue(ClassVal).Prototype.DefineProperty(ElementName, - TGocciaPropertyDescriptorAccessor.Create( - GetterValue, DecoratorResult, [pfEnumerable, pfConfigurable, pfWritable])); + DefineDecoratedAccessorProperty( + IsStatic, ElementName, ElementKey, GetterValue, DecoratorResult); end; end; 'f': @@ -6195,24 +6333,15 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; NewInit := DecResultObj.GetProperty(PROP_INIT); if Assigned(NewGetter) and not (NewGetter is TGocciaUndefinedLiteralValue) then - begin - if IsStatic then - TGocciaClassValue(ClassVal).AddStaticGetter( - ElementName, TGocciaFunctionBase(NewGetter)) - else - TGocciaClassValue(ClassVal).AddGetter( - ElementName, TGocciaFunctionBase(NewGetter)); - end; + GetterValue := NewGetter; if Assigned(NewSetter) and not (NewSetter is TGocciaUndefinedLiteralValue) then - begin - if IsStatic then - TGocciaClassValue(ClassVal).AddStaticSetter( - ElementName, TGocciaFunctionBase(NewSetter)) - else - TGocciaClassValue(ClassVal).AddSetter( - ElementName, TGocciaFunctionBase(NewSetter)); - end; + SetterValue := NewSetter; + + if (Assigned(NewGetter) and not (NewGetter is TGocciaUndefinedLiteralValue)) or + (Assigned(NewSetter) and not (NewSetter is TGocciaUndefinedLiteralValue)) then + DefineDecoratedAccessorProperty( + IsStatic, ElementName, ElementKey, GetterValue, SetterValue); if Assigned(NewInit) and not (NewInit is TGocciaUndefinedLiteralValue) and NewInit.IsCallable then diff --git a/tests/language/decorators/computed-field-decorator.js b/tests/language/decorators/computed-field-decorator.js index 8f095a5c..2528f8b4 100644 --- a/tests/language/decorators/computed-field-decorator.js +++ b/tests/language/decorators/computed-field-decorator.js @@ -40,6 +40,94 @@ describe("computed field decorators", () => { expect(instance[key]).toBe(42); }); + test("symbol computed field access helper preserves symbol key", () => { + const key = Symbol("field-access"); + const decorate = (value, context) => { + context.addInitializer(({ init() { context.access.set(this, 42); } }).init); + }; + + class C { + @decorate + [key] = 1; + } + + expect(new C()[key]).toBe(42); + }); + + test("symbol computed method decorator uses resolved key", () => { + let receivedName; + let accessGet; + const key = Symbol("method"); + const decorate = (value, context) => { + receivedName = context.name; + accessGet = context.access.get; + return () => 42; + }; + + class C { + @decorate + [key]() { + return 1; + } + } + + expect(receivedName === key).toBe(true); + const instance = new C(); + expect(instance[key]()).toBe(42); + expect(accessGet(instance)()).toBe(42); + }); + + test("symbol computed getter decorator uses resolved key", () => { + let receivedName; + let accessGet; + const key = Symbol("getter"); + const decorate = (value, context) => { + receivedName = context.name; + accessGet = context.access.get; + return () => 42; + }; + + class C { + @decorate + get [key]() { + return 1; + } + } + + expect(receivedName === key).toBe(true); + const instance = new C(); + expect(instance[key]).toBe(42); + expect(accessGet(instance)).toBe(42); + }); + + test("symbol computed setter decorator uses resolved key", () => { + let receivedName; + let accessSet; + let stored = 0; + const key = Symbol("setter"); + const decorate = (value, context) => { + receivedName = context.name; + accessSet = context.access.set; + return (next) => { + stored = next * 2; + }; + }; + + class C { + @decorate + set [key](next) { + stored = next; + } + } + + const instance = new C(); + instance[key] = 21; + expect(receivedName === key).toBe(true); + expect(stored).toBe(42); + accessSet(instance, 7); + expect(stored).toBe(14); + }); + test("static computed field context uses resolved key", () => { let receivedName; const key = "staticName"; From dbb56ef5f3fccc286b2287f473257f9dde52f8a7 Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Wed, 20 May 2026 22:37:48 +0100 Subject: [PATCH 5/8] fix: address computed decorator review findings --- source/units/Goccia.Bytecode.OpCodeNames.pas | 1 + source/units/Goccia.Bytecode.pas | 5 +- source/units/Goccia.Compiler.Statements.pas | 59 +++++-- source/units/Goccia.Evaluator.Decorators.pas | 16 +- source/units/Goccia.Evaluator.pas | 22 ++- source/units/Goccia.Parser.pas | 4 +- source/units/Goccia.VM.pas | 151 +++++++++--------- source/units/Goccia.Values.ClassValue.pas | 25 ++- .../decorators/auto-accessor-decorator.js | 25 +++ tests/language/decorators/auto-accessor.js | 26 +++ .../decorators/computed-field-decorator.js | 59 +++++++ 11 files changed, 289 insertions(+), 104 deletions(-) diff --git a/source/units/Goccia.Bytecode.OpCodeNames.pas b/source/units/Goccia.Bytecode.OpCodeNames.pas index 19dad575..6a1196dc 100644 --- a/source/units/Goccia.Bytecode.OpCodeNames.pas +++ b/source/units/Goccia.Bytecode.OpCodeNames.pas @@ -150,6 +150,7 @@ function OpCodeName(const AOp: UInt8): string; OP_DIV: Result := 'OP_DIV'; OP_MOD: Result := 'OP_MOD'; OP_POW: Result := 'OP_POW'; + OP_SETUP_AUTO_ACCESSOR_DYNAMIC: Result := 'OP_SETUP_AUTO_ACCESSOR_DYNAMIC'; OP_BAND: Result := 'OP_BAND'; OP_BOR: Result := 'OP_BOR'; OP_BXOR: Result := 'OP_BXOR'; diff --git a/source/units/Goccia.Bytecode.pas b/source/units/Goccia.Bytecode.pas index 7d321f0b..4cd2173e 100644 --- a/source/units/Goccia.Bytecode.pas +++ b/source/units/Goccia.Bytecode.pas @@ -61,7 +61,9 @@ interface // class fields. // v35 -> v36: added OP_TO_PROPERTY_KEY so delayed computed class field // definitions can reuse source-order property keys. - GOCCIA_FORMAT_VERSION = 36; + // v36 -> v37: added OP_SETUP_AUTO_ACCESSOR_DYNAMIC for computed + // auto-accessor keys. + GOCCIA_FORMAT_VERSION = 37; GOCCIA_BINARY_MAGIC: array[0..3] of Byte = (Ord('G'), Ord('B'), Ord('C'), 0); GOCCIA_NULLISH_MATCH_UNDEFINED = 0; GOCCIA_NULLISH_MATCH_NULL = 1; @@ -233,6 +235,7 @@ interface OP_DIV = 131, OP_MOD = 132, OP_POW = 133, + OP_SETUP_AUTO_ACCESSOR_DYNAMIC = 134, OP_BAND = 135, OP_BOR = 136, OP_BXOR = 137, diff --git a/source/units/Goccia.Compiler.Statements.pas b/source/units/Goccia.Compiler.Statements.pas index 41fe0de3..244b86ac 100644 --- a/source/units/Goccia.Compiler.Statements.pas +++ b/source/units/Goccia.Compiler.Statements.pas @@ -3718,10 +3718,11 @@ procedure CompileComputedElements(const ACtx: TGocciaCompilationContext; Elem := AClassDef.FElements[I]; if not Elem.IsComputed then Continue; - if not (Elem.Kind in [cekGetter, cekSetter, cekMethod, cekField]) then + if not (Elem.Kind in [cekGetter, cekSetter, cekMethod, cekField, cekAccessor]) then Continue; - NeedsKeyLocal := (Elem.Kind = cekField) or (Length(Elem.Decorators) > 0); + NeedsKeyLocal := (Elem.Kind in [cekField, cekAccessor]) or + (Length(Elem.Decorators) > 0); KeyIsLocal := False; if NeedsKeyLocal then begin @@ -3737,13 +3738,13 @@ procedure CompileComputedElements(const ACtx: TGocciaCompilationContext; else KeyReg := ACtx.Scope.AllocateRegister; ACtx.CompileExpression(Elem.ComputedKeyExpression, KeyReg); + if (Elem.Kind in [cekField, cekAccessor]) or + (Length(Elem.Decorators) > 0) then + EmitInstruction(ACtx, EncodeABC(OP_TO_PROPERTY_KEY, KeyReg, KeyReg, 0)); case Elem.Kind of cekField: begin - // ES2026 §15.7.10 ClassFieldDefinitionEvaluation evaluates - // ClassElementName, including ToPropertyKey, during class definition. - EmitInstruction(ACtx, EncodeABC(OP_TO_PROPERTY_KEY, KeyReg, KeyReg, 0)); Continue; end; cekGetter: @@ -3764,6 +3765,8 @@ procedure CompileComputedElements(const ACtx: TGocciaCompilationContext; cekMethod: CompileComputedMethodBody(ACtx, ATargetReg, KeyReg, Elem.MethodNode, Elem.IsStatic); + cekAccessor: + ; // Auto-accessor installation consumes the captured property key later. end; if not KeyIsLocal then @@ -3812,6 +3815,7 @@ procedure CompileFieldInitializer(const ACtx: TGocciaCompilationContext; Elem: TGocciaClassElement; FieldExpr: TGocciaExpression; ComputedKeyName: string; + AccessorBackingName: string; begin OldTemplate := ACtx.Template; OldScope := ACtx.Scope; @@ -3914,7 +3918,11 @@ procedure CompileFieldInitializer(const ACtx: TGocciaCompilationContext; Continue; ValReg := ChildScope.AllocateRegister; ACtx.CompileExpression(Elem.FieldInitializer, ValReg); - KeyIdx := ChildTemplate.AddConstantString('__accessor_' + Elem.Name); + if Elem.IsComputed then + AccessorBackingName := '__accessor_computed_' + IntToStr(I) + else + AccessorBackingName := '__accessor_' + Elem.Name; + KeyIdx := ChildTemplate.AddConstantString(AccessorBackingName); if KeyIdx > High(UInt8) then raise Exception.Create('Constant pool overflow: accessor backing field name index exceeds 255'); EmitInstruction(ChildCtx, EncodeABC(OP_SET_PROP_CONST, ThisReg, @@ -4056,12 +4064,16 @@ function HasDecoratorsOrAccessors( end; procedure CompileAutoAccessors(const ACtx: TGocciaCompilationContext; - const AClassReg: UInt8; const AClassDef: TGocciaClassDefinition); + const AClassReg: UInt8; const AClassDef: TGocciaClassDefinition; + const AComputedFieldKeyLocals: TComputedFieldKeyLocals); var I: Integer; Elem: TGocciaClassElement; NameIdx: UInt16; - PairReg, InitReg: UInt8; + KeyReg: UInt8; + LocalIdx: Integer; + ComputedKeyName: string; + BackingName: string; begin for I := 0 to High(AClassDef.FElements) do begin @@ -4069,17 +4081,29 @@ procedure CompileAutoAccessors(const ACtx: TGocciaCompilationContext; if Elem.Kind <> cekAccessor then Continue; - NameIdx := ACtx.Template.AddConstantString(Elem.Name); + if Elem.IsComputed then + BackingName := '__accessor_computed_' + IntToStr(I) + else + BackingName := Elem.Name; + + NameIdx := ACtx.Template.AddConstantString(BackingName); if NameIdx > High(UInt8) then raise Exception.Create('Constant pool overflow: accessor name index exceeds 255'); - PairReg := ACtx.Scope.AllocateRegister; - InitReg := ACtx.Scope.AllocateRegister; - EmitInstruction(ACtx, EncodeABC(OP_LOAD_UNDEFINED, InitReg, 0, 0)); - EmitInstruction(ACtx, EncodeABC(OP_SETUP_AUTO_ACCESSOR_CONST, PairReg, 0, - UInt8(NameIdx))); - ACtx.Scope.FreeRegister; - ACtx.Scope.FreeRegister; + if Elem.IsComputed then + begin + ComputedKeyName := FindComputedFieldKeyLocalName( + AComputedFieldKeyLocals, I); + LocalIdx := ACtx.Scope.ResolveLocal(ComputedKeyName); + if LocalIdx < 0 then + raise Exception.Create('Compiler error: computed auto-accessor key was not captured'); + KeyReg := ACtx.Scope.GetLocal(LocalIdx).Slot; + EmitInstruction(ACtx, EncodeABC(OP_SETUP_AUTO_ACCESSOR_DYNAMIC, + KeyReg, Ord(Elem.IsStatic), UInt8(NameIdx))); + end + else + EmitInstruction(ACtx, EncodeABC(OP_SETUP_AUTO_ACCESSOR_CONST, + 0, Ord(Elem.IsStatic), UInt8(NameIdx))); end; end; @@ -4209,7 +4233,8 @@ procedure CompileDecoratorAndAccessorPass( ACtx.Scope.FreeRegister; ACtx.Scope.FreeRegister; - CompileAutoAccessors(ACtx, AClassReg, AClassDef); + CompileAutoAccessors(ACtx, AClassReg, AClassDef, + AComputedFieldKeyLocals); CompileDecoratorOrchestration(ACtx, AClassReg, AClassDef, AComputedFieldKeyLocals); diff --git a/source/units/Goccia.Evaluator.Decorators.pas b/source/units/Goccia.Evaluator.Decorators.pas index ada4f7d4..716c0450 100644 --- a/source/units/Goccia.Evaluator.Decorators.pas +++ b/source/units/Goccia.Evaluator.Decorators.pas @@ -42,6 +42,7 @@ TGocciaAccessSetter = class implementation uses + Goccia.Values.ClassValue, Goccia.Values.ObjectValue, Goccia.Values.SymbolValue; @@ -98,7 +99,10 @@ function TGocciaAccessGetter.Get(const AArgs: TGocciaArgumentsCollection; const if FPropertyKey is TGocciaSymbolValue then begin - if Target is TGocciaObjectValue then + if Target is TGocciaClassValue then + Result := TGocciaClassValue(Target).GetSymbolProperty( + TGocciaSymbolValue(FPropertyKey)) + else if Target is TGocciaObjectValue then Result := TGocciaObjectValue(Target).GetSymbolProperty( TGocciaSymbolValue(FPropertyKey)) else @@ -149,8 +153,14 @@ function TGocciaAccessSetter.SetValue(const AArgs: TGocciaArgumentsCollection; c if Target is TGocciaObjectValue then begin if FPropertyKey is TGocciaSymbolValue then - TGocciaObjectValue(Target).AssignSymbolProperty( - TGocciaSymbolValue(FPropertyKey), NewValue) + begin + if Target is TGocciaClassValue then + TGocciaClassValue(Target).AssignSymbolProperty( + TGocciaSymbolValue(FPropertyKey), NewValue) + else + TGocciaObjectValue(Target).AssignSymbolProperty( + TGocciaSymbolValue(FPropertyKey), NewValue); + end else TGocciaObjectValue(Target).AssignProperty(FPropertyName, NewValue); end; diff --git a/source/units/Goccia.Evaluator.pas b/source/units/Goccia.Evaluator.pas index 6fc0a5d2..794d2f8f 100644 --- a/source/units/Goccia.Evaluator.pas +++ b/source/units/Goccia.Evaluator.pas @@ -4452,7 +4452,7 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const Elem := AClassDef.FElements[I]; if not Elem.IsComputed then Continue; - if not (Elem.Kind in [cekMethod, cekGetter, cekSetter, cekField]) then + if not (Elem.Kind in [cekMethod, cekGetter, cekSetter, cekField, cekAccessor]) then Continue; // ES2026 §15.4 ClassDefinitionEvaluation step 6.b for computed names: @@ -4474,6 +4474,8 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const case Elem.Kind of cekField: ; + cekAccessor: + ; cekMethod: begin Method := TGocciaMethodValue(EvaluateClassMethod(Elem.MethodNode, AContext, MethodSuperClass)); @@ -4595,12 +4597,26 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const if AClassDef.FElements[I].Kind = cekAccessor then begin Elem := AClassDef.FElements[I]; + ComputedKey := nil; AccessorBackingName := '__accessor_' + Elem.Name; + if Elem.IsComputed then + begin + if I <= High(ResolvedComputedElementKeys) then + ComputedKey := ResolvedComputedElementKeys[I]; + if not Assigned(ComputedKey) then + ComputedKey := ToPropertyKey(EvaluateExpression( + Elem.ComputedKeyExpression, AContext)); + AccessorBackingName := '__accessor_computed_' + IntToStr(I); + end; if Assigned(Elem.FieldInitializer) then ClassValue.AddInstanceProperty(AccessorBackingName, Elem.FieldInitializer); - ClassValue.AddAutoAccessor(Elem.Name, AccessorBackingName, Elem.IsStatic); + if Elem.IsComputed then + ClassValue.AddAutoAccessorWithKey( + Elem.Name, ComputedKey, AccessorBackingName, Elem.IsStatic) + else + ClassValue.AddAutoAccessor(Elem.Name, AccessorBackingName, Elem.IsStatic); end; end; @@ -5090,7 +5106,7 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const if Assigned(Continuation) then for I := 0 to High(AClassDef.FElements) do if AClassDef.FElements[I].IsComputed and - (AClassDef.FElements[I].Kind in [cekMethod, cekGetter, cekSetter, cekField]) then + (AClassDef.FElements[I].Kind in [cekMethod, cekGetter, cekSetter, cekField, cekAccessor]) then Continuation.ClearExpressionValue( AClassDef.FElements[I].ComputedKeyExpression); diff --git a/source/units/Goccia.Parser.pas b/source/units/Goccia.Parser.pas index 584b039c..22031914 100644 --- a/source/units/Goccia.Parser.pas +++ b/source/units/Goccia.Parser.pas @@ -5551,8 +5551,8 @@ function TGocciaParser.ParseClassBody(const AClassName: string): TGocciaClassDef Elements[High(Elements)].Name := MemberName; Elements[High(Elements)].IsStatic := IsStatic; Elements[High(Elements)].IsPrivate := IsPrivate; - Elements[High(Elements)].IsComputed := False; - Elements[High(Elements)].ComputedKeyExpression := nil; + Elements[High(Elements)].IsComputed := IsComputed; + Elements[High(Elements)].ComputedKeyExpression := ComputedKeyExpression; Elements[High(Elements)].Decorators := MemberDecorators; Elements[High(Elements)].FieldInitializer := PropertyValue; Elements[High(Elements)].TypeAnnotation := FieldType; diff --git a/source/units/Goccia.VM.pas b/source/units/Goccia.VM.pas index d52ab62f..d8ef8a27 100644 --- a/source/units/Goccia.VM.pas +++ b/source/units/Goccia.VM.pas @@ -184,7 +184,9 @@ TGocciaVM = class function FinalizeEnumValue(const AValue: TGocciaValue; const AName: string): TGocciaValue; procedure StampBytecodePrivateBrands(const AClassValue: TGocciaClassValue; const AInstance: TGocciaValue); - procedure SetupAutoAccessorValue(const AName: string); + procedure SetupAutoAccessorValue(const AName: string; const AIsStatic: Boolean); + procedure SetupAutoAccessorValueByKey(const AKey: TGocciaValue; + const ABackingName: string; const AIsStatic: Boolean); procedure RunClassInitializers(const AClassValue: TGocciaClassValue; const AInstance: TGocciaValue); function MaterializeArguments( @@ -5848,7 +5850,8 @@ function TGocciaVM.InvokeImplicitSuperInitializationRegisters( Result := TargetInstance; end; -procedure TGocciaVM.SetupAutoAccessorValue(const AName: string); +procedure TGocciaVM.SetupAutoAccessorValue(const AName: string; + const AIsStatic: Boolean); var ClassVal: TGocciaClassValue; begin @@ -5859,7 +5862,22 @@ procedure TGocciaVM.SetupAutoAccessorValue(const AName: string); ClassVal := TGocciaClassValue( TGocciaVMDecoratorSession(FActiveDecoratorSession).ClassValue); - ClassVal.AddAutoAccessor(AName, '__accessor_' + AName, False); + ClassVal.AddAutoAccessor(AName, '__accessor_' + AName, AIsStatic); +end; + +procedure TGocciaVM.SetupAutoAccessorValueByKey(const AKey: TGocciaValue; + const ABackingName: string; const AIsStatic: Boolean); +var + ClassVal: TGocciaClassValue; +begin + if not Assigned(FActiveDecoratorSession) then + Exit; + if not (TGocciaVMDecoratorSession(FActiveDecoratorSession).ClassValue is TGocciaClassValue) then + Exit; + + ClassVal := TGocciaClassValue( + TGocciaVMDecoratorSession(FActiveDecoratorSession).ClassValue); + ClassVal.AddAutoAccessorWithKey('', AKey, ABackingName, AIsStatic); end; procedure TGocciaVM.BeginDecorators(const AClassValue, ASuperValue: TGocciaValue); @@ -5983,28 +6001,20 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; Result := TGocciaClassValue(ClassVal).Prototype.GetProperty(AName); end; - procedure DefineDecoratedDataProperty(const AIsStatic: Boolean; + procedure DefineDecoratedMethodProperty(const AIsStatic: Boolean; const AName: string; const AKey, AValue: TGocciaValue); + var + TargetValue, KeyValue: TGocciaValue; begin - if AKey is TGocciaSymbolValue then - begin - if AIsStatic then - TGocciaClassValue(ClassVal).DefineSymbolProperty( - TGocciaSymbolValue(AKey), - TGocciaPropertyDescriptorData.Create( - AValue, [pfConfigurable, pfWritable])) - else - TGocciaClassValue(ClassVal).Prototype.DefineSymbolProperty( - TGocciaSymbolValue(AKey), - TGocciaPropertyDescriptorData.Create( - AValue, [pfConfigurable, pfWritable])); - end - else if AIsStatic then - TGocciaClassValue(ClassVal).DefineProperty(AName, - TGocciaPropertyDescriptorData.Create( - AValue, [pfConfigurable, pfWritable])) + if AIsStatic then + TargetValue := ClassVal else - TGocciaClassValue(ClassVal).Prototype.AssignProperty(AName, AValue); + TargetValue := TGocciaClassValue(ClassVal).Prototype; + if Assigned(AKey) then + KeyValue := AKey + else + KeyValue := TGocciaStringLiteralValue.Create(AName); + DefineMethodPropertyByKey(TargetValue, ValueToRegister(KeyValue), AValue); end; function GetDecoratedAccessorDescriptor(const AIsStatic: Boolean; @@ -6026,30 +6036,34 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; .GetOwnPropertyDescriptor(AName); end; - procedure DefineDecoratedAccessorProperty(const AIsStatic: Boolean; - const AName: string; const AKey, AGetter, ASetter: TGocciaValue); + procedure DefineDecoratedGetterProperty(const AIsStatic: Boolean; + const AName: string; const AKey, AGetter: TGocciaValue); + var + KeyValue: TGocciaValue; begin - if AKey is TGocciaSymbolValue then - begin - if AIsStatic then - TGocciaClassValue(ClassVal).DefineSymbolProperty( - TGocciaSymbolValue(AKey), - TGocciaPropertyDescriptorAccessor.Create( - AGetter, ASetter, [pfConfigurable])) - else - TGocciaClassValue(ClassVal).Prototype.DefineSymbolProperty( - TGocciaSymbolValue(AKey), - TGocciaPropertyDescriptorAccessor.Create( - AGetter, ASetter, [pfConfigurable])); - end - else if AIsStatic then - TGocciaClassValue(ClassVal).DefineProperty(AName, - TGocciaPropertyDescriptorAccessor.Create( - AGetter, ASetter, [pfConfigurable])) + if Assigned(AKey) then + KeyValue := AKey else - TGocciaClassValue(ClassVal).Prototype.DefineProperty(AName, - TGocciaPropertyDescriptorAccessor.Create( - AGetter, ASetter, [pfConfigurable, pfWritable])); + KeyValue := TGocciaStringLiteralValue.Create(AName); + if AIsStatic then + DefineStaticGetterPropertyByKey(ClassVal, KeyValue, AGetter) + else + DefineGetterPropertyByKey(ClassVal, KeyValue, AGetter); + end; + + procedure DefineDecoratedSetterProperty(const AIsStatic: Boolean; + const AName: string; const AKey, ASetter: TGocciaValue); + var + KeyValue: TGocciaValue; + begin + if Assigned(AKey) then + KeyValue := AKey + else + KeyValue := TGocciaStringLiteralValue.Create(AName); + if AIsStatic then + DefineStaticSetterPropertyByKey(ClassVal, KeyValue, ASetter) + else + DefineSetterPropertyByKey(ClassVal, KeyValue, ASetter); end; begin if not Assigned(FActiveDecoratorSession) then @@ -6275,7 +6289,7 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; TGocciaClassValue(ClassVal).Prototype.AssignProperty( '#' + Name, DecoratorResult) else - DefineDecoratedDataProperty( + DefineDecoratedMethodProperty( IsStatic, ElementName, ElementKey, DecoratorResult); end; 'g': @@ -6286,16 +6300,8 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; TGocciaClassValue(ClassVal).AddPrivateGetter( Name, TGocciaFunctionBase(DecoratorResult)) else - begin - ExistingDescriptor := GetDecoratedAccessorDescriptor( - IsStatic, ElementName, ElementKey); - SetterValue := nil; - if (ExistingDescriptor is TGocciaPropertyDescriptorAccessor) and - Assigned(TGocciaPropertyDescriptorAccessor(ExistingDescriptor).Setter) then - SetterValue := TGocciaPropertyDescriptorAccessor(ExistingDescriptor).Setter; - DefineDecoratedAccessorProperty( - IsStatic, ElementName, ElementKey, DecoratorResult, SetterValue); - end; + DefineDecoratedGetterProperty( + IsStatic, ElementName, ElementKey, DecoratorResult); end; 's': begin @@ -6305,16 +6311,8 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; TGocciaClassValue(ClassVal).AddPrivateSetter( Name, TGocciaFunctionBase(DecoratorResult)) else - begin - ExistingDescriptor := GetDecoratedAccessorDescriptor( - IsStatic, ElementName, ElementKey); - GetterValue := nil; - if (ExistingDescriptor is TGocciaPropertyDescriptorAccessor) and - Assigned(TGocciaPropertyDescriptorAccessor(ExistingDescriptor).Getter) then - GetterValue := TGocciaPropertyDescriptorAccessor(ExistingDescriptor).Getter; - DefineDecoratedAccessorProperty( - IsStatic, ElementName, ElementKey, GetterValue, DecoratorResult); - end; + DefineDecoratedSetterProperty( + IsStatic, ElementName, ElementKey, DecoratorResult); end; 'f': begin @@ -6333,15 +6331,18 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; NewInit := DecResultObj.GetProperty(PROP_INIT); if Assigned(NewGetter) and not (NewGetter is TGocciaUndefinedLiteralValue) then + begin GetterValue := NewGetter; + DefineDecoratedGetterProperty( + IsStatic, ElementName, ElementKey, GetterValue); + end; if Assigned(NewSetter) and not (NewSetter is TGocciaUndefinedLiteralValue) then + begin SetterValue := NewSetter; - - if (Assigned(NewGetter) and not (NewGetter is TGocciaUndefinedLiteralValue)) or - (Assigned(NewSetter) and not (NewSetter is TGocciaUndefinedLiteralValue)) then - DefineDecoratedAccessorProperty( - IsStatic, ElementName, ElementKey, GetterValue, SetterValue); + DefineDecoratedSetterProperty( + IsStatic, ElementName, ElementKey, SetterValue); + end; if Assigned(NewInit) and not (NewInit is TGocciaUndefinedLiteralValue) and NewInit.IsCallable then @@ -8310,9 +8311,6 @@ // ES2022 §15.7.14: execute static block closure with this = class begin RightValue := RegisterToValue(FRegisters[C]); TargetValue := GetRegister(A); - if (TargetValue is TGocciaClassValue) or - (TargetValue is TGocciaObjectValue) then - SetBytecodeHomeObject(RightValue, TargetValue); if (TargetValue is TGocciaObjectValue) and (FRegisters[B].Kind = grkObject) and (FRegisters[B].ObjectValue is TGocciaSymbolValue) then @@ -9848,7 +9846,12 @@ // ES2022 §15.7.14: execute static block closure with this = class end; OP_SETUP_AUTO_ACCESSOR_CONST: - SetupAutoAccessorValue(Template.GetConstantUnchecked(C).StringValue); + SetupAutoAccessorValue(Template.GetConstantUnchecked(C).StringValue, + B <> 0); + + OP_SETUP_AUTO_ACCESSOR_DYNAMIC: + SetupAutoAccessorValueByKey(RegisterToValue(FRegisters[A]), + Template.GetConstantUnchecked(C).StringValue, B <> 0); OP_BEGIN_DECORATORS: BeginDecorators(RegisterToValue(FRegisters[A]), RegisterToValue(FRegisters[A + 1])); diff --git a/source/units/Goccia.Values.ClassValue.pas b/source/units/Goccia.Values.ClassValue.pas index 1dc55b3d..b940c3a3 100644 --- a/source/units/Goccia.Values.ClassValue.pas +++ b/source/units/Goccia.Values.ClassValue.pas @@ -149,6 +149,7 @@ TGocciaClassValue = class(TGocciaObjectValue) procedure AppendMethodInitializers(const AInitializers: array of TGocciaValue); procedure AppendFieldInitializers(const AInitializers: array of TGocciaValue); procedure AddAutoAccessor(const AName, ABackingName: string; const AIsStatic: Boolean); + procedure AddAutoAccessorWithKey(const AName: string; const AKey: TGocciaValue; const ABackingName: string; const AIsStatic: Boolean); procedure RunMethodInitializers(const AInstance: TGocciaValue); procedure RunFieldInitializers(const AInstance: TGocciaValue); procedure RunDecoratorFieldInitializers(const AInstance: TGocciaValue); @@ -768,25 +769,41 @@ procedure TGocciaClassValue.AppendFieldInitializers(const AInitializers: array o // TC39 proposal-decorators: auto-accessor creates backing getter/setter procedure TGocciaClassValue.AddAutoAccessor(const AName, ABackingName: string; const AIsStatic: Boolean); +begin + AddAutoAccessorWithKey(AName, nil, ABackingName, AIsStatic); +end; + +procedure TGocciaClassValue.AddAutoAccessorWithKey(const AName: string; const AKey: TGocciaValue; const ABackingName: string; const AIsStatic: Boolean); var GetterHelper: TGocciaAutoAccessorGetter; SetterHelper: TGocciaAutoAccessorSetter; GetterFn, SetterFn: TGocciaNativeFunctionValue; Target: TGocciaObjectValue; + PropertyName: string; begin GetterHelper := TGocciaAutoAccessorGetter.Create(ABackingName); SetterHelper := TGocciaAutoAccessorSetter.Create(ABackingName); - GetterFn := TGocciaNativeFunctionValue.CreateWithoutPrototype(GetterHelper.Get, 'get ' + AName, 0); - SetterFn := TGocciaNativeFunctionValue.CreateWithoutPrototype(SetterHelper.SetValue, 'set ' + AName, 1); + if Assigned(AKey) and not (AKey is TGocciaSymbolValue) then + PropertyName := AKey.ToStringLiteral.Value + else + PropertyName := AName; + + GetterFn := TGocciaNativeFunctionValue.CreateWithoutPrototype(GetterHelper.Get, 'get ' + PropertyName, 0); + SetterFn := TGocciaNativeFunctionValue.CreateWithoutPrototype(SetterHelper.SetValue, 'set ' + PropertyName, 1); // Static auto-accessors go on the constructor; instance ones on the prototype if AIsStatic then Target := Self else Target := FClassPrototype; - Target.DefineProperty(AName, TGocciaPropertyDescriptorAccessor.Create( - GetterFn, SetterFn, [pfConfigurable, pfWritable])); + if AKey is TGocciaSymbolValue then + Target.DefineSymbolProperty(TGocciaSymbolValue(AKey), + TGocciaPropertyDescriptorAccessor.Create( + GetterFn, SetterFn, [pfConfigurable, pfWritable])) + else + Target.DefineProperty(PropertyName, TGocciaPropertyDescriptorAccessor.Create( + GetterFn, SetterFn, [pfConfigurable, pfWritable])); end; procedure TGocciaClassValue.RunMethodInitializers(const AInstance: TGocciaValue); diff --git a/tests/language/decorators/auto-accessor-decorator.js b/tests/language/decorators/auto-accessor-decorator.js index 8fcd085c..42c1aca8 100644 --- a/tests/language/decorators/auto-accessor-decorator.js +++ b/tests/language/decorators/auto-accessor-decorator.js @@ -22,4 +22,29 @@ describe("auto-accessor decorators", () => { expect(receivedContext.name).toBe("x"); expect(typeof receivedValue).toBe("object"); }); + + test("computed accessor decorator context and access use resolved symbol key", () => { + let receivedName; + let accessGet; + const key = Symbol("accessor"); + const decorate = (value, context) => { + receivedName = context.name; + accessGet = context.access.get; + return { + get() { + return value.get.call(this) + 1; + } + }; + }; + + class C { + @decorate + accessor [key] = 41; + } + + const instance = new C(); + expect(receivedName === key).toBe(true); + expect(instance[key]).toBe(42); + expect(accessGet(instance)).toBe(42); + }); }); diff --git a/tests/language/decorators/auto-accessor.js b/tests/language/decorators/auto-accessor.js index d490d01a..8397d38d 100644 --- a/tests/language/decorators/auto-accessor.js +++ b/tests/language/decorators/auto-accessor.js @@ -31,4 +31,30 @@ describe("auto-accessor", () => { const c = new C(); expect(c.x).toBe(undefined); }); + + test("computed auto-accessor uses resolved property key", () => { + const key = "x"; + + class C { + accessor [key] = 1; + } + + const c = new C(); + expect(c.x).toBe(1); + c.x = 2; + expect(c.x).toBe(2); + }); + + test("symbol computed auto-accessor uses resolved property key", () => { + const key = Symbol("x"); + + class C { + accessor [key] = 1; + } + + const c = new C(); + expect(c[key]).toBe(1); + c[key] = 2; + expect(c[key]).toBe(2); + }); }); diff --git a/tests/language/decorators/computed-field-decorator.js b/tests/language/decorators/computed-field-decorator.js index 2528f8b4..13a8066b 100644 --- a/tests/language/decorators/computed-field-decorator.js +++ b/tests/language/decorators/computed-field-decorator.js @@ -54,6 +54,20 @@ describe("computed field decorators", () => { expect(new C()[key]).toBe(42); }); + test("static symbol computed field access helper preserves class symbol key", () => { + const key = Symbol("static-field-access"); + const decorate = (value, context) => { + context.addInitializer(({ init() { context.access.set(this, 42); } }).init); + }; + + class C { + @decorate + static [key] = 1; + } + + expect(C[key]).toBe(42); + }); + test("symbol computed method decorator uses resolved key", () => { let receivedName; let accessGet; @@ -77,6 +91,51 @@ describe("computed field decorators", () => { expect(accessGet(instance)()).toBe(42); }); + test("decorated computed method context uses property-key coercion", () => { + let receivedName; + let toStringCalls = 0; + const key = { + toString() { + toStringCalls++; + return "coerced"; + } + }; + const decorate = (value, context) => { + receivedName = context.name; + return value; + }; + + class C { + @decorate + [key]() { + return 42; + } + } + + expect(receivedName).toBe("coerced"); + expect(toStringCalls).toBe(1); + expect(new C().coerced()).toBe(42); + }); + + test("static symbol computed method access helper preserves class symbol key", () => { + let accessGet; + const key = Symbol("static-method-access"); + const decorate = (value, context) => { + accessGet = context.access.get; + return () => 42; + }; + + class C { + @decorate + static [key]() { + return 1; + } + } + + expect(C[key]()).toBe(42); + expect(accessGet(C)()).toBe(42); + }); + test("symbol computed getter decorator uses resolved key", () => { let receivedName; let accessGet; From 185d265aea33e9f8a5290b6d4a54ec7fc2b51534 Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Wed, 20 May 2026 23:20:13 +0100 Subject: [PATCH 6/8] fix: use completed cache for computed class keys --- source/units/Goccia.Evaluator.pas | 7 ++++--- source/units/Goccia.Generator.Continuation.pas | 7 +++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/source/units/Goccia.Evaluator.pas b/source/units/Goccia.Evaluator.pas index 794d2f8f..c291431b 100644 --- a/source/units/Goccia.Evaluator.pas +++ b/source/units/Goccia.Evaluator.pas @@ -4461,13 +4461,14 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const // replay keys already resolved before a later computed name suspended. HasReplayedComputedKey := False; if Assigned(Continuation) then - HasReplayedComputedKey := Continuation.TakeExpressionValue( + HasReplayedComputedKey := Continuation.TakeCompletedExpressionValue( Elem.ComputedKeyExpression, ComputedKey); if not HasReplayedComputedKey then ComputedKey := ToPropertyKey(EvaluateExpression( Elem.ComputedKeyExpression, AContext)); if Assigned(Continuation) then - Continuation.SaveExpressionValue(Elem.ComputedKeyExpression, ComputedKey); + Continuation.SaveCompletedExpressionValue( + Elem.ComputedKeyExpression, ComputedKey); ResolvedComputedElementKeys[I] := ComputedKey; @@ -5107,7 +5108,7 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const for I := 0 to High(AClassDef.FElements) do if AClassDef.FElements[I].IsComputed and (AClassDef.FElements[I].Kind in [cekMethod, cekGetter, cekSetter, cekField, cekAccessor]) then - Continuation.ClearExpressionValue( + Continuation.ClearCompletedExpressionValue( AClassDef.FElements[I].ComputedKeyExpression); Result := ClassValue; diff --git a/source/units/Goccia.Generator.Continuation.pas b/source/units/Goccia.Generator.Continuation.pas index 7f489507..dbacbb15 100644 --- a/source/units/Goccia.Generator.Continuation.pas +++ b/source/units/Goccia.Generator.Continuation.pas @@ -121,6 +121,7 @@ TGocciaGeneratorContinuation = class const AContext: TGocciaEvaluationContext): TGocciaValue; procedure SaveCompletedExpressionValue(const AExpression: TObject; const AValue: TGocciaValue); function TakeCompletedExpressionValue(const AExpression: TObject; out AValue: TGocciaValue): Boolean; + procedure ClearCompletedExpressionValue(const AExpression: TObject); procedure SaveExpressionValue(const AExpression: TObject; const AValue: TGocciaValue); function TakeExpressionValue(const AExpression: TObject; out AValue: TGocciaValue): Boolean; procedure ClearExpressionValue(const AExpression: TObject); @@ -966,6 +967,12 @@ function TGocciaGeneratorContinuation.TakeCompletedExpressionValue( FCompletedExpressionValues.Remove(AExpression); end; +procedure TGocciaGeneratorContinuation.ClearCompletedExpressionValue( + const AExpression: TObject); +begin + FCompletedExpressionValues.Remove(AExpression); +end; + procedure TGocciaGeneratorContinuation.SaveExpressionValue( const AExpression: TObject; const AValue: TGocciaValue); begin From 4e7c94fe09272d309d9071cfaa04348195c885e6 Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Wed, 20 May 2026 23:47:08 +0100 Subject: [PATCH 7/8] fix: address decorator review findings --- source/units/Goccia.Evaluator.Decorators.pas | 2 + source/units/Goccia.Evaluator.pas | 1 + source/units/Goccia.VM.pas | 86 +++++++++++++-- source/units/Goccia.Values.ClassValue.pas | 104 +++++++++++++++--- .../decorators/computed-field-decorator.js | 88 +++++++++++++++ 5 files changed, 258 insertions(+), 23 deletions(-) diff --git a/source/units/Goccia.Evaluator.Decorators.pas b/source/units/Goccia.Evaluator.Decorators.pas index 716c0450..88161fed 100644 --- a/source/units/Goccia.Evaluator.Decorators.pas +++ b/source/units/Goccia.Evaluator.Decorators.pas @@ -161,6 +161,8 @@ function TGocciaAccessSetter.SetValue(const AArgs: TGocciaArgumentsCollection; c TGocciaObjectValue(Target).AssignSymbolProperty( TGocciaSymbolValue(FPropertyKey), NewValue); end + else if Target is TGocciaClassValue then + TGocciaClassValue(Target).SetProperty(FPropertyName, NewValue) else TGocciaObjectValue(Target).AssignProperty(FPropertyName, NewValue); end; diff --git a/source/units/Goccia.Evaluator.pas b/source/units/Goccia.Evaluator.pas index c291431b..b8aba9e6 100644 --- a/source/units/Goccia.Evaluator.pas +++ b/source/units/Goccia.Evaluator.pas @@ -5070,6 +5070,7 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const ClassValue.SetMethodInitializers(InitializerResults); InitializerResults := FieldCollector.GetInitializers; ClassValue.SetFieldInitializers(InitializerResults); + ClassValue.RunDecoratorStaticFieldInitializers; // Run class-level initializers after static fields InitializerResults := ClassCollector.GetInitializers; diff --git a/source/units/Goccia.VM.pas b/source/units/Goccia.VM.pas index d8ef8a27..c8b09a0b 100644 --- a/source/units/Goccia.VM.pas +++ b/source/units/Goccia.VM.pas @@ -6004,17 +6004,28 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; procedure DefineDecoratedMethodProperty(const AIsStatic: Boolean; const AName: string; const AKey, AValue: TGocciaValue); var - TargetValue, KeyValue: TGocciaValue; + TargetObject: TGocciaObjectValue; + KeyValue: TGocciaValue; begin if AIsStatic then - TargetValue := ClassVal + TargetObject := TGocciaObjectValue(ClassVal) else - TargetValue := TGocciaClassValue(ClassVal).Prototype; + TargetObject := TGocciaClassValue(ClassVal).Prototype; if Assigned(AKey) then KeyValue := AKey else KeyValue := TGocciaStringLiteralValue.Create(AName); - DefineMethodPropertyByKey(TargetValue, ValueToRegister(KeyValue), AValue); + SetBytecodeHomeObject(AValue, TargetObject); + if KeyValue is TGocciaSymbolValue then + TargetObject.DefineSymbolProperty( + TGocciaSymbolValue(KeyValue), + TGocciaPropertyDescriptorData.Create( + AValue, [pfConfigurable, pfWritable])) + else + TargetObject.DefineProperty( + KeyValue.ToStringLiteral.Value, + TGocciaPropertyDescriptorData.Create( + AValue, [pfConfigurable, pfWritable])); end; function GetDecoratedAccessorDescriptor(const AIsStatic: Boolean; @@ -6039,31 +6050,85 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; procedure DefineDecoratedGetterProperty(const AIsStatic: Boolean; const AName: string; const AKey, AGetter: TGocciaValue); var + TargetObject: TGocciaObjectValue; KeyValue: TGocciaValue; + ExistingDescriptor: TGocciaPropertyDescriptor; + ExistingSetter: TGocciaValue; begin + if AIsStatic then + TargetObject := TGocciaObjectValue(ClassVal) + else + TargetObject := TGocciaClassValue(ClassVal).Prototype; if Assigned(AKey) then KeyValue := AKey else KeyValue := TGocciaStringLiteralValue.Create(AName); - if AIsStatic then - DefineStaticGetterPropertyByKey(ClassVal, KeyValue, AGetter) + + SetBytecodeHomeObject(AGetter, TargetObject); + if KeyValue is TGocciaSymbolValue then + ExistingDescriptor := TargetObject.GetOwnSymbolPropertyDescriptor( + TGocciaSymbolValue(KeyValue)) + else + ExistingDescriptor := TargetObject.GetOwnPropertyDescriptor( + KeyValue.ToStringLiteral.Value); + + ExistingSetter := nil; + if (ExistingDescriptor is TGocciaPropertyDescriptorAccessor) and + Assigned(TGocciaPropertyDescriptorAccessor(ExistingDescriptor).Setter) then + ExistingSetter := TGocciaPropertyDescriptorAccessor(ExistingDescriptor).Setter; + + if KeyValue is TGocciaSymbolValue then + TargetObject.DefineSymbolProperty( + TGocciaSymbolValue(KeyValue), + TGocciaPropertyDescriptorAccessor.Create( + AGetter, ExistingSetter, [pfConfigurable])) else - DefineGetterPropertyByKey(ClassVal, KeyValue, AGetter); + TargetObject.DefineProperty( + KeyValue.ToStringLiteral.Value, + TGocciaPropertyDescriptorAccessor.Create( + AGetter, ExistingSetter, [pfConfigurable])); end; procedure DefineDecoratedSetterProperty(const AIsStatic: Boolean; const AName: string; const AKey, ASetter: TGocciaValue); var + TargetObject: TGocciaObjectValue; KeyValue: TGocciaValue; + ExistingDescriptor: TGocciaPropertyDescriptor; + ExistingGetter: TGocciaValue; begin + if AIsStatic then + TargetObject := TGocciaObjectValue(ClassVal) + else + TargetObject := TGocciaClassValue(ClassVal).Prototype; if Assigned(AKey) then KeyValue := AKey else KeyValue := TGocciaStringLiteralValue.Create(AName); - if AIsStatic then - DefineStaticSetterPropertyByKey(ClassVal, KeyValue, ASetter) + + SetBytecodeHomeObject(ASetter, TargetObject); + if KeyValue is TGocciaSymbolValue then + ExistingDescriptor := TargetObject.GetOwnSymbolPropertyDescriptor( + TGocciaSymbolValue(KeyValue)) + else + ExistingDescriptor := TargetObject.GetOwnPropertyDescriptor( + KeyValue.ToStringLiteral.Value); + + ExistingGetter := nil; + if (ExistingDescriptor is TGocciaPropertyDescriptorAccessor) and + Assigned(TGocciaPropertyDescriptorAccessor(ExistingDescriptor).Getter) then + ExistingGetter := TGocciaPropertyDescriptorAccessor(ExistingDescriptor).Getter; + + if KeyValue is TGocciaSymbolValue then + TargetObject.DefineSymbolProperty( + TGocciaSymbolValue(KeyValue), + TGocciaPropertyDescriptorAccessor.Create( + ExistingGetter, ASetter, [pfConfigurable])) else - DefineSetterPropertyByKey(ClassVal, KeyValue, ASetter); + TargetObject.DefineProperty( + KeyValue.ToStringLiteral.Value, + TGocciaPropertyDescriptorAccessor.Create( + ExistingGetter, ASetter, [pfConfigurable])); end; begin if not Assigned(FActiveDecoratorSession) then @@ -6382,6 +6447,7 @@ function TGocciaVM.FinishDecorators(const ACurrentValue: TGocciaValue): TGocciaV ClassVal.AppendMethodInitializers(InitializerResults); InitializerResults := Session.FieldCollector.GetInitializers; ClassVal.AppendFieldInitializers(InitializerResults); + ClassVal.RunDecoratorStaticFieldInitializers; InitializerResults := Session.ClassCollector.GetInitializers; for I := 0 to High(InitializerResults) do diff --git a/source/units/Goccia.Values.ClassValue.pas b/source/units/Goccia.Values.ClassValue.pas index b940c3a3..8593dc59 100644 --- a/source/units/Goccia.Values.ClassValue.pas +++ b/source/units/Goccia.Values.ClassValue.pas @@ -31,6 +31,14 @@ TGocciaClassFieldOrderEntry = record Initializer: TGocciaExpression; end; + TGocciaDecoratorFieldInitializerEntry = record + Name: string; + ComputedKey: TGocciaValue; + Initializer: TGocciaValue; + IsPrivate: Boolean; + IsStatic: Boolean; + end; + TGocciaClassValue = class(TGocciaObjectValue) private FName: string; @@ -52,13 +60,8 @@ TGocciaClassValue = class(TGocciaObjectValue) FPrivateBrandToken: string; FMethodInitializers: array of TGocciaValue; FFieldInitializers: array of TGocciaValue; - FDecoratorFieldInitializers: array of record - Name: string; - ComputedKey: TGocciaValue; - Initializer: TGocciaValue; - IsPrivate: Boolean; - IsStatic: Boolean; - end; + FDecoratorFieldInitializers: array of TGocciaDecoratorFieldInitializerEntry; + FStaticDecoratorFieldInitializers: array of TGocciaDecoratorFieldInitializerEntry; function GetPropertyGetter(const AName: string): TGocciaFunctionBase; inline; function GetPropertySetter(const AName: string): TGocciaFunctionBase; inline; function GetStaticPropertyGetter(const AName: string): TGocciaFunctionBase; inline; @@ -153,6 +156,7 @@ TGocciaClassValue = class(TGocciaObjectValue) procedure RunMethodInitializers(const AInstance: TGocciaValue); procedure RunFieldInitializers(const AInstance: TGocciaValue); procedure RunDecoratorFieldInitializers(const AInstance: TGocciaValue); + procedure RunDecoratorStaticFieldInitializers; end; TGocciaArrayClassValue = class(TGocciaClassValue) @@ -447,6 +451,13 @@ procedure TGocciaClassValue.MarkReferences; if Assigned(FDecoratorFieldInitializers[I].ComputedKey) then FDecoratorFieldInitializers[I].ComputedKey.MarkReferences; end; + for I := 0 to High(FStaticDecoratorFieldInitializers) do + begin + if Assigned(FStaticDecoratorFieldInitializers[I].Initializer) then + FStaticDecoratorFieldInitializers[I].Initializer.MarkReferences; + if Assigned(FStaticDecoratorFieldInitializers[I].ComputedKey) then + FStaticDecoratorFieldInitializers[I].ComputedKey.MarkReferences; + end; for I := 0 to High(FFieldOrder) do if Assigned(FFieldOrder[I].ComputedKey) then FFieldOrder[I].ComputedKey.MarkReferences; @@ -720,13 +731,30 @@ procedure TGocciaClassValue.AddFieldInitializer(const AName: string; const AInit end; procedure TGocciaClassValue.AddFieldInitializerWithKey(const AName: string; const AComputedKey: TGocciaValue; const AInitializer: TGocciaValue; const AIsPrivate, AIsStatic: Boolean); +var + EntryIndex: Integer; begin - SetLength(FDecoratorFieldInitializers, Length(FDecoratorFieldInitializers) + 1); - FDecoratorFieldInitializers[High(FDecoratorFieldInitializers)].Name := AName; - FDecoratorFieldInitializers[High(FDecoratorFieldInitializers)].ComputedKey := AComputedKey; - FDecoratorFieldInitializers[High(FDecoratorFieldInitializers)].Initializer := AInitializer; - FDecoratorFieldInitializers[High(FDecoratorFieldInitializers)].IsPrivate := AIsPrivate; - FDecoratorFieldInitializers[High(FDecoratorFieldInitializers)].IsStatic := AIsStatic; + if AIsStatic then + begin + SetLength(FStaticDecoratorFieldInitializers, + Length(FStaticDecoratorFieldInitializers) + 1); + EntryIndex := High(FStaticDecoratorFieldInitializers); + FStaticDecoratorFieldInitializers[EntryIndex].Name := AName; + FStaticDecoratorFieldInitializers[EntryIndex].ComputedKey := AComputedKey; + FStaticDecoratorFieldInitializers[EntryIndex].Initializer := AInitializer; + FStaticDecoratorFieldInitializers[EntryIndex].IsPrivate := AIsPrivate; + FStaticDecoratorFieldInitializers[EntryIndex].IsStatic := AIsStatic; + Exit; + end; + + SetLength(FDecoratorFieldInitializers, + Length(FDecoratorFieldInitializers) + 1); + EntryIndex := High(FDecoratorFieldInitializers); + FDecoratorFieldInitializers[EntryIndex].Name := AName; + FDecoratorFieldInitializers[EntryIndex].ComputedKey := AComputedKey; + FDecoratorFieldInitializers[EntryIndex].Initializer := AInitializer; + FDecoratorFieldInitializers[EntryIndex].IsPrivate := AIsPrivate; + FDecoratorFieldInitializers[EntryIndex].IsStatic := AIsStatic; end; procedure TGocciaClassValue.SetMethodInitializers(const AInitializers: array of TGocciaValue); @@ -886,6 +914,56 @@ procedure TGocciaClassValue.RunDecoratorFieldInitializers(const AInstance: TGocc end; end; +procedure TGocciaClassValue.RunDecoratorStaticFieldInitializers; +var + Idx: Integer; + Args: TGocciaArgumentsCollection; + InitResult, OriginalValue: TGocciaValue; + PropertyKey: TGocciaValue; + PropertyName: string; +begin + for Idx := 0 to High(FStaticDecoratorFieldInitializers) do + begin + PropertyKey := FStaticDecoratorFieldInitializers[Idx].ComputedKey; + if FStaticDecoratorFieldInitializers[Idx].IsPrivate then + begin + if not FPrivateStaticProperties.TryGetValue( + FStaticDecoratorFieldInitializers[Idx].Name, OriginalValue) then + OriginalValue := TGocciaUndefinedLiteralValue.UndefinedValue; + end + else if PropertyKey is TGocciaSymbolValue then + OriginalValue := GetSymbolProperty(TGocciaSymbolValue(PropertyKey)) + else + begin + if Assigned(PropertyKey) then + PropertyName := PropertyKey.ToStringLiteral.Value + else + PropertyName := FStaticDecoratorFieldInitializers[Idx].Name; + OriginalValue := GetProperty(PropertyName); + end; + if not Assigned(OriginalValue) then + OriginalValue := TGocciaUndefinedLiteralValue.UndefinedValue; + + Args := TGocciaArgumentsCollection.Create([OriginalValue]); + try + InitResult := TGocciaFunctionBase( + FStaticDecoratorFieldInitializers[Idx].Initializer).Call(Args, Self); + if Assigned(InitResult) and not (InitResult is TGocciaUndefinedLiteralValue) then + begin + if FStaticDecoratorFieldInitializers[Idx].IsPrivate then + AddPrivateStaticProperty( + FStaticDecoratorFieldInitializers[Idx].Name, InitResult) + else if PropertyKey is TGocciaSymbolValue then + AssignSymbolProperty(TGocciaSymbolValue(PropertyKey), InitResult) + else + SetProperty(PropertyName, InitResult); + end; + finally + Args.Free; + end; + end; +end; + procedure TGocciaClassValue.AddInstanceProperty(const AName: string; const AExpression: TGocciaExpression); begin FInstancePropertyDefs.Add(AName, AExpression); diff --git a/tests/language/decorators/computed-field-decorator.js b/tests/language/decorators/computed-field-decorator.js index 13a8066b..354ff58f 100644 --- a/tests/language/decorators/computed-field-decorator.js +++ b/tests/language/decorators/computed-field-decorator.js @@ -202,4 +202,92 @@ describe("computed field decorators", () => { expect(receivedName).toBe("staticName"); expect(C.staticName).toBe(1); }); + + test("static string computed field decorator initializer updates resolved key", () => { + const key = "staticInit"; + const decorate = (value, context) => { + return (initialValue) => initialValue + 1; + }; + + class C { + @decorate + static [key] = 41; + } + + expect(C.staticInit).toBe(42); + }); + + test("static symbol computed field decorator initializer updates resolved key", () => { + const key = Symbol("static-init"); + const decorate = (value, context) => { + return (initialValue) => initialValue + 1; + }; + + class C { + @decorate + static [key] = 41; + } + + expect(C[key]).toBe(42); + }); + + test("static string computed field access helper writes through class properties", () => { + const key = "name"; + const decorate = (value, context) => { + context.addInitializer(({ init() { + context.access.set(this, "DecoratedName"); + } }).init); + }; + + class Original { + @decorate + static [key] = "InitialName"; + } + + expect(Original.name).toBe("DecoratedName"); + }); + + test("decorated computed methods remain non-enumerable", () => { + const key = "method"; + const decorate = (value, context) => value; + + class C { + @decorate + [key]() { + return 1; + } + } + + const descriptor = Object.getOwnPropertyDescriptor(C.prototype, key); + expect(descriptor.enumerable).toBe(false); + }); + + test("decorated computed getters remain non-enumerable", () => { + const key = "value"; + const decorate = (value, context) => value; + + class C { + @decorate + get [key]() { + return 1; + } + } + + const descriptor = Object.getOwnPropertyDescriptor(C.prototype, key); + expect(descriptor.enumerable).toBe(false); + }); + + test("decorated computed setters remain non-enumerable", () => { + const key = "value"; + const decorate = (value, context) => value; + + class C { + @decorate + set [key](next) { + } + } + + const descriptor = Object.getOwnPropertyDescriptor(C.prototype, key); + expect(descriptor.enumerable).toBe(false); + }); }); From 6db50cbec8bc5ca7348eab9c30eb733bf24e09fe Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Thu, 21 May 2026 00:12:39 +0100 Subject: [PATCH 8/8] fix: run static decorator initializers before class replacement --- source/units/Goccia.Evaluator.pas | 5 +- source/units/Goccia.VM.pas | 64 +++++++++++++++---- .../decorators/auto-accessor-decorator.js | 26 ++++++++ .../decorators/basic-class-decorator.js | 22 +++++++ 4 files changed, 105 insertions(+), 12 deletions(-) diff --git a/source/units/Goccia.Evaluator.pas b/source/units/Goccia.Evaluator.pas index b8aba9e6..37d70ac4 100644 --- a/source/units/Goccia.Evaluator.pas +++ b/source/units/Goccia.Evaluator.pas @@ -4202,6 +4202,7 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const NewGetter, NewSetter, NewInit: TGocciaValue; ExistingGetterValue, ExistingSetterValue: TGocciaValue; MethodCollector, FieldCollector, StaticFieldCollector, ClassCollector: TGocciaInitializerCollector; + OriginalClassValue: TGocciaClassValue; AccessGetterHelper: TGocciaAccessGetter; AccessSetterHelper: TGocciaAccessSetter; InitializerResults: TArray; @@ -5028,6 +5029,9 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const end; end; + OriginalClassValue := ClassValue; + OriginalClassValue.RunDecoratorStaticFieldInitializers; + // Phase 3: Call class decorators (bottom-up) for I := High(EvaluatedClassDecorators) downto 0 do begin @@ -5070,7 +5074,6 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const ClassValue.SetMethodInitializers(InitializerResults); InitializerResults := FieldCollector.GetInitializers; ClassValue.SetFieldInitializers(InitializerResults); - ClassValue.RunDecoratorStaticFieldInitializers; // Run class-level initializers after static fields InitializerResults := ClassCollector.GetInitializers; diff --git a/source/units/Goccia.VM.pas b/source/units/Goccia.VM.pas index c8b09a0b..93c48c02 100644 --- a/source/units/Goccia.VM.pas +++ b/source/units/Goccia.VM.pas @@ -774,6 +774,8 @@ TGocciaVMDecoratorSession = class StaticFieldCollector: TGocciaInitializerCollector; ClassCollector: TGocciaInitializerCollector; ClassValue: TGocciaValue; + OriginalClassValue: TGocciaValue; + StaticDecoratorInitializersRun: Boolean; constructor Create(const AMetadataObject: TGocciaObjectValue); destructor Destroy; override; procedure MarkReferences; @@ -1218,6 +1220,8 @@ constructor TGocciaVMDecoratorSession.Create( StaticFieldCollector := TGocciaInitializerCollector.Create; ClassCollector := TGocciaInitializerCollector.Create; ClassValue := nil; + OriginalClassValue := nil; + StaticDecoratorInitializersRun := False; end; destructor TGocciaVMDecoratorSession.Destroy; @@ -1249,12 +1253,26 @@ procedure TGocciaVMDecoratorSession.MarkReferences; MetadataObject.MarkReferences; if Assigned(ClassValue) then ClassValue.MarkReferences; + if Assigned(OriginalClassValue) and (OriginalClassValue <> ClassValue) then + OriginalClassValue.MarkReferences; MarkCollector(MethodCollector); MarkCollector(FieldCollector); MarkCollector(StaticFieldCollector); MarkCollector(ClassCollector); end; +procedure RunStaticDecoratorInitializersForSession( + const ASession: TGocciaVMDecoratorSession); +begin + if not Assigned(ASession) or ASession.StaticDecoratorInitializersRun then + Exit; + + if ASession.OriginalClassValue is TGocciaClassValue then + TGocciaClassValue(ASession.OriginalClassValue) + .RunDecoratorStaticFieldInitializers; + ASession.StaticDecoratorInitializersRun := True; +end; + threadvar GActiveBytecodeGenerator: TGocciaBytecodeGeneratorObjectValue; @@ -5915,6 +5933,8 @@ procedure TGocciaVM.BeginDecorators(const AClassValue, ASuperValue: TGocciaValue FActiveDecoratorSession := TGocciaVMDecoratorSession.Create(Meta); TGocciaVMDecoratorSession(FActiveDecoratorSession).ClassValue := ClassVal; + TGocciaVMDecoratorSession(FActiveDecoratorSession).OriginalClassValue := + ClassVal; end; procedure TGocciaVM.ApplyClassDecorator(const ADecoratorFn: TGocciaValue); @@ -5933,6 +5953,7 @@ procedure TGocciaVM.ApplyClassDecorator(const ADecoratorFn: TGocciaValue); Exit; if not ADecoratorFn.IsCallable then ThrowTypeError(SErrorDecoratorMustBeFunction, SSuggestDecoratorFunction); + RunStaticDecoratorInitializersForSession(Session); ContextObject := TGocciaObjectValue.Create; ContextObject.AssignProperty(PROP_KIND, @@ -6008,14 +6029,17 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; KeyValue: TGocciaValue; begin if AIsStatic then - TargetObject := TGocciaObjectValue(ClassVal) + TargetObject := TGocciaClassValue(ClassVal) else TargetObject := TGocciaClassValue(ClassVal).Prototype; if Assigned(AKey) then KeyValue := AKey else KeyValue := TGocciaStringLiteralValue.Create(AName); - SetBytecodeHomeObject(AValue, TargetObject); + if AIsStatic then + SetBytecodeHomeObject(AValue, TGocciaClassValue(ClassVal)) + else + SetBytecodeHomeObject(AValue, TargetObject); if KeyValue is TGocciaSymbolValue then TargetObject.DefineSymbolProperty( TGocciaSymbolValue(KeyValue), @@ -6056,7 +6080,7 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; ExistingSetter: TGocciaValue; begin if AIsStatic then - TargetObject := TGocciaObjectValue(ClassVal) + TargetObject := TGocciaClassValue(ClassVal) else TargetObject := TGocciaClassValue(ClassVal).Prototype; if Assigned(AKey) then @@ -6064,10 +6088,19 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; else KeyValue := TGocciaStringLiteralValue.Create(AName); - SetBytecodeHomeObject(AGetter, TargetObject); + if AIsStatic then + SetBytecodeHomeObject(AGetter, TGocciaClassValue(ClassVal)) + else + SetBytecodeHomeObject(AGetter, TargetObject); if KeyValue is TGocciaSymbolValue then - ExistingDescriptor := TargetObject.GetOwnSymbolPropertyDescriptor( - TGocciaSymbolValue(KeyValue)) + begin + if AIsStatic then + ExistingDescriptor := TGocciaClassValue(ClassVal) + .GetOwnStaticSymbolDescriptor(TGocciaSymbolValue(KeyValue)) + else + ExistingDescriptor := TargetObject.GetOwnSymbolPropertyDescriptor( + TGocciaSymbolValue(KeyValue)); + end else ExistingDescriptor := TargetObject.GetOwnPropertyDescriptor( KeyValue.ToStringLiteral.Value); @@ -6098,7 +6131,7 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; ExistingGetter: TGocciaValue; begin if AIsStatic then - TargetObject := TGocciaObjectValue(ClassVal) + TargetObject := TGocciaClassValue(ClassVal) else TargetObject := TGocciaClassValue(ClassVal).Prototype; if Assigned(AKey) then @@ -6106,10 +6139,19 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; else KeyValue := TGocciaStringLiteralValue.Create(AName); - SetBytecodeHomeObject(ASetter, TargetObject); + if AIsStatic then + SetBytecodeHomeObject(ASetter, TGocciaClassValue(ClassVal)) + else + SetBytecodeHomeObject(ASetter, TargetObject); if KeyValue is TGocciaSymbolValue then - ExistingDescriptor := TargetObject.GetOwnSymbolPropertyDescriptor( - TGocciaSymbolValue(KeyValue)) + begin + if AIsStatic then + ExistingDescriptor := TGocciaClassValue(ClassVal) + .GetOwnStaticSymbolDescriptor(TGocciaSymbolValue(KeyValue)) + else + ExistingDescriptor := TargetObject.GetOwnSymbolPropertyDescriptor( + TGocciaSymbolValue(KeyValue)); + end else ExistingDescriptor := TargetObject.GetOwnPropertyDescriptor( KeyValue.ToStringLiteral.Value); @@ -6437,6 +6479,7 @@ function TGocciaVM.FinishDecorators(const ACurrentValue: TGocciaValue): TGocciaV Exit(ACurrentValue); end; + RunStaticDecoratorInitializersForSession(Session); ClassVal := TGocciaClassValue(Session.ClassValue); ClassVal.DefineSymbolProperty( TGocciaSymbolValue.WellKnownMetadata, @@ -6447,7 +6490,6 @@ function TGocciaVM.FinishDecorators(const ACurrentValue: TGocciaValue): TGocciaV ClassVal.AppendMethodInitializers(InitializerResults); InitializerResults := Session.FieldCollector.GetInitializers; ClassVal.AppendFieldInitializers(InitializerResults); - ClassVal.RunDecoratorStaticFieldInitializers; InitializerResults := Session.ClassCollector.GetInitializers; for I := 0 to High(InitializerResults) do diff --git a/tests/language/decorators/auto-accessor-decorator.js b/tests/language/decorators/auto-accessor-decorator.js index 42c1aca8..bb14f8cd 100644 --- a/tests/language/decorators/auto-accessor-decorator.js +++ b/tests/language/decorators/auto-accessor-decorator.js @@ -47,4 +47,30 @@ describe("auto-accessor decorators", () => { expect(instance[key]).toBe(42); expect(accessGet(instance)).toBe(42); }); + + test("static accessor decorator initializer runs before class replacement", () => { + let observedOriginalValue; + const accessor = (value, context) => { + return { + init() { + return 42; + } + }; + }; + const replace = (cls, context) => { + observedOriginalValue = cls.value; + return class Replacement { + static value = 100; + }; + }; + + @replace + class C { + @accessor + static accessor value = 41; + } + + expect(observedOriginalValue).toBe(42); + expect(C.value).toBe(100); + }); }); diff --git a/tests/language/decorators/basic-class-decorator.js b/tests/language/decorators/basic-class-decorator.js index 2ae06701..c0470278 100644 --- a/tests/language/decorators/basic-class-decorator.js +++ b/tests/language/decorators/basic-class-decorator.js @@ -78,4 +78,26 @@ describe("class decorators", () => { expect(c.x).toBe(1); expect(c.wrapped).toBe(true); }); + + test("static field decorator initializer runs before class replacement", () => { + let observedOriginalValue; + const field = (value, context) => { + return (initialValue) => initialValue + 1; + }; + const replace = (cls, context) => { + observedOriginalValue = cls.value; + return class Replacement { + static value = 100; + }; + }; + + @replace + class C { + @field + static value = 41; + } + + expect(observedOriginalValue).toBe(42); + expect(C.value).toBe(100); + }); });