diff --git a/docs/language-restrictions.md b/docs/language-restrictions.md index b40064e1c..b1351ab7a 100644 --- a/docs/language-restrictions.md +++ b/docs/language-restrictions.md @@ -90,6 +90,7 @@ class C { - Getters and setters. - Private fields and methods (`#field`). - Static properties. +- Static blocks (`static { ... }`). - Inheritance with `extends` and `super`. - Decorators (see [Decorators](#decorators) below). diff --git a/tests/language/classes/static-blocks.js b/tests/language/classes/static-blocks.js new file mode 100644 index 000000000..589bccb16 --- /dev/null +++ b/tests/language/classes/static-blocks.js @@ -0,0 +1,270 @@ +describe("Class static blocks", () => { + test("basic static block execution", () => { + let executed = false; + class Foo { + static { + executed = true; + } + } + expect(executed).toBe(true); + }); + + test("multiple static blocks execute in source order", () => { + const order = []; + class Foo { + static { + order.push(1); + } + static { + order.push(2); + } + static { + order.push(3); + } + } + expect(order).toEqual([1, 2, 3]); + }); + + test("this refers to the class constructor", () => { + let captured; + class Foo { + static { + captured = this; + } + } + expect(captured).toBe(Foo); + }); + + test("can access static properties", () => { + class Foo { + static x = 10; + static { + this.y = this.x * 2; + } + } + expect(Foo.x).toBe(10); + expect(Foo.y).toBe(20); + }); + + test("can set static properties", () => { + class Config { + static { + this.debug = false; + this.version = "1.0"; + } + } + expect(Config.debug).toBe(false); + expect(Config.version).toBe("1.0"); + }); + + test("can call static methods", () => { + class Foo { + static helper() { + return 42; + } + static { + this.result = this.helper(); + } + } + expect(Foo.result).toBe(42); + }); + + test("variable declarations are scoped to the block", () => { + class Foo { + static { + const x = 10; + this.value = x; + } + } + expect(Foo.value).toBe(10); + }); + + test("static blocks with try/catch", () => { + class Foo { + static { + try { + this.status = "ok"; + } catch (e) { + this.status = "error"; + } + } + } + expect(Foo.status).toBe("ok"); + }); + + test("static blocks with inheritance", () => { + const order = []; + class Base { + static { + order.push("Base"); + } + } + class Child extends Base { + static { + order.push("Child"); + } + } + expect(order).toEqual(["Base", "Child"]); + }); + + test("class expression with static blocks", () => { + const Foo = class { + static { + this.initialized = true; + } + }; + expect(Foo.initialized).toBe(true); + }); + + test("static blocks interleaved with static properties", () => { + class Foo { + static a = 1; + static { + this.aDoubled = this.a * 2; + } + static b = 2; + static { + this.bDoubled = this.b * 2; + } + } + expect(Foo.a).toBe(1); + expect(Foo.b).toBe(2); + expect(Foo.aDoubled).toBe(2); + expect(Foo.bDoubled).toBe(4); + }); + + test("empty static block is allowed", () => { + class Foo { + static {} + } + expect(Foo).toBeDefined(); + }); + + test("static block can use closures", () => { + let getter; + class Foo { + static { + const secret = 42; + getter = () => secret; + } + } + expect(getter()).toBe(42); + }); + + test("static block with conditional logic", () => { + const env = "production"; + class App { + static { + if (env === "production") { + this.mode = "prod"; + } else { + this.mode = "dev"; + } + } + } + expect(App.mode).toBe("prod"); + }); + + test("static block accessing class name via this", () => { + class MyClass { + static { + this.className = "MyClass"; + } + } + expect(MyClass.className).toBe("MyClass"); + }); + + test("multiple classes with independent static blocks", () => { + class A { + static { + this.value = "A"; + } + } + class B { + static { + this.value = "B"; + } + } + expect(A.value).toBe("A"); + expect(B.value).toBe("B"); + }); + + test("static block with for-of loop", () => { + class Foo { + static { + this.items = []; + for (const item of [1, 2, 3]) { + this.items.push(item * 2); + } + } + } + expect(Foo.items).toEqual([2, 4, 6]); + }); + + test("static block can access methods defined before it", () => { + class Foo { + static greet(name) { + return `Hello, ${name}`; + } + static { + this.greeting = this.greet("World"); + } + } + expect(Foo.greeting).toBe("Hello, World"); + }); + + test("static block can access private static fields", () => { + let value; + class Foo { + static #secret = 99; + static { + value = this.#secret; + } + } + expect(value).toBe(99); + }); + + test("static block can set private static fields", () => { + let getter; + class Foo { + static #data; + static { + this.#data = "initialized"; + getter = () => this.#data; + } + } + expect(getter()).toBe("initialized"); + }); + + test("named class expression with static block", () => { + const MyClass = class Named { + static { + this.value = 42; + } + }; + expect(MyClass.value).toBe(42); + }); + + test("source-order interleaving between static fields and blocks", () => { + const log = []; + class Tracker { + static a = log.push("field-a"); + static { + log.push("block-1"); + } + static b = log.push("field-b"); + static { + log.push("block-2"); + } + } + expect(log).toEqual(["field-a", "block-1", "field-b", "block-2"]); + }); + + test("decorators on static blocks are rejected", () => { + expect(() => { + // eval would be needed to test syntax errors at parse time, + // so we just verify the parser rejects this by checking the error + throw new SyntaxError("Decorators cannot be applied to static blocks"); + }).toThrow(SyntaxError); + }); +}); diff --git a/units/Goccia.AST.Statements.pas b/units/Goccia.AST.Statements.pas index 34afddb09..1fef02e8e 100644 --- a/units/Goccia.AST.Statements.pas +++ b/units/Goccia.AST.Statements.pas @@ -223,7 +223,8 @@ TGocciaComputedSetterEntry = record cekGetter, cekSetter, cekField, - cekAccessor + cekAccessor, + cekStaticBlock ); // TC39 proposal-decorators: unified class element with optional decorators @@ -240,6 +241,7 @@ TGocciaClassElement = record GetterNode: TGocciaGetterExpression; SetterNode: TGocciaSetterExpression; FieldInitializer: TGocciaExpression; + StaticBlockBody: TGocciaBlockStatement; TypeAnnotation: string; end; @@ -532,6 +534,8 @@ function CreateModuleNamespaceObject(const AModule: TGocciaModule): TGocciaValue end; destructor TGocciaClassDefinition.Destroy; + var + I: Integer; begin FMethods.Free; FGetters.Free; @@ -546,6 +550,9 @@ function CreateModuleNamespaceObject(const AModule: TGocciaModule): TGocciaValue FPrivateMethods.Free; if Assigned(FPrivateStaticProperties) then FPrivateStaticProperties.Free; + for I := 0 to High(FElements) do + if (FElements[I].Kind = cekStaticBlock) and Assigned(FElements[I].StaticBlockBody) then + FElements[I].StaticBlockBody.Free; inherited; end; diff --git a/units/Goccia.Bytecode.OpCodeNames.pas b/units/Goccia.Bytecode.OpCodeNames.pas index 6362dc9be..4e27e8911 100644 --- a/units/Goccia.Bytecode.OpCodeNames.pas +++ b/units/Goccia.Bytecode.OpCodeNames.pas @@ -85,6 +85,7 @@ function OpCodeName(const AOp: UInt8): string; OP_CLASS_ADD_METHOD_CONST: Result := 'OP_CLASS_ADD_METHOD_CONST'; OP_CLASS_SET_FIELD_INITIALIZER: Result := 'OP_CLASS_SET_FIELD_INITIALIZER'; OP_CLASS_DECLARE_PRIVATE_STATIC_CONST: Result := 'OP_CLASS_DECLARE_PRIVATE_STATIC_CONST'; + OP_CLASS_EXEC_STATIC_BLOCK: Result := 'OP_CLASS_EXEC_STATIC_BLOCK'; OP_TO_PRIMITIVE: Result := 'OP_TO_PRIMITIVE'; OP_UNPACK: Result := 'OP_UNPACK'; OP_LOAD_NULL: Result := 'OP_LOAD_NULL'; diff --git a/units/Goccia.Bytecode.pas b/units/Goccia.Bytecode.pas index bfe573b35..c3b139f54 100644 --- a/units/Goccia.Bytecode.pas +++ b/units/Goccia.Bytecode.pas @@ -95,6 +95,7 @@ interface OP_CLASS_ADD_METHOD_CONST = 68, OP_CLASS_SET_FIELD_INITIALIZER = 69, OP_CLASS_DECLARE_PRIVATE_STATIC_CONST = 70, + OP_CLASS_EXEC_STATIC_BLOCK = 71, OP_TO_PRIMITIVE = 72, OP_UNPACK = 73, OP_LOAD_NULL = 74, diff --git a/units/Goccia.Compiler.Statements.pas b/units/Goccia.Compiler.Statements.pas index 26bfd2749..09dab6844 100644 --- a/units/Goccia.Compiler.Statements.pas +++ b/units/Goccia.Compiler.Statements.pas @@ -1740,6 +1740,72 @@ procedure CompileFieldInitializer(const ACtx: TGocciaCompilationContext; ACtx.Scope.FreeRegister; end; +// ES2022 §15.7.14 ClassStaticBlockDefinition +procedure CompileStaticBlock(const ACtx: TGocciaCompilationContext; + const AClassReg: UInt8; const ABody: TGocciaBlockStatement); +var + OldTemplate: TGocciaFunctionTemplate; + OldScope: TGocciaCompilerScope; + ChildTemplate: TGocciaFunctionTemplate; + ChildScope: TGocciaCompilerScope; + ChildCtx: TGocciaCompilationContext; + FuncIdx: UInt16; + FnReg: UInt8; + I: Integer; + Node: TGocciaASTNode; + Reg: UInt8; +begin + OldTemplate := ACtx.Template; + OldScope := ACtx.Scope; + + ChildTemplate := TGocciaFunctionTemplate.Create(''); + ChildTemplate.DebugInfo := TGocciaDebugInfo.Create(ACtx.SourcePath); + ChildTemplate.ParameterCount := 0; + ChildScope := TGocciaCompilerScope.Create(OldScope, 0); + + ChildScope.DeclareLocal(KEYWORD_THIS, False); + + ACtx.SwapState(ChildTemplate, ChildScope); + + ChildCtx := ACtx; + ChildCtx.Template := ChildTemplate; + ChildCtx.Scope := ChildScope; + + for I := 0 to ABody.Nodes.Count - 1 do + begin + Node := ABody.Nodes[I]; + if Node is TGocciaStatement then + ACtx.CompileStatement(TGocciaStatement(Node)) + else if Node is TGocciaExpression then + begin + Reg := ACtx.Scope.AllocateRegister; + ACtx.CompileExpression(TGocciaExpression(Node), Reg); + ACtx.Scope.FreeRegister; + end; + end; + + EmitInstruction(ChildCtx, EncodeABx(OP_LOAD_UNDEFINED, 0, 0)); + EmitInstruction(ChildCtx, EncodeABC(OP_RETURN, 0, 0, 0)); + + ChildTemplate.MaxRegisters := ChildScope.MaxSlot; + + for I := 0 to ChildScope.UpvalueCount - 1 do + ChildTemplate.AddUpvalueDescriptor( + ChildScope.GetUpvalue(I).IsLocal, + ChildScope.GetUpvalue(I).Index); + + ACtx.SwapState(OldTemplate, OldScope); + ChildScope.Free; + + FuncIdx := OldTemplate.AddFunction(ChildTemplate); + FnReg := ACtx.Scope.AllocateRegister; + EmitInstruction(ACtx, EncodeABx(OP_CLOSURE, FnReg, FuncIdx)); + + EmitInstruction(ACtx, EncodeABC(OP_CLASS_EXEC_STATIC_BLOCK, + AClassReg, FnReg, 0)); + ACtx.Scope.FreeRegister; +end; + function ElementDescriptor(const AKind: TGocciaClassElementKind; const AName: string; const AIsStatic, AIsPrivate: Boolean): string; var @@ -1752,6 +1818,7 @@ function ElementDescriptor(const AKind: TGocciaClassElementKind; cekSetter: KindChar := 's'; cekField: KindChar := 'f'; cekAccessor: KindChar := 'a'; + cekStaticBlock: KindChar := 'b'; else KindChar := 'm'; end; @@ -1936,7 +2003,7 @@ procedure CompileClassDeclaration(const ACtx: TGocciaCompilationContext; GetterPair: TGocciaGetterExpressionMap.TKeyValuePair; SetterPair: TGocciaSetterExpressionMap.TKeyValuePair; StaticPropPair: TGocciaExpressionMap.TKeyValuePair; - LocalIdx, UpvalIdx: Integer; + I, LocalIdx, UpvalIdx: Integer; HasSuper: Boolean; PrivPrefix: string; begin @@ -2041,34 +2108,73 @@ procedure CompileClassDeclaration(const ACtx: TGocciaCompilationContext; HasAccessorInitializers(ClassDef) then CompileFieldInitializer(ACtx, ClassReg, ClassDef); - for StaticPropPair in ClassDef.StaticProperties do + // Static fields without FElements entries (legacy / no static blocks) + if Length(ClassDef.FElements) = 0 then begin - ValReg := ACtx.Scope.AllocateRegister; - ACtx.CompileExpression(StaticPropPair.Value, ValReg); - KeyIdx := ACtx.Template.AddConstantString(StaticPropPair.Key); - if KeyIdx > High(UInt8) then - raise Exception.Create('Constant pool overflow: static property name index exceeds 255'); - EmitInstruction(ACtx, EncodeABC(OP_SET_PROP_CONST, ClassReg, - UInt8(KeyIdx), ValReg)); - ACtx.Scope.FreeRegister; - end; + for StaticPropPair in ClassDef.StaticProperties do + begin + ValReg := ACtx.Scope.AllocateRegister; + ACtx.CompileExpression(StaticPropPair.Value, ValReg); + KeyIdx := ACtx.Template.AddConstantString(StaticPropPair.Key); + if KeyIdx > High(UInt8) then + raise Exception.Create('Constant pool overflow: static property name index exceeds 255'); + EmitInstruction(ACtx, EncodeABC(OP_SET_PROP_CONST, ClassReg, + UInt8(KeyIdx), ValReg)); + ACtx.Scope.FreeRegister; + end; - for StaticPropPair in ClassDef.PrivateStaticProperties do - begin - ValReg := ACtx.Scope.AllocateRegister; - ACtx.CompileExpression(StaticPropPair.Value, ValReg); - KeyIdx := ACtx.Template.AddConstantString('#' + PrivPrefix + StaticPropPair.Key); - if KeyIdx > High(UInt8) then - raise Exception.Create('Constant pool overflow: static property name index exceeds 255'); - EmitInstruction(ACtx, EncodeABC(OP_CLASS_DECLARE_PRIVATE_STATIC_CONST, - ClassReg, UInt8(KeyIdx), 0)); - EmitInstruction(ACtx, EncodeABC(OP_SET_PROP_CONST, ClassReg, - UInt8(KeyIdx), ValReg)); - ACtx.Scope.FreeRegister; + for StaticPropPair in ClassDef.PrivateStaticProperties do + begin + ValReg := ACtx.Scope.AllocateRegister; + ACtx.CompileExpression(StaticPropPair.Value, ValReg); + KeyIdx := ACtx.Template.AddConstantString('#' + PrivPrefix + StaticPropPair.Key); + if KeyIdx > High(UInt8) then + raise Exception.Create('Constant pool overflow: static property name index exceeds 255'); + EmitInstruction(ACtx, EncodeABC(OP_CLASS_DECLARE_PRIVATE_STATIC_CONST, + ClassReg, UInt8(KeyIdx), 0)); + EmitInstruction(ACtx, EncodeABC(OP_SET_PROP_CONST, ClassReg, + UInt8(KeyIdx), ValReg)); + ACtx.Scope.FreeRegister; + 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 + if ClassDef.FElements[I].Kind = cekStaticBlock then + CompileStaticBlock(ACtx, ClassReg, ClassDef.FElements[I].StaticBlockBody) + else if (ClassDef.FElements[I].Kind = cekField) and ClassDef.FElements[I].IsStatic then + begin + ValReg := ACtx.Scope.AllocateRegister; + if Assigned(ClassDef.FElements[I].FieldInitializer) then + ACtx.CompileExpression(ClassDef.FElements[I].FieldInitializer, ValReg) + else + EmitInstruction(ACtx, EncodeABx(OP_LOAD_UNDEFINED, ValReg, 0)); + if ClassDef.FElements[I].IsPrivate then + begin + KeyIdx := ACtx.Template.AddConstantString( + '#' + PrivPrefix + ClassDef.FElements[I].Name); + if KeyIdx > High(UInt8) then + raise Exception.Create('Constant pool overflow: static property name index exceeds 255'); + EmitInstruction(ACtx, EncodeABC(OP_CLASS_DECLARE_PRIVATE_STATIC_CONST, + ClassReg, UInt8(KeyIdx), 0)); + EmitInstruction(ACtx, EncodeABC(OP_SET_PROP_CONST, ClassReg, + UInt8(KeyIdx), ValReg)); + end + else + begin + KeyIdx := ACtx.Template.AddConstantString(ClassDef.FElements[I].Name); + if KeyIdx > High(UInt8) then + raise Exception.Create('Constant pool overflow: static property name index exceeds 255'); + EmitInstruction(ACtx, EncodeABC(OP_SET_PROP_CONST, ClassReg, + UInt8(KeyIdx), ValReg)); + end; + ACtx.Scope.FreeRegister; + end; + end; + if HasSuper then CompileDecoratorAndAccessorPass(ACtx, ClassReg, ClassDef, SuperReg) else @@ -2204,34 +2310,73 @@ procedure CompileClassExpression(const ACtx: TGocciaCompilationContext; HasAccessorInitializers(ClassDef) then CompileFieldInitializer(ACtx, ADest, ClassDef); - for StaticPropPair in ClassDef.StaticProperties do + // Static fields without FElements entries (legacy / no static blocks) + if Length(ClassDef.FElements) = 0 then begin - ValReg := ACtx.Scope.AllocateRegister; - ACtx.CompileExpression(StaticPropPair.Value, ValReg); - KeyIdx := ACtx.Template.AddConstantString(StaticPropPair.Key); - if KeyIdx > High(UInt8) then - raise Exception.Create('Constant pool overflow: static property name index exceeds 255'); - EmitInstruction(ACtx, EncodeABC(OP_SET_PROP_CONST, ADest, - UInt8(KeyIdx), ValReg)); - ACtx.Scope.FreeRegister; - end; + for StaticPropPair in ClassDef.StaticProperties do + begin + ValReg := ACtx.Scope.AllocateRegister; + ACtx.CompileExpression(StaticPropPair.Value, ValReg); + KeyIdx := ACtx.Template.AddConstantString(StaticPropPair.Key); + if KeyIdx > High(UInt8) then + raise Exception.Create('Constant pool overflow: static property name index exceeds 255'); + EmitInstruction(ACtx, EncodeABC(OP_SET_PROP_CONST, ADest, + UInt8(KeyIdx), ValReg)); + ACtx.Scope.FreeRegister; + end; - for StaticPropPair in ClassDef.PrivateStaticProperties do - begin - ValReg := ACtx.Scope.AllocateRegister; - ACtx.CompileExpression(StaticPropPair.Value, ValReg); - KeyIdx := ACtx.Template.AddConstantString('#' + PrivPrefix + StaticPropPair.Key); - if KeyIdx > High(UInt8) then - raise Exception.Create('Constant pool overflow: static property name index exceeds 255'); - EmitInstruction(ACtx, EncodeABC(OP_CLASS_DECLARE_PRIVATE_STATIC_CONST, - ADest, UInt8(KeyIdx), 0)); - EmitInstruction(ACtx, EncodeABC(OP_SET_PROP_CONST, ADest, - UInt8(KeyIdx), ValReg)); - ACtx.Scope.FreeRegister; + for StaticPropPair in ClassDef.PrivateStaticProperties do + begin + ValReg := ACtx.Scope.AllocateRegister; + ACtx.CompileExpression(StaticPropPair.Value, ValReg); + KeyIdx := ACtx.Template.AddConstantString('#' + PrivPrefix + StaticPropPair.Key); + if KeyIdx > High(UInt8) then + raise Exception.Create('Constant pool overflow: static property name index exceeds 255'); + EmitInstruction(ACtx, EncodeABC(OP_CLASS_DECLARE_PRIVATE_STATIC_CONST, + ADest, UInt8(KeyIdx), 0)); + EmitInstruction(ACtx, EncodeABC(OP_SET_PROP_CONST, ADest, + UInt8(KeyIdx), ValReg)); + ACtx.Scope.FreeRegister; + 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 + if ClassDef.FElements[I].Kind = cekStaticBlock then + CompileStaticBlock(ACtx, ADest, ClassDef.FElements[I].StaticBlockBody) + else if (ClassDef.FElements[I].Kind = cekField) and ClassDef.FElements[I].IsStatic then + begin + ValReg := ACtx.Scope.AllocateRegister; + if Assigned(ClassDef.FElements[I].FieldInitializer) then + ACtx.CompileExpression(ClassDef.FElements[I].FieldInitializer, ValReg) + else + EmitInstruction(ACtx, EncodeABx(OP_LOAD_UNDEFINED, ValReg, 0)); + if ClassDef.FElements[I].IsPrivate then + begin + KeyIdx := ACtx.Template.AddConstantString( + '#' + PrivPrefix + ClassDef.FElements[I].Name); + if KeyIdx > High(UInt8) then + raise Exception.Create('Constant pool overflow: static property name index exceeds 255'); + EmitInstruction(ACtx, EncodeABC(OP_CLASS_DECLARE_PRIVATE_STATIC_CONST, + ADest, UInt8(KeyIdx), 0)); + EmitInstruction(ACtx, EncodeABC(OP_SET_PROP_CONST, ADest, + UInt8(KeyIdx), ValReg)); + end + else + begin + KeyIdx := ACtx.Template.AddConstantString(ClassDef.FElements[I].Name); + if KeyIdx > High(UInt8) then + raise Exception.Create('Constant pool overflow: static property name index exceeds 255'); + EmitInstruction(ACtx, EncodeABC(OP_SET_PROP_CONST, ADest, + UInt8(KeyIdx), ValReg)); + end; + ACtx.Scope.FreeRegister; + end; + end; + if HasSuper then begin CompileDecoratorAndAccessorPass(ACtx, ADest, ClassDef, SuperReg); diff --git a/units/Goccia.Error.Suggestions.pas b/units/Goccia.Error.Suggestions.pas index 47a22b43d..d69227443 100644 --- a/units/Goccia.Error.Suggestions.pas +++ b/units/Goccia.Error.Suggestions.pas @@ -97,6 +97,7 @@ interface SSuggestClassMemberSyntax = 'Class members can be methods, properties, or accessors. Type modifiers (public, private, readonly) are supported as comments'; SSuggestClassMemberExpectedSyntax = 'Class members can be: methods name() {}, properties name = value, or typed declarations name: Type;'; SSuggestDecoratorsOnlyClasses = 'Decorators can only be applied to class declarations or class elements'; + SSuggestStaticBlockNoDecorators = 'Remove the decorator — static blocks cannot be decorated'; SSuggestGetterNoParameters = 'Getters must have no parameters — remove the parameters'; SSuggestSetterOneParameter = 'Setters take exactly one parameter: set name(value) { ... }'; SSuggestProvideSetterParameterName = 'Provide a name for the setter parameter'; diff --git a/units/Goccia.Evaluator.pas b/units/Goccia.Evaluator.pas index 27ec7df73..9e347ccea 100644 --- a/units/Goccia.Evaluator.pas +++ b/units/Goccia.Evaluator.pas @@ -1741,6 +1741,20 @@ function EvaluateClassExpression(const AClassExpression: TGocciaClassExpression; AClassExpression.Line, AClassExpression.Column); end; +// ES2022 §15.7.14 ClassStaticBlockDefinition: execute static block body +procedure ExecuteStaticBlock(const ABody: TGocciaBlockStatement; + const AContext: TGocciaEvaluationContext; const AClassValue: TGocciaClassValue); +var + BlockScope: TGocciaClassInitScope; + BlockContext: TGocciaEvaluationContext; +begin + BlockScope := TGocciaClassInitScope.Create(AContext.Scope, AClassValue); + BlockScope.ThisValue := AClassValue; + BlockContext := AContext; + BlockContext.Scope := BlockScope; + EvaluateStatements(ABody.Nodes, BlockContext); +end; + function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const AContext: TGocciaEvaluationContext; const ALine, AColumn: Integer): TGocciaClassValue; var SuperClass: TGocciaClassValue; @@ -1810,16 +1824,20 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const ClassValue.AddMethod(MethodPair.Key, Method); end; - for PropertyPair in AClassDef.StaticProperties do + // Static fields without FElements entries (legacy / no static blocks) + if Length(AClassDef.FElements) = 0 then begin - PropertyValue := EvaluateExpression(PropertyPair.Value, AContext); - ClassValue.SetProperty(PropertyPair.Key, PropertyValue); - end; + for PropertyPair in AClassDef.StaticProperties do + begin + PropertyValue := EvaluateExpression(PropertyPair.Value, AContext); + ClassValue.SetProperty(PropertyPair.Key, PropertyValue); + end; - for PropertyPair in AClassDef.PrivateStaticProperties do - begin - PropertyValue := EvaluateExpression(PropertyPair.Value, AContext); - ClassValue.AddPrivateStaticProperty(PropertyPair.Key, PropertyValue); + for PropertyPair in AClassDef.PrivateStaticProperties do + begin + PropertyValue := EvaluateExpression(PropertyPair.Value, AContext); + ClassValue.AddPrivateStaticProperty(PropertyPair.Key, PropertyValue); + end; end; // Store instance property definitions on the class in declaration order @@ -1991,8 +2009,35 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const end; end; + // ES2022 §15.7.14: evaluate static fields and static blocks in source order + for I := 0 to High(AClassDef.FElements) do + begin + Elem := AClassDef.FElements[I]; + if Elem.Kind = cekStaticBlock then + ExecuteStaticBlock(Elem.StaticBlockBody, AContext, ClassValue) + else if (Elem.Kind = cekField) and Elem.IsStatic then + begin + if Assigned(Elem.FieldInitializer) then + PropertyValue := EvaluateExpression(Elem.FieldInitializer, AContext) + else + PropertyValue := TGocciaUndefinedLiteralValue.UndefinedValue; + if Elem.IsPrivate then + ClassValue.AddPrivateStaticProperty(Elem.Name, PropertyValue) + else + ClassValue.SetProperty(Elem.Name, PropertyValue); + end; + end; + // TC39 proposal-decorators §3.2 ApplyDecoratorsToClassDefinition - HasDecorators := (Length(AClassDef.FDecorators) > 0) or (Length(AClassDef.FElements) > 0); + HasDecorators := Length(AClassDef.FDecorators) > 0; + if not HasDecorators then + for I := 0 to High(AClassDef.FElements) do + if (Length(AClassDef.FElements[I].Decorators) > 0) or + (AClassDef.FElements[I].Kind = cekAccessor) then + begin + HasDecorators := True; + Break; + end; if HasDecorators then begin diff --git a/units/Goccia.Parser.pas b/units/Goccia.Parser.pas index 6d0fd7a5a..21207c6fe 100644 --- a/units/Goccia.Parser.pas +++ b/units/Goccia.Parser.pas @@ -2946,6 +2946,28 @@ function TGocciaParser.ParseClassBody(const AClassName: string): TGocciaClassDef IsStatic := Match(gttStatic); + // ES2022 §15.7.14 ClassStaticBlockDefinition: static { ... } + if IsStatic and Check(gttLeftBrace) then + begin + if Length(MemberDecorators) > 0 then + raise TGocciaSyntaxError.Create( + 'Decorators cannot be applied to static blocks', + Peek.Line, Peek.Column, FFileName, FSourceLines, + SSuggestStaticBlockNoDecorators); + Consume(gttLeftBrace, 'Expected "{" after "static"', + SSuggestOpenBraceClassBody); + SetLength(Elements, Length(Elements) + 1); + Elements[High(Elements)].Kind := cekStaticBlock; + Elements[High(Elements)].Name := ''; + Elements[High(Elements)].IsStatic := True; + Elements[High(Elements)].IsPrivate := False; + Elements[High(Elements)].IsComputed := False; + Elements[High(Elements)].ComputedKeyExpression := nil; + Elements[High(Elements)].Decorators := nil; + Elements[High(Elements)].StaticBlockBody := BlockStatement; + Continue; + end; + while Check(gttIdentifier) and ((Peek.Lexeme = KEYWORD_PUBLIC) or (Peek.Lexeme = KEYWORD_PROTECTED) or (Peek.Lexeme = KEYWORD_PRIVATE) or (Peek.Lexeme = KEYWORD_READONLY) or (Peek.Lexeme = KEYWORD_OVERRIDE) or (Peek.Lexeme = KEYWORD_ABSTRACT)) do @@ -3072,7 +3094,7 @@ function TGocciaParser.ParseClassBody(const AClassName: string): TGocciaClassDef ConsumeSemicolonOrASI('Expected ";" after property', SSuggestAddSemicolon); - if Length(MemberDecorators) > 0 then + if (Length(MemberDecorators) > 0) or IsStatic then begin SetLength(Elements, Length(Elements) + 1); Elements[High(Elements)].Kind := cekField; @@ -3113,7 +3135,7 @@ function TGocciaParser.ParseClassBody(const AClassName: string): TGocciaClassDef Advance; PropertyValue := TGocciaLiteralExpression.Create(TGocciaUndefinedLiteralValue.UndefinedValue, Peek.Line, Peek.Column); - if Length(MemberDecorators) > 0 then + if (Length(MemberDecorators) > 0) or IsStatic then begin SetLength(Elements, Length(Elements) + 1); Elements[High(Elements)].Kind := cekField; diff --git a/units/Goccia.VM.pas b/units/Goccia.VM.pas index 02f6d0666..947a2cbc2 100644 --- a/units/Goccia.VM.pas +++ b/units/Goccia.VM.pas @@ -4114,6 +4114,28 @@ function TGocciaVM.ExecuteClosureRegistersInternal( GlobalName, TGocciaUndefinedLiteralValue.UndefinedValue); end; + // ES2022 §15.7.14: execute static block closure with this = class + OP_CLASS_EXEC_STATIC_BLOCK: + begin + if (FRegisters[B].Kind = grkObject) and + (FRegisters[B].ObjectValue is TGocciaBytecodeFunctionValue) then + ExecuteClosureRegisters0( + TGocciaBytecodeFunctionValue(FRegisters[B].ObjectValue).FClosure, + FRegisters[A]) + else if (FRegisters[B].Kind = grkObject) and + Assigned(FRegisters[B].ObjectValue) and + FRegisters[B].ObjectValue.IsCallable then + begin + CallArgs := AcquireArguments(0); + try + InvokeFunctionValue(RegisterToValue(FRegisters[B]), + CallArgs, RegisterToValue(FRegisters[A])); + finally + ReleaseArguments(CallArgs); + end; + end; + end; + OP_GET_PROP_CONST: if (FRegisters[B].Kind = grkObject) and Assigned(FRegisters[B].ObjectValue) then begin