From 986337e92a41910e827693caf5f186d95ea6cce6 Mon Sep 17 00:00:00 2001 From: bing Date: Mon, 18 May 2026 21:14:54 +0800 Subject: [PATCH 1/2] feat(dsl): support static value fields on classes Unblocks ChainSafe/lodestar#8900 Support was previously added in ChainSafe/lodestar-z#328 but the DSL update removed it --- examples/js_dsl/mod.test.ts | 15 +++++++ examples/js_dsl/mod.zig | 24 +++++++++++ src/js/class_meta.zig | 86 ++++++++++++++++++++++++++++++++++++- src/js/export_module.zig | 1 + src/js/wrap_class.zig | 56 ++++++++++++++++++++++++ 5 files changed, 180 insertions(+), 2 deletions(-) diff --git a/examples/js_dsl/mod.test.ts b/examples/js_dsl/mod.test.ts index 505bd95..6d290f9 100644 --- a/examples/js_dsl/mod.test.ts +++ b/examples/js_dsl/mod.test.ts @@ -538,3 +538,18 @@ describe("module lifecycle - worker threads", () => { expect(mod.getEnvRefcount()).toEqual(refcountBefore); }); }); + +// Section 16: Static Class Fields +describe("static class fields", () => { + it("exposes BLS PublicKey sizes as own properties of the constructor", () => { + expect(mod.BlsPublicKey.COMPRESS_SIZE).toEqual(48); + expect(mod.BlsPublicKey.SERIALIZE_SIZE).toEqual(96); + }); + + it("statics live on the constructor, not on instances", () => { + const pk = new mod.BlsPublicKey(); + expect(pk.COMPRESS_SIZE).toBeUndefined(); + expect(Object.prototype.hasOwnProperty.call(mod.BlsPublicKey, "COMPRESS_SIZE")).toBe(true); + expect(Object.prototype.hasOwnProperty.call(pk, "COMPRESS_SIZE")).toBe(false); + }); +}); diff --git a/examples/js_dsl/mod.zig b/examples/js_dsl/mod.zig index 71804ee..29629e6 100644 --- a/examples/js_dsl/mod.zig +++ b/examples/js_dsl/mod.zig @@ -537,6 +537,30 @@ pub fn makeToken(value: Number) Token { return .{ .value = value.assertI32() + 1 }; } +// ============================================================================ +// Section 16: Static Class Fields +// ============================================================================ + +/// Demonstrates `js.class(.{ .static = .{ ... } })`: comptime-known values +/// exposed as own properties of the JS constructor (e.g. `BlsPublicKey.COMPRESS_SIZE`, +/// not `new BlsPublicKey().COMPRESS_SIZE`). BLS12-381 keys and signatures have +/// curve-fixed serialized sizes. Exposing them as static constants on the JS class +/// lets callers size buffers, validate hex lengths, etc. +pub const BlsPublicKey = struct { + pub const js_meta = js.class(.{ + .static = .{ + .COMPRESS_SIZE = 48, + .SERIALIZE_SIZE = 96, + }, + }); + + bytes: [96]u8, + + pub fn init() BlsPublicKey { + return .{ .bytes = [_]u8{0} ** 96 }; + } +}; + comptime { js.exportModule(@This(), .{ .init = struct { diff --git a/src/js/class_meta.zig b/src/js/class_meta.zig index 0236095..7c46a7d 100644 --- a/src/js/class_meta.zig +++ b/src/js/class_meta.zig @@ -128,6 +128,19 @@ pub fn hasProperties(comptime T: type) bool { return @hasField(@TypeOf(T.js_meta.options), "properties"); } +/// Checks if a ZAPI DSL class type `T` declares static value fields in its `js_meta`. +pub fn hasStaticFields(comptime T: type) bool { + if (!hasClassMeta(T)) return false; + return @hasField(@TypeOf(T.js_meta.options), "static"); +} + +/// Returns the struct fields describing the static-value entries declared in +/// `T`'s `js_meta.options.static`. Empty slice if none. +pub fn staticFieldInfos(comptime T: type) []const std.builtin.Type.StructField { + if (comptime !hasStaticFields(T)) return &.{}; + return @typeInfo(@TypeOf(T.js_meta.options.static)).@"struct".fields; +} + /// Returns a slice of `std.builtin.Type.StructField` for the properties defined /// in a ZAPI DSL class `T`'s `js_meta`. /// @@ -183,8 +196,11 @@ fn validateClassOptions(comptime Opts: type, comptime opts: Opts) void { } inline for (@typeInfo(Opts).@"struct".fields) |field_info| { - if (!std.mem.eql(u8, field_info.name, "name") and !std.mem.eql(u8, field_info.name, "properties")) { - @compileError("js.class only supports .name and .properties"); + const is_known = comptime (std.mem.eql(u8, field_info.name, "name") or + std.mem.eql(u8, field_info.name, "properties") or + std.mem.eql(u8, field_info.name, "static")); + if (!is_known) { + @compileError("js.class only supports .name, .properties, and .static (got: " ++ field_info.name ++ ")"); } } @@ -201,6 +217,36 @@ fn validateClassOptions(comptime Opts: type, comptime opts: Opts) void { if (@hasField(Opts, "properties")) { validateProperties(@TypeOf(opts.properties), opts.properties); } + + if (@hasField(Opts, "static")) { + validateStaticFields(@TypeOf(opts.static), opts.static); + } +} + +fn validateStaticFields(comptime Fields: type, comptime fields: Fields) void { + if (@typeInfo(Fields) != .@"struct") @compileError("js.class .static must be a struct literal"); + inline for (@typeInfo(Fields).@"struct".fields) |field_info| { + const value = @field(fields, field_info.name); + switch (@typeInfo(@TypeOf(value))) { + .comptime_int, + .int, + .comptime_float, + .float, + .bool, + => {}, + .pointer => |ptr| { + const ok_slice = ptr.size == .slice and ptr.child == u8 and ptr.is_const; + const ok_array_ptr = ptr.size == .one and @typeInfo(ptr.child) == .array and + @typeInfo(ptr.child).array.child == u8; + if (!ok_slice and !ok_array_ptr) { + @compileError("js.class .static value for '" ++ field_info.name ++ + "' must be an int, float, bool, or string"); + } + }, + else => @compileError("js.class .static value for '" ++ field_info.name ++ + "' must be an int, float, bool, or string"), + } + } } fn validateProperties(comptime Props: type, comptime props: Props) void { @@ -281,3 +327,39 @@ test "js.prop accepts explicit read-only derived getter" { test "bare bool property specs are invalid" { try std.testing.expect(propertyKind(true) == .invalid); } + +test "js.class accepts .static field map" { + const meta = class(.{ + .static = .{ + .MAX = 100, + .NAME = "demo", + .FLAG = true, + }, + }); + try std.testing.expect(isClassMetaValue(meta)); +} + +test "hasStaticFields and staticFieldInfos report declared statics" { + const T = struct { + pub const js_meta = class(.{ + .static = .{ + .COMPRESS_SIZE = 96, + .SERIALIZE_SIZE = 192, + }, + }); + }; + try std.testing.expect(hasStaticFields(T)); + + const fields = staticFieldInfos(T); + try std.testing.expectEqual(@as(usize, 2), fields.len); + try std.testing.expectEqualStrings("COMPRESS_SIZE", fields[0].name); + try std.testing.expectEqualStrings("SERIALIZE_SIZE", fields[1].name); +} + +test "hasStaticFields false when .static is omitted" { + const T = struct { + pub const js_meta = class(.{}); + }; + try std.testing.expect(!hasStaticFields(T)); + try std.testing.expectEqual(@as(usize, 0), staticFieldInfos(T).len); +} diff --git a/src/js/export_module.zig b/src/js/export_module.zig index aa70ba1..47aa34d 100644 --- a/src/js/export_module.zig +++ b/src/js/export_module.zig @@ -191,6 +191,7 @@ fn registerDecls(comptime Module: type, env: napi.Env, module: napi.Value, compt )); const cls = napi.Value{ .env = env.env, .value = class_val }; + try wrap_class.applyStaticFields(InnerType, env, cls); try class_runtime.registerClass(InnerType, env, cls); try module.setNamedProperty(name, cls); exported_any = true; diff --git a/src/js/wrap_class.zig b/src/js/wrap_class.zig index 7b10cef..09e9266 100644 --- a/src/js/wrap_class.zig +++ b/src/js/wrap_class.zig @@ -681,6 +681,62 @@ pub fn wrapClass(comptime T: type) type { }; } +/// Attaches the comptime-known values declared in `T.js_meta.options.static` +/// as own properties of the just-defined JS class constructor `class_val`. +/// +/// Called by `export_module.zig` right after `napi_define_class` returns, so +/// the values land on the constructor itself (i.e. `MyClass.MY_CONST` in JS), +/// not on instances. Each value is converted to a `napi.Value` per its Zig +/// type — see `validateStaticFields` in `class_meta.zig` for the accepted set. +pub fn applyStaticFields(comptime T: type, env: napi.Env, class_val: napi.Value) !void { + if (comptime !class_meta.hasStaticFields(T)) return; + inline for (comptime class_meta.staticFieldInfos(T)) |field_info| { + const value = @field(T.js_meta.options.static, field_info.name); + const napi_value = try createStaticFieldValue(env, value); + const name: [:0]const u8 = field_info.name ++ ""; + try class_val.setNamedProperty(name, napi_value); + } +} + +fn createStaticFieldValue(env: napi.Env, comptime value: anytype) !napi.Value { + const T = @TypeOf(value); + switch (@typeInfo(T)) { + .comptime_int => { + if (value >= 0 and value <= std.math.maxInt(u32)) { + return env.createUint32(@intCast(value)); + } + if (value >= std.math.minInt(i32) and value <= std.math.maxInt(i32)) { + return env.createInt32(@intCast(value)); + } + return env.createInt64(@intCast(value)); + }, + .int => |info| { + if (info.signedness == .unsigned and info.bits <= 32) { + return env.createUint32(@intCast(value)); + } + if (info.signedness == .signed and info.bits <= 32) { + return env.createInt32(@intCast(value)); + } + return env.createInt64(@intCast(value)); + }, + .comptime_float, .float => return env.createDouble(@floatCast(value)), + .bool => return env.getBoolean(value), + .pointer => |ptr| { + if (ptr.size == .slice and ptr.child == u8 and ptr.is_const) { + return env.createStringUtf8(value); + } + if (ptr.size == .one and @typeInfo(ptr.child) == .array and + @typeInfo(ptr.child).array.child == u8) + { + const arr = @typeInfo(ptr.child).array; + return env.createStringUtf8(value[0..arr.len]); + } + @compileError("createStaticFieldValue: unsupported pointer type " ++ @typeName(T)); + }, + else => @compileError("createStaticFieldValue: unsupported type " ++ @typeName(T)), + } +} + test "wrapClass compile-time validation requires class metadata" { try std.testing.expect(true); } From b6a59fb084a907232ceb1a3311fdb2b2c63169f0 Mon Sep 17 00:00:00 2001 From: bing Date: Tue, 19 May 2026 13:33:02 +0800 Subject: [PATCH 2/2] refactor(dsl): drop `.static` block in favor of auto-discovered `pub const` decls Walking `@typeInfo(T).@"struct".decls` at comptime and exposing each int/float/bool/string-typed decl removes the need for a separate `.static = .{...}` block. --- examples/js_dsl/mod.zig | 19 ++++----- src/js/class_meta.zig | 86 +---------------------------------------- src/js/wrap_class.zig | 33 +++++++++++----- 3 files changed, 34 insertions(+), 104 deletions(-) diff --git a/examples/js_dsl/mod.zig b/examples/js_dsl/mod.zig index 29629e6..0cf16f3 100644 --- a/examples/js_dsl/mod.zig +++ b/examples/js_dsl/mod.zig @@ -541,18 +541,15 @@ pub fn makeToken(value: Number) Token { // Section 16: Static Class Fields // ============================================================================ -/// Demonstrates `js.class(.{ .static = .{ ... } })`: comptime-known values -/// exposed as own properties of the JS constructor (e.g. `BlsPublicKey.COMPRESS_SIZE`, -/// not `new BlsPublicKey().COMPRESS_SIZE`). BLS12-381 keys and signatures have -/// curve-fixed serialized sizes. Exposing them as static constants on the JS class -/// lets callers size buffers, validate hex lengths, etc. +/// Demonstrates auto-exposure of scalar/string `pub const` decls as own +/// properties of the JS constructor (e.g. `BlsPublicKey.COMPRESS_SIZE`). +/// BLS12-381 keys and signatures have curve-fixed serialized sizes. +/// Exposing them as static constants lets callers size buffers, validate hex lengths, etc. pub const BlsPublicKey = struct { - pub const js_meta = js.class(.{ - .static = .{ - .COMPRESS_SIZE = 48, - .SERIALIZE_SIZE = 96, - }, - }); + pub const js_meta = js.class(.{}); + + pub const COMPRESS_SIZE = 48; + pub const SERIALIZE_SIZE = 96; bytes: [96]u8, diff --git a/src/js/class_meta.zig b/src/js/class_meta.zig index 7c46a7d..0236095 100644 --- a/src/js/class_meta.zig +++ b/src/js/class_meta.zig @@ -128,19 +128,6 @@ pub fn hasProperties(comptime T: type) bool { return @hasField(@TypeOf(T.js_meta.options), "properties"); } -/// Checks if a ZAPI DSL class type `T` declares static value fields in its `js_meta`. -pub fn hasStaticFields(comptime T: type) bool { - if (!hasClassMeta(T)) return false; - return @hasField(@TypeOf(T.js_meta.options), "static"); -} - -/// Returns the struct fields describing the static-value entries declared in -/// `T`'s `js_meta.options.static`. Empty slice if none. -pub fn staticFieldInfos(comptime T: type) []const std.builtin.Type.StructField { - if (comptime !hasStaticFields(T)) return &.{}; - return @typeInfo(@TypeOf(T.js_meta.options.static)).@"struct".fields; -} - /// Returns a slice of `std.builtin.Type.StructField` for the properties defined /// in a ZAPI DSL class `T`'s `js_meta`. /// @@ -196,11 +183,8 @@ fn validateClassOptions(comptime Opts: type, comptime opts: Opts) void { } inline for (@typeInfo(Opts).@"struct".fields) |field_info| { - const is_known = comptime (std.mem.eql(u8, field_info.name, "name") or - std.mem.eql(u8, field_info.name, "properties") or - std.mem.eql(u8, field_info.name, "static")); - if (!is_known) { - @compileError("js.class only supports .name, .properties, and .static (got: " ++ field_info.name ++ ")"); + if (!std.mem.eql(u8, field_info.name, "name") and !std.mem.eql(u8, field_info.name, "properties")) { + @compileError("js.class only supports .name and .properties"); } } @@ -217,36 +201,6 @@ fn validateClassOptions(comptime Opts: type, comptime opts: Opts) void { if (@hasField(Opts, "properties")) { validateProperties(@TypeOf(opts.properties), opts.properties); } - - if (@hasField(Opts, "static")) { - validateStaticFields(@TypeOf(opts.static), opts.static); - } -} - -fn validateStaticFields(comptime Fields: type, comptime fields: Fields) void { - if (@typeInfo(Fields) != .@"struct") @compileError("js.class .static must be a struct literal"); - inline for (@typeInfo(Fields).@"struct".fields) |field_info| { - const value = @field(fields, field_info.name); - switch (@typeInfo(@TypeOf(value))) { - .comptime_int, - .int, - .comptime_float, - .float, - .bool, - => {}, - .pointer => |ptr| { - const ok_slice = ptr.size == .slice and ptr.child == u8 and ptr.is_const; - const ok_array_ptr = ptr.size == .one and @typeInfo(ptr.child) == .array and - @typeInfo(ptr.child).array.child == u8; - if (!ok_slice and !ok_array_ptr) { - @compileError("js.class .static value for '" ++ field_info.name ++ - "' must be an int, float, bool, or string"); - } - }, - else => @compileError("js.class .static value for '" ++ field_info.name ++ - "' must be an int, float, bool, or string"), - } - } } fn validateProperties(comptime Props: type, comptime props: Props) void { @@ -327,39 +281,3 @@ test "js.prop accepts explicit read-only derived getter" { test "bare bool property specs are invalid" { try std.testing.expect(propertyKind(true) == .invalid); } - -test "js.class accepts .static field map" { - const meta = class(.{ - .static = .{ - .MAX = 100, - .NAME = "demo", - .FLAG = true, - }, - }); - try std.testing.expect(isClassMetaValue(meta)); -} - -test "hasStaticFields and staticFieldInfos report declared statics" { - const T = struct { - pub const js_meta = class(.{ - .static = .{ - .COMPRESS_SIZE = 96, - .SERIALIZE_SIZE = 192, - }, - }); - }; - try std.testing.expect(hasStaticFields(T)); - - const fields = staticFieldInfos(T); - try std.testing.expectEqual(@as(usize, 2), fields.len); - try std.testing.expectEqualStrings("COMPRESS_SIZE", fields[0].name); - try std.testing.expectEqualStrings("SERIALIZE_SIZE", fields[1].name); -} - -test "hasStaticFields false when .static is omitted" { - const T = struct { - pub const js_meta = class(.{}); - }; - try std.testing.expect(!hasStaticFields(T)); - try std.testing.expectEqual(@as(usize, 0), staticFieldInfos(T).len); -} diff --git a/src/js/wrap_class.zig b/src/js/wrap_class.zig index 09e9266..6a34c0c 100644 --- a/src/js/wrap_class.zig +++ b/src/js/wrap_class.zig @@ -681,24 +681,39 @@ pub fn wrapClass(comptime T: type) type { }; } -/// Attaches the comptime-known values declared in `T.js_meta.options.static` -/// as own properties of the just-defined JS class constructor `class_val`. +/// Walks `T`'s public declarations and attaches each scalar/string `pub const` +/// as an own property of the just-defined JS class constructor `class_val`. /// /// Called by `export_module.zig` right after `napi_define_class` returns, so /// the values land on the constructor itself (i.e. `MyClass.MY_CONST` in JS), -/// not on instances. Each value is converted to a `napi.Value` per its Zig -/// type — see `validateStaticFields` in `class_meta.zig` for the accepted set. +/// not on instances. A decl is exposed iff its value's type is an int, float, +/// bool, or string (`[]const u8` or `*const [N]u8`); functions, types, and +/// other decls are silently skipped — so existing methods/getters and the +/// `js_meta` decl naturally fall out. pub fn applyStaticFields(comptime T: type, env: napi.Env, class_val: napi.Value) !void { - if (comptime !class_meta.hasStaticFields(T)) return; - inline for (comptime class_meta.staticFieldInfos(T)) |field_info| { - const value = @field(T.js_meta.options.static, field_info.name); + inline for (@typeInfo(T).@"struct".decls) |decl| { + const value = @field(T, decl.name); + if (comptime !isStaticValueType(@TypeOf(value))) continue; const napi_value = try createStaticFieldValue(env, value); - const name: [:0]const u8 = field_info.name ++ ""; + const name: [:0]const u8 = decl.name ++ ""; try class_val.setNamedProperty(name, napi_value); } } -fn createStaticFieldValue(env: napi.Env, comptime value: anytype) !napi.Value { +fn isStaticValueType(comptime T: type) bool { + return switch (@typeInfo(T)) { + .comptime_int, .int, .comptime_float, .float, .bool => true, + .pointer => |ptr| blk: { + if (ptr.size == .slice and ptr.child == u8 and ptr.is_const) break :blk true; + if (ptr.size == .one and @typeInfo(ptr.child) == .array and + @typeInfo(ptr.child).array.child == u8) break :blk true; + break :blk false; + }, + else => false, + }; +} + +fn createStaticFieldValue(env: napi.Env, value: anytype) !napi.Value { const T = @TypeOf(value); switch (@typeInfo(T)) { .comptime_int => {