diff --git a/src/runtime/js.zig b/src/runtime/js.zig index 8e947861..87d4cb48 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -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")); +} diff --git a/src/runtime/test_complex_types.zig b/src/runtime/test_complex_types.zig new file mode 100644 index 00000000..1173c027 --- /dev/null +++ b/src/runtime/test_complex_types.zig @@ -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" }, + }, .{}); +} diff --git a/src/runtime/test_primitive_types.zig b/src/runtime/test_primitive_types.zig new file mode 100644 index 00000000..178a1d79 --- /dev/null +++ b/src/runtime/test_primitive_types.zig @@ -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" }, + }, .{}); +} diff --git a/src/runtime/testing.zig b/src/runtime/testing.zig new file mode 100644 index 00000000..c19632ff --- /dev/null +++ b/src/runtime/testing.zig @@ -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 {};