Import some of the zig-js-runtime env tests

- Fix passing nothing into variadic (i.e. slice) parameter
- Optimize @sizeOf(T) == 0 types by avoiding uncessary allocations
  (something zig-js-runtime is doing)
This commit is contained in:
Karl Seguin
2025-04-14 21:51:53 +08:00
parent 9e36702eb2
commit 8af71be551
4 changed files with 566 additions and 40 deletions

View File

@@ -21,7 +21,6 @@ const ArenaAllocator = std.heap.ArenaAllocator;
const log = std.log.scoped(.js);
// Global, should only be initialized once.
pub const Platform = struct {
inner: v8.Platform,
@@ -453,7 +452,11 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
}
}.callback);
template.getInstanceTemplate().setInternalFieldCount(1);
if (comptime isEmpty(Receiver(Struct)) == false) {
// If the struct is empty, we won't store a Zig reference inside
// the JS object, so we don't need to set the internal field count
template.getInstanceTemplate().setInternalFieldCount(1);
}
const class_name = v8.String.initUtf8(self.isolate, comptime classNameForStruct(Struct));
template.setClassName(class_name);
@@ -973,19 +976,28 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
else => @compileError("mapZigInstanceToJs requires a v8.Object (constructors) or v8.FunctionTemplate, got: " ++ @typeName(@TypeOf(js_obj_or_template))),
};
// The TAO contains the pointer ot our Zig instance as
// well as any meta data we'll need to use it later.
// See the TaggedAnyOpaque struct for more details.
const tao = try scope_arena.create(TaggedAnyOpaque);
tao.* = .{
.ptr = value,
.index = @field(TYPE_LOOKUP, @typeName(ptr.child)),
.sub_type = if (@hasDecl(ptr.child, "sub_type")) ptr.child.sub_type else null,
.offset = if (@typeInfo(ptr.child) != .@"opaque" and @hasField(ptr.child, "proto")) @offsetOf(ptr.child, "proto") else -1,
};
const isolate = self.isolate;
js_obj.setInternalField(0, v8.External.init(isolate, tao));
if (isEmpty(ptr.child) == false) {
// The TAO contains the pointer ot our Zig instance as
// well as any meta data we'll need to use it later.
// See the TaggedAnyOpaque struct for more details.
const tao = try scope_arena.create(TaggedAnyOpaque);
tao.* = .{
.ptr = value,
.index = @field(TYPE_LOOKUP, @typeName(ptr.child)),
.sub_type = if (@hasDecl(ptr.child, "sub_type")) ptr.child.sub_type else null,
.offset = if (@typeInfo(ptr.child) != .@"opaque" and @hasField(ptr.child, "proto")) @offsetOf(ptr.child, "proto") else -1,
};
js_obj.setInternalField(0, v8.External.init(isolate, tao));
} else {
// If the struct is empty, we don't need to do all
// the TOA stuff and setting the internal data.
// When we try to map this from JS->Zig, in
// typeTaggedAnyOpaque, we'll also know there that
// the type is empty and can create an empty instance.
}
const js_persistent = PersistentObject.init(isolate, js_obj);
gop.value_ptr.* = js_persistent;
return js_persistent;
@@ -1267,7 +1279,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
// Reverses the mapZigInstanceToJs, making sure that our TaggedAnyOpaque
// contains a ptr to the correct type.
fn typeTaggedAnyOpaque(comptime named_function: anytype, comptime R: type, op: ?*anyopaque) !R {
fn typeTaggedAnyOpaque(comptime named_function: anytype, comptime R: type, js_obj: v8.Object) !R {
const ti = @typeInfo(R);
if (ti != .pointer) {
@compileError(std.fmt.comptimePrint(
@@ -1276,7 +1288,15 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
));
}
const type_name = @typeName(ti.pointer.child);
const T = ti.pointer.child;
if (comptime isEmpty(T)) {
// Empty structs aren't stored as TOAs and there's no data
// stored in the JSObject's IntenrnalField. Why bother when
// we can just return an empty struct here?
return @constCast(@as(*const T, &.{}));
}
const type_name = @typeName(T);
if (@hasField(TypeLookup, type_name) == false) {
@compileError(std.fmt.comptimePrint(
"{s} has an unknown Zig type: {s}",
@@ -1284,8 +1304,9 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
));
}
const op = js_obj.getInternalField(0).castTo(v8.External).get();
const toa: *TaggedAnyOpaque = @alignCast(@ptrCast(op));
const expected_type_index = @field(TYPE_LOOKUP, @typeName(ti.pointer.child));
const expected_type_index = @field(TYPE_LOOKUP, type_name);
var type_index = toa.index;
if (type_index == expected_type_index) {
@@ -1319,6 +1340,10 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
};
}
fn isEmpty(comptime T: type) bool {
return @typeInfo(T) != .@"opaque" and @sizeOf(T) == 0;
}
// Responsible for calling Zig functions from JS invokations. This could
// probably just contained in Executor, but having this specific logic, which
// is somewhat repetitive between constructors, functions, getters, etc contained
@@ -1380,8 +1405,7 @@ fn Caller(comptime E: type) type {
comptime assertSelfReceiver(named_function);
var args = try self.getArgs(named_function, 1, info);
const external = info.getThis().getInternalField(0).castTo(v8.External);
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(S), external.get());
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(S), info.getThis());
// inject 'self' as the first parameter
@field(args, "0") = zig_instance;
@@ -1402,8 +1426,7 @@ fn Caller(comptime E: type) type {
switch (arg_fields.len) {
0 => {}, // getters _can_ be parameterless
1, 2 => {
const external = info.getThis().getInternalField(0).castTo(v8.External);
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(S), external.get());
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(S), info.getThis());
comptime assertSelfReceiver(named_function);
@field(args, "0") = zig_instance;
if (comptime arg_fields.len == 2) {
@@ -1421,8 +1444,7 @@ fn Caller(comptime E: type) type {
const S = named_function.S;
comptime assertSelfReceiver(named_function);
const external = info.getThis().getInternalField(0).castTo(v8.External);
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(S), external.get());
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(S), info.getThis());
const Setter = @TypeOf(named_function.func);
var args: ParamterTypes(Setter) = undefined;
@@ -1464,8 +1486,7 @@ fn Caller(comptime E: type) type {
switch (arg_fields.len) {
0, 1, 2 => @compileError(named_function.full_name ++ " must take at least a u32 and *bool parameter"),
3, 4 => {
const external = info.getThis().getInternalField(0).castTo(v8.External);
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(S), external.get());
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(S), info.getThis());
comptime assertSelfReceiver(named_function);
@field(args, "0") = zig_instance;
@field(args, "1") = idx;
@@ -1492,8 +1513,7 @@ fn Caller(comptime E: type) type {
const S = named_function.S;
comptime assertSelfReceiver(named_function);
const external = info.getThis().getInternalField(0).castTo(v8.External);
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(S), external.get());
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(S), info.getThis());
const IndexedSet = @TypeOf(named_function.func);
var args: ParamterTypes(IndexedSet) = undefined;
@@ -1537,8 +1557,7 @@ fn Caller(comptime E: type) type {
switch (arg_fields.len) {
0, 1, 2 => @compileError(named_function.full_name ++ " must take at least a u32 and *bool parameter"),
3, 4 => {
const external = info.getThis().getInternalField(0).castTo(v8.External);
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(S), external.get());
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(S), info.getThis());
comptime assertSelfReceiver(named_function);
@field(args, "0") = zig_instance;
@field(args, "1") = try self.nameToString(name);
@@ -1565,8 +1584,7 @@ fn Caller(comptime E: type) type {
const S = named_function.S;
comptime assertSelfReceiver(named_function);
const external = info.getThis().getInternalField(0).castTo(v8.External);
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(S), external.get());
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(S), info.getThis());
const IndexedSet = @TypeOf(named_function.func);
var args: ParamterTypes(IndexedSet) = undefined;
@@ -1749,14 +1767,17 @@ fn Caller(comptime E: type) type {
const corresponding_js_index = last_parameter_index - adjusted_offset;
const corresponding_js_value = info.getArg(@as(u32, @intCast(corresponding_js_index)));
if (corresponding_js_value.isArray() == false and slice_type != u8) {
const arr = try self.call_allocator.alloc(last_parameter_type_info.pointer.child, js_parameter_count - expected_js_parameters + 1);
for (arr, corresponding_js_index..) |*a, i| {
const js_value = info.getArg(@as(u32, @intCast(i)));
a.* = try self.jsValueToZig(named_function, slice_type, js_value);
}
is_variadic = true;
@field(args, tupleFieldName(last_parameter_index)) = arr;
if (js_parameter_count == 0) {
@field(args, tupleFieldName(last_parameter_index)) = &.{};
} else {
const arr = try self.call_allocator.alloc(last_parameter_type_info.pointer.child, js_parameter_count - expected_js_parameters + 1);
for (arr, corresponding_js_index..) |*a, i| {
const js_value = info.getArg(@as(u32, @intCast(i)));
a.* = try self.jsValueToZig(named_function, slice_type, js_value);
}
@field(args, tupleFieldName(last_parameter_index)) = arr;
}
}
}
}
@@ -1773,7 +1794,7 @@ fn Caller(comptime E: type) type {
@compileError("State must be the 2nd parameter: " ++ named_function.full_name);
} else if (i >= js_parameter_count) {
if (@typeInfo(param.type.?) != .optional) {
return error.TypeError;
return error.InvalidArgument;
}
@field(args, tupleFieldName(field_index)) = null;
} else {
@@ -1812,7 +1833,7 @@ fn Caller(comptime E: type) type {
if (obj.internalFieldCount() == 0) {
return error.InvalidArgument;
}
return E.typeTaggedAnyOpaque(named_function, *Receiver(ptr.child), obj.getInternalField(0).castTo(v8.External).get());
return E.typeTaggedAnyOpaque(named_function, *Receiver(ptr.child), obj);
}
},
.slice => {
@@ -2271,3 +2292,8 @@ fn getTaggedAnyOpaque(value: v8.Value) ?*TaggedAnyOpaque {
const external_data = obj.getInternalField(0).castTo(v8.External).get().?;
return @alignCast(@ptrCast(external_data));
}
test {
std.testing.refAllDecls(@import("test_primitive_types.zig"));
std.testing.refAllDecls(@import("test_complex_types.zig"));
}

View File

@@ -0,0 +1,256 @@
// Copyright 2023-2024 Lightpanda (Selecy SAS)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
const std = @import("std");
const Allocator = std.mem.Allocator;
const MyList = struct {
items: []u8,
pub fn constructor(state: State, elem1: u8, elem2: u8, elem3: u8) MyList {
var items = state.arena.alloc(u8, 3) catch unreachable;
items[0] = elem1;
items[1] = elem2;
items[2] = elem3;
return .{ .items = items };
}
pub fn _first(self: *const MyList) u8 {
return self.items[0];
}
pub fn _symbol_iterator(self: *const MyList) IterableU8 {
return IterableU8.init(self.items);
}
};
const MyVariadic = struct {
member: u8,
pub fn constructor() MyVariadic {
return .{ .member = 0 };
}
pub fn _len(_: *const MyVariadic, variadic: []bool) u64 {
return @as(u64, variadic.len);
}
pub fn _first(_: *const MyVariadic, _: []const u8, variadic: []bool) bool {
return variadic[0];
}
pub fn _last(_: *const MyVariadic, variadic: []bool) bool {
return variadic[variadic.len - 1];
}
pub fn _empty(_: *const MyVariadic, _: []bool) bool {
return true;
}
pub fn _myListLen(_: *const MyVariadic, variadic: []*const MyList) u8 {
return @as(u8, @intCast(variadic.len));
}
pub fn _myListFirst(_: *const MyVariadic, variadic: []*const MyList) ?u8 {
if (variadic.len == 0) return null;
return variadic[0]._first();
}
};
const MyErrorUnion = struct {
pub fn constructor(is_err: bool) !MyErrorUnion {
if (is_err) return error.MyError;
return .{};
}
pub fn get_withoutError(_: *const MyErrorUnion) !u8 {
return 0;
}
pub fn get_withError(_: *const MyErrorUnion) !u8 {
return error.MyError;
}
pub fn set_withoutError(_: *const MyErrorUnion, _: bool) !void {}
pub fn set_withError(_: *const MyErrorUnion, _: bool) !void {
return error.MyError;
}
pub fn _funcWithoutError(_: *const MyErrorUnion) !void {}
pub fn _funcWithError(_: *const MyErrorUnion) !void {
return error.MyError;
}
};
pub const MyException = struct {
err: ErrorSet,
const errorNames = [_][]const u8{
"MyCustomError",
};
const errorMsgs = [_][]const u8{
"Some custom message.",
};
fn errorStrings(comptime i: usize) []const u8 {
return errorNames[0] ++ ": " ++ errorMsgs[i];
}
// interface definition
pub const ErrorSet = error{
MyCustomError,
};
pub fn init(_: Allocator, err: anyerror, _: []const u8) !MyException {
return .{ .err = @as(ErrorSet, @errorCast(err)) };
}
pub fn get_name(self: *const MyException) []const u8 {
return switch (self.err) {
ErrorSet.MyCustomError => errorNames[0],
};
}
pub fn get_message(self: *const MyException) []const u8 {
return switch (self.err) {
ErrorSet.MyCustomError => errorMsgs[0],
};
}
pub fn _toString(self: *const MyException) []const u8 {
return switch (self.err) {
ErrorSet.MyCustomError => errorStrings(0),
};
}
};
const MyTypeWithException = struct {
pub const Exception = MyException;
pub fn constructor() MyTypeWithException {
return .{};
}
pub fn _withoutError(_: *const MyTypeWithException) MyException.ErrorSet!void {}
pub fn _withError(_: *const MyTypeWithException) MyException.ErrorSet!void {
return MyException.ErrorSet.MyCustomError;
}
pub fn _superSetError(_: *const MyTypeWithException) !void {
return MyException.ErrorSet.MyCustomError;
}
pub fn _outOfMemory(_: *const MyTypeWithException) !void {
return error.OutOfMemory;
}
};
const IterableU8 = Iterable(u8);
pub fn Iterable(comptime T: type) type {
return struct {
const Self = @This();
items: []T,
index: usize = 0,
pub fn init(items: []T) Self {
return .{ .items = items };
}
pub const Return = struct {
value: ?T,
done: bool,
};
pub fn _next(self: *Self) Return {
if (self.items.len > self.index) {
const val = self.items[self.index];
self.index += 1;
return .{ .value = val, .done = false };
} else {
return .{ .value = null, .done = true };
}
}
};
}
const State = struct {
arena: Allocator,
};
const testing = @import("testing.zig");
test "JS: complex types" {
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
var runner = try testing.Runner(State, void, .{
MyList,
IterableU8,
MyVariadic,
MyErrorUnion,
MyException,
MyTypeWithException,
}).init(.{ .arena = arena.allocator() }, {});
defer runner.deinit();
try runner.testCases(&.{
.{ "let myList = new MyList(1, 2, 3);", "undefined" },
.{ "myList.first();", "1" },
.{ "let iter = myList[Symbol.iterator]();", "undefined" },
.{ "iter.next().value;", "1" },
.{ "iter.next().value;", "2" },
.{ "iter.next().value;", "3" },
.{ "iter.next().done;", "true" },
.{ "let arr = Array.from(myList);", "undefined" },
.{ "arr.length;", "3" },
.{ "arr[0];", "1" },
}, .{});
try runner.testCases(&.{
.{ "let myVariadic = new MyVariadic();", "undefined" },
.{ "myVariadic.len(true, false, true)", "3" },
.{ "myVariadic.first('a_str', true, false, true, false)", "true" },
.{ "myVariadic.last(true, false)", "false" },
.{ "myVariadic.empty()", "true" },
.{ "myVariadic.myListLen(myList)", "1" },
.{ "myVariadic.myListFirst(myList)", "1" },
}, .{});
try runner.testCases(&.{
.{ "var myErrorCstr = ''; try {new MyErrorUnion(true)} catch (error) {myErrorCstr = error}; myErrorCstr", "Error: MyError" },
.{ "let myErrorUnion = new MyErrorUnion(false);", "undefined" },
.{ "myErrorUnion.withoutError", "0" },
.{ "var myErrorGetter = ''; try {myErrorUnion.withError} catch (error) {myErrorGetter = error}; myErrorGetter", "Error: MyError" },
.{ "myErrorUnion.withoutError = true", "true" },
.{ "var myErrorSetter = ''; try {myErrorUnion.withError = true} catch (error) {myErrorSetter = error}; myErrorSetter", "Error: MyError" },
.{ "myErrorUnion.funcWithoutError()", "undefined" },
.{ "var myErrorFunc = ''; try {myErrorUnion.funcWithError()} catch (error) {myErrorFunc = error}; myErrorFunc", "Error: MyError" },
}, .{});
try runner.testCases(&.{
.{ "MyException.prototype.__proto__ === Error.prototype", "true" },
.{ "let myTypeWithException = new MyTypeWithException();", "undefined" },
.{ "myTypeWithException.withoutError()", "undefined" },
.{ "var myCustomError = ''; try {myTypeWithException.withError()} catch (error) {myCustomError = error}", "MyCustomError: Some custom message." },
.{ "myCustomError instanceof MyException", "true" },
.{ "myCustomError instanceof Error", "true" },
.{ "var mySuperError = ''; try {myTypeWithException.superSetError()} catch (error) {mySuperError = error}", "MyCustomError: Some custom message." },
.{ "var oomError = ''; try {myTypeWithException.outOfMemory()} catch (error) {oomError = error}; oomError", "Error: out of memory" },
}, .{});
}

View File

@@ -0,0 +1,158 @@
const std = @import("std");
// TODO: use functions instead of "fake" struct once we handle function API generation
const Primitives = struct {
pub fn constructor() Primitives {
return .{};
}
// List of bytes (string)
pub fn _checkString(_: *const Primitives, v: []u8) []u8 {
return v;
}
// Integers signed
pub fn _checkI32(_: *const Primitives, v: i32) i32 {
return v;
}
pub fn _checkI64(_: *const Primitives, v: i64) i64 {
return v;
}
// Integers unsigned
pub fn _checkU32(_: *const Primitives, v: u32) u32 {
return v;
}
pub fn _checkU64(_: *const Primitives, v: u64) u64 {
return v;
}
// Floats
pub fn _checkF32(_: *const Primitives, v: f32) f32 {
return v;
}
pub fn _checkF64(_: *const Primitives, v: f64) f64 {
return v;
}
// Bool
pub fn _checkBool(_: *const Primitives, v: bool) bool {
return v;
}
// Undefined
// TODO: there is a bug with this function
// void paramater does not work => avoid for now
// pub fn _checkUndefined(_: *const Primitives, v: void) void {
// return v;
// }
// Null
pub fn _checkNullEmpty(_: *const Primitives, v: ?u32) bool {
return (v == null);
}
pub fn _checkNullNotEmpty(_: *const Primitives, v: ?u32) bool {
return (v != null);
}
// Optionals
pub fn _checkOptional(_: *const Primitives, _: ?u8, v: u8, _: ?u8, _: ?u8) u8 {
return v;
}
pub fn _checkNonOptional(_: *const Primitives, v: u8) u8 {
std.debug.print("x: {d}\n", .{v});
return v;
}
pub fn _checkOptionalReturn(_: *const Primitives) ?bool {
return true;
}
pub fn _checkOptionalReturnNull(_: *const Primitives) ?bool {
return null;
}
pub fn _checkOptionalReturnString(_: *const Primitives) ?[]const u8 {
return "ok";
}
};
const testing = @import("testing.zig");
test "JS: primitive types" {
var runner = try testing.Runner(void, void, .{Primitives}).init({}, {});
defer runner.deinit();
// constructor
try runner.testCases(&.{
.{ "let p = new Primitives();", "undefined" },
}, .{});
// JS <> Native translation of primitive types
try runner.testCases(&.{
.{ "p.checkString('ok ascii') === 'ok ascii';", "true" },
.{ "p.checkString('ok emoji 🚀') === 'ok emoji 🚀';", "true" },
.{ "p.checkString('ok chinese 鿍') === 'ok chinese 鿍';", "true" },
// String (JS liberal cases)
.{ "p.checkString(1) === '1';", "true" },
.{ "p.checkString(null) === 'null';", "true" },
.{ "p.checkString(undefined) === 'undefined';", "true" },
// Integers
// signed
.{ "const min_i32 = -2147483648", "undefined" },
.{ "p.checkI32(min_i32) === min_i32;", "true" },
.{ "p.checkI32(min_i32-1) === min_i32-1;", "false" },
.{ "try { p.checkI32(9007199254740995n) } catch(e) { e instanceof TypeError; }", "true" },
// unsigned
.{ "const max_u32 = 4294967295", "undefined" },
.{ "p.checkU32(max_u32) === max_u32;", "true" },
.{ "p.checkU32(max_u32+1) === max_u32+1;", "false" },
// int64 (with BigInt)
.{ "const big_int = 9007199254740995n", "undefined" },
.{ "p.checkI64(big_int) === big_int", "true" },
.{ "p.checkU64(big_int) === big_int;", "true" },
.{ "p.checkI64(0) === 0;", "true" },
.{ "p.checkI64(-1) === -1;", "true" },
.{ "p.checkU64(0) === 0;", "true" },
// Floats
// use round 2 decimals for float to ensure equality
.{ "const r = function(x) {return Math.round(x * 100) / 100};", "undefined" },
.{ "const double = 10.02;", "undefined" },
.{ "r(p.checkF32(double)) === double;", "true" },
.{ "r(p.checkF64(double)) === double;", "true" },
// Bool
.{ "p.checkBool(true);", "true" },
.{ "p.checkBool(false);", "false" },
.{ "p.checkBool(0);", "false" },
.{ "p.checkBool(1);", "true" },
// Bool (JS liberal cases)
.{ "p.checkBool(null);", "false" },
.{ "p.checkBool(undefined);", "false" },
// Undefined
// see TODO on Primitives.checkUndefined
// .{ "p.checkUndefined(undefined) === undefined;", "true" },
// Null
.{ "p.checkNullEmpty(null);", "true" },
.{ "p.checkNullEmpty(undefined);", "true" },
.{ "p.checkNullNotEmpty(1);", "true" },
// Optional
.{ "p.checkOptional(null, 3);", "3" },
.{ "p.checkNonOptional();", "TypeError" },
.{ "p.checkOptionalReturn() === true;", "true" },
.{ "p.checkOptionalReturnNull() === null;", "true" },
.{ "p.checkOptionalReturnString() === 'ok';", "true" },
}, .{});
}

86
src/runtime/testing.zig Normal file
View File

@@ -0,0 +1,86 @@
const std = @import("std");
const js = @import("js.zig");
const generate = @import("generate.zig");
pub const allocator = std.testing.allocator;
// Very similar to the JSRunner in src/testing.zig, but it isn't tied to the
// browser.Env or the browser.SessionState
pub fn Runner(comptime State: type, comptime Global: type, comptime types: anytype) type {
const AdjustedTypes = if (Global == void) generate.Tuple(.{ types, DefaultGlobal }) else types;
const Env = js.Env(State, AdjustedTypes{});
return struct {
env: *Env,
executor: *Env.Executor,
const Self = @This();
pub fn init(state: State, global: Global) !*Self {
const runner = try allocator.create(Self);
errdefer allocator.destroy(runner);
runner.env = try Env.init(allocator, .{});
errdefer runner.env.deinit();
const G = if (Global == void) DefaultGlobal else Global;
runner.executor = try runner.env.startExecutor(G, state, runner);
errdefer runner.env.stopExecutor(runner.executor);
try runner.executor.startScope(if (Global == void) &default_global else global);
return runner;
}
pub fn deinit(self: *Self) void {
self.executor.endScope();
self.env.stopExecutor(self.executor);
self.env.deinit();
allocator.destroy(self);
}
const RunOpts = struct {};
pub const Case = std.meta.Tuple(&.{ []const u8, []const u8 });
pub fn testCases(self: *Self, cases: []const Case, _: RunOpts) !void {
for (cases, 0..) |case, i| {
var try_catch: Env.TryCatch = undefined;
try_catch.init(self.executor);
defer try_catch.deinit();
const value = self.executor.exec(case.@"0", null) catch |err| {
if (try try_catch.err(allocator)) |msg| {
defer allocator.free(msg);
if (isExpectedTypeError(case.@"1", msg)) {
continue;
}
std.debug.print("{s}\n\nCase: {d}\n{s}\n", .{ msg, i + 1, case.@"0" });
}
return err;
};
const actual = try value.toString(allocator);
defer allocator.free(actual);
if (std.mem.eql(u8, case.@"1", actual) == false) {
std.debug.print("Expected:\n{s}\n\nGot:\n{s}\n\nCase: {d}\n{s}\n", .{ case.@"1", actual, i + 1, case.@"0" });
return error.UnexpectedResult;
}
}
}
pub fn fetchModuleSource(ctx: *anyopaque, specifier: []const u8) ![]const u8 {
_ = ctx;
_ = specifier;
return error.DummyModuleLoader;
}
};
}
fn isExpectedTypeError(expected: []const u8, msg: []const u8) bool {
if (!std.mem.eql(u8, expected, "TypeError")) {
return false;
}
return std.mem.startsWith(u8, msg, "TypeError: ");
}
var default_global = DefaultGlobal{};
const DefaultGlobal = struct {};