Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/language-restrictions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
270 changes: 270 additions & 0 deletions tests/language/classes/static-blocks.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
9 changes: 8 additions & 1 deletion units/Goccia.AST.Statements.pas
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,8 @@ TGocciaComputedSetterEntry = record
cekGetter,
cekSetter,
cekField,
cekAccessor
cekAccessor,
cekStaticBlock
);

// TC39 proposal-decorators: unified class element with optional decorators
Expand All @@ -240,6 +241,7 @@ TGocciaClassElement = record
GetterNode: TGocciaGetterExpression;
SetterNode: TGocciaSetterExpression;
FieldInitializer: TGocciaExpression;
StaticBlockBody: TGocciaBlockStatement;
TypeAnnotation: string;
end;

Expand Down Expand Up @@ -532,6 +534,8 @@ function CreateModuleNamespaceObject(const AModule: TGocciaModule): TGocciaValue
end;

destructor TGocciaClassDefinition.Destroy;
var
I: Integer;
begin
FMethods.Free;
FGetters.Free;
Expand All @@ -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;

Expand Down
1 change: 1 addition & 0 deletions units/Goccia.Bytecode.OpCodeNames.pas
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
1 change: 1 addition & 0 deletions units/Goccia.Bytecode.pas
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading
Loading