From 6451065c779e0b1f0bfb9a27ab0d55834edb3d7d Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 3 Jun 2025 14:40:10 +0800 Subject: [PATCH] Poor support for functions/namespaces. If you look at the specification for `console` [1], you'll note that it's a namespace, not an interface (like most things). Furthermore, MDN lists its methods as "static". But it's a pretty weird namespace IMO, because some of its "functions", like `count` can have state associated with them. This causes some problems with our current implementation. Something like: ``` [1].forEach(console.log) ``` Fails, since `this` isn't our window-attached Console instance. This commit introducing a new `static_XYZ` naming convention which does not have the class/Self as a receiver: ``` pub fn static_log(values: []JsObject, page: *Page) !void { ``` This turns Console into a namespace for these specific functions, while still being used normally for those functions that require state. We could infer this behavior from the first parameter, but that seems more error prone. For now, I prefer having the explicit `static_` prefix. [1] https://console.spec.whatwg.org/#console-namespace --- src/browser/console/console.zig | 29 ++++++++++++++++++++--------- src/runtime/js.zig | 29 +++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/src/browser/console/console.zig b/src/browser/console/console.zig index 20148b8a..baa6bb33 100644 --- a/src/browser/console/console.zig +++ b/src/browser/console/console.zig @@ -30,46 +30,46 @@ pub const Console = struct { timers: std.StringHashMapUnmanaged(u32) = .{}, counts: std.StringHashMapUnmanaged(u32) = .{}, - pub fn _lp(_: *const Console, values: []JsObject, page: *Page) !void { + pub fn static_lp(values: []JsObject, page: *Page) !void { if (values.len == 0) { return; } log.fatal(.console, "lightpanda", .{ .args = try serializeValues(values, page) }); } - pub fn _log(_: *const Console, values: []JsObject, page: *Page) !void { + pub fn static_log(values: []JsObject, page: *Page) !void { if (values.len == 0) { return; } log.info(.console, "info", .{ .args = try serializeValues(values, page) }); } - pub fn _info(console: *const Console, values: []JsObject, page: *Page) !void { - return console._log(values, page); + pub fn static_info(values: []JsObject, page: *Page) !void { + return static_log(values, page); } - pub fn _debug(_: *const Console, values: []JsObject, page: *Page) !void { + pub fn static_debug(values: []JsObject, page: *Page) !void { if (values.len == 0) { return; } log.debug(.console, "debug", .{ .args = try serializeValues(values, page) }); } - pub fn _warn(_: *const Console, values: []JsObject, page: *Page) !void { + pub fn static_warn(values: []JsObject, page: *Page) !void { if (values.len == 0) { return; } log.warn(.console, "warn", .{ .args = try serializeValues(values, page) }); } - pub fn _error(_: *const Console, values: []JsObject, page: *Page) !void { + pub fn static_error(values: []JsObject, page: *Page) !void { if (values.len == 0) { return; } log.info(.console, "error", .{ .args = try serializeValues(values, page) }); } - pub fn _clear(_: *const Console) void {} + pub fn static_clear() void {} pub fn _count(self: *Console, label_: ?[]const u8, page: *Page) !void { const label = label_ orelse "default"; @@ -130,7 +130,7 @@ pub const Console = struct { log.warn(.console, "timer stop", .{ .label = label, .elapsed = elapsed - kv.value }); } - pub fn _assert(_: *Console, assertion: JsObject, values: []JsObject, page: *Page) !void { + pub fn static_assert(assertion: JsObject, values: []JsObject, page: *Page) !void { if (assertion.isTruthy()) { return; } @@ -225,7 +225,18 @@ test "Browser.Console" { try testing.expectEqual("[assertion failed] values= 1: x 2: true", captured[1]); try testing.expectEqual("[assertion failed] values= 1: x", captured[2]); } + + { + test_capture.reset(); + try runner.testCases(&.{ + .{ "[1].forEach(console.log)", null }, + }, .{}); + + const captured = test_capture.captured.items; + try testing.expectEqual("[info] args= 1: 1 2: 0 3: [1]", captured[0]); + } } + const TestCapture = struct { captured: std.ArrayListUnmanaged([]const u8) = .{}, diff --git a/src/runtime/js.zig b/src/runtime/js.zig index 89fb4aff..7f5a2b6e 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -1680,6 +1680,8 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { } } else if (comptime std.mem.startsWith(u8, name, "get_")) { generateProperty(Struct, name[4..], isolate, template_proto); + } else if (comptime std.mem.startsWith(u8, name, "static_")) { + generateFunction(Struct, name[7..], isolate, template_proto); } } @@ -1769,6 +1771,23 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { template_proto.set(js_name, function_template, v8.PropertyAttribute.None); } + fn generateFunction(comptime Struct: type, comptime name: []const u8, isolate: v8.Isolate, template_proto: v8.ObjectTemplate) void { + const js_name = v8.String.initUtf8(isolate, name).toName(); + const function_template = v8.FunctionTemplate.initCallback(isolate, struct { + fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void { + const info = v8.FunctionCallbackInfo.initFromV8(raw_info); + var caller = Caller(Self, State).init(info); + defer caller.deinit(); + + const named_function = comptime NamedFunction.init(Struct, "static_" ++ name); + caller.function(Struct, named_function, info) catch |err| { + caller.handleError(Struct, named_function, err, info); + }; + } + }.callback); + template_proto.set(js_name, function_template, v8.PropertyAttribute.None); + } + fn generateAttribute(comptime Struct: type, comptime name: []const u8, isolate: v8.Isolate, template: v8.FunctionTemplate, template_proto: v8.ObjectTemplate) void { const zig_value = @field(Struct, name); const js_value = simpleZigValueToJs(isolate, zig_value, true); @@ -2154,6 +2173,8 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { return @ptrFromInt(@intFromPtr(toa.ptr) + @as(usize, @intCast(offset))); } if (prototype_index == type_index) { + // When a type has itself as the prototype, then we've + // reached the end of the chain. return error.InvalidArgument; } type_index = prototype_index; @@ -2341,6 +2362,14 @@ fn Caller(comptime E: type, comptime State: type) type { info.getReturnValue().set(try scope.zigValueToJs(res)); } + fn function(self: *Self, comptime Struct: type, comptime named_function: NamedFunction, info: v8.FunctionCallbackInfo) !void { + const scope = self.scope; + const func = @field(Struct, named_function.name); + const args = try self.getArgs(Struct, named_function, 0, info); + const res = @call(.auto, func, args); + info.getReturnValue().set(try scope.zigValueToJs(res)); + } + fn getter(self: *Self, comptime Struct: type, comptime named_function: NamedFunction, info: v8.FunctionCallbackInfo) !void { const scope = self.scope; const func = @field(Struct, named_function.name);