From 1e56d28ada2e95c049c732db43712c293c41850d Mon Sep 17 00:00:00 2001 From: "Nurul Huda (Apon)" Date: Thu, 12 Feb 2026 01:26:36 +0600 Subject: [PATCH] feat: add wasi target --- .gitignore | 4 +- .gitmodules | 3 + build.zig | 51 ++- example/src/wasm.zig | 826 ++++++++++++++++++++++++++++++++++++++ test/test262/duration.zig | 824 ------------------------------------- test/test262/polyfill.js | 683 +++++++++++++++++++++++++++++++ test/test262/root.zig | 3 - test/test262/runner.mjs | 35 ++ 8 files changed, 1575 insertions(+), 854 deletions(-) create mode 100644 .gitmodules delete mode 100644 test/test262/duration.zig create mode 100644 test/test262/polyfill.js delete mode 100644 test/test262/root.zig create mode 100644 test/test262/runner.mjs diff --git a/.gitignore b/.gitignore index 6f2b2ea..33ba287 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ lib !lib/*/libtemporal_capi.a !lib/*/temporal_capi.lib -tmp \ No newline at end of file +tmp + +test/test262/polyfill-injected.js \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..b283437 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "test/temporal-test262-runner"] + path = test/temporal-test262-runner + url = https://github.com/js-temporal/temporal-test262-runner.git diff --git a/build.zig b/build.zig index d2f08a4..7f24711 100644 --- a/build.zig +++ b/build.zig @@ -4,7 +4,7 @@ const build_crab = @import("build_crab"); pub fn build(b: *std.Build) !void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); - const is_wasm_freestanding = target.result.cpu.arch.isWasm(); + const is_wasm_freestanding = target.result.cpu.arch.isWasm() and target.result.os.tag == .freestanding; // --- Zig Module: temporalz --- // const mod = b.addModule("temporalz", .{ @@ -108,20 +108,31 @@ pub fn build(b: *std.Build) !void { }, }), }); + exe.root_module.link_libc = !is_wasm_freestanding; + exe.rdynamic = is_wasm_freestanding; b.installArtifact(exe); // --- Steps: Run --- // { const run_step = b.step("run", "Run the app"); - if (is_wasm_freestanding) { - const run_cmd = b.addSystemCommand(&.{ "node", "example/src/main.mjs" }); - run_cmd.step.dependOn(b.getInstallStep()); - run_step.dependOn(&run_cmd.step); - } else { - const run_cmd = b.addRunArtifact(exe); - run_cmd.step.dependOn(b.getInstallStep()); - if (b.args) |args| run_cmd.addArgs(args); - run_step.dependOn(&run_cmd.step); + switch (target.result.os.tag) { + .freestanding => { + const run_cmd = b.addSystemCommand(&.{ "node", "example/src/main.mjs" }); + run_cmd.step.dependOn(b.getInstallStep()); + run_step.dependOn(&run_cmd.step); + }, + .wasi => { + const run_cmd = b.addSystemCommand(&.{"wasmtime"}); + run_cmd.addFileArg(exe.getEmittedBin()); + run_cmd.step.dependOn(b.getInstallStep()); + run_step.dependOn(&run_cmd.step); + }, + else => { + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| run_cmd.addArgs(args); + run_step.dependOn(&run_cmd.step); + }, } } @@ -159,22 +170,10 @@ pub fn build(b: *std.Build) !void { // --- Steps: test-262 --- // { const test262_step = b.step("test262", "Run test-262 tests"); - const test262_tests = b.addTest(.{ - .root_module = b.createModule(.{ - .root_source_file = b.path("test/test262/root.zig"), - .target = target, - .optimize = optimize, - .imports = &.{ - .{ .name = "temporalz", .module = mod }, - }, - }), - .test_runner = .{ - .path = b.path("test/runner.zig"), - .mode = .simple, - }, - }); - test262_tests.root_module.link_libc = !is_wasm_freestanding; - test262_step.dependOn(&b.addRunArtifact(test262_tests).step); + const run_cmd = b.addSystemCommand(&.{ "node", "test/test262/runner.mjs" }); + if (b.args) |args| run_cmd.addArgs(args); + run_cmd.step.dependOn(b.getInstallStep()); + test262_step.dependOn(&run_cmd.step); } // --- Steps: Build all platforms --- // diff --git a/example/src/wasm.zig b/example/src/wasm.zig index 36a1420..02ca9b3 100644 --- a/example/src/wasm.zig +++ b/example/src/wasm.zig @@ -1,11 +1,837 @@ const std = @import("std"); const program = @import("root.zig"); +const Temporal = @import("temporalz"); export fn _start() void { const allocator = std.heap.wasm_allocator; program.run(allocator, null) catch {}; } +const wasm_allocator = std.heap.wasm_allocator; +const PolyfillError = error{InvalidHandle}; + +var instants_init = false; +var durations_init = false; +var instants: std.ArrayList(?Temporal.Instant) = .empty; +var durations: std.ArrayList(?Temporal.Duration) = .empty; +var plain_dates_init = false; +var plain_dates: std.ArrayList(?Temporal.PlainDate) = .empty; + +var last_error: ?[]u8 = null; + +fn ensureInstants() void { + if (!instants_init) { + instants = .empty; + instants_init = true; + } +} + +fn ensureDurations() void { + if (!durations_init) { + durations = .empty; + durations_init = true; + } +} + +fn ensurePlainDates() void { + if (!plain_dates_init) { + plain_dates = .empty; + plain_dates_init = true; + } +} + +fn addInstant(inst: Temporal.Instant) u32 { + ensureInstants(); + instants.append(wasm_allocator, inst) catch return 0; + return @intCast(instants.items.len); +} + +fn getInstant(handle: u32) !Temporal.Instant { + ensureInstants(); + if (handle == 0 or handle > instants.items.len) return PolyfillError.InvalidHandle; + return instants.items[handle - 1] orelse PolyfillError.InvalidHandle; +} + +fn removeInstant(handle: u32) void { + if (!instants_init or handle == 0 or handle > instants.items.len) return; + if (instants.items[handle - 1]) |inst| { + inst.deinit(); + instants.items[handle - 1] = null; + } +} + +fn addDuration(dur: Temporal.Duration) u32 { + ensureDurations(); + durations.append(wasm_allocator, dur) catch return 0; + return @intCast(durations.items.len); +} + +fn getDuration(handle: u32) !Temporal.Duration { + ensureDurations(); + if (handle == 0 or handle > durations.items.len) return PolyfillError.InvalidHandle; + return durations.items[handle - 1] orelse PolyfillError.InvalidHandle; +} + +fn removeDuration(handle: u32) void { + if (!durations_init or handle == 0 or handle > durations.items.len) return; + if (durations.items[handle - 1]) |dur| { + dur.deinit(); + durations.items[handle - 1] = null; + } +} + +fn addPlainDate(date: Temporal.PlainDate) u32 { + ensurePlainDates(); + plain_dates.append(wasm_allocator, date) catch return 0; + return @intCast(plain_dates.items.len); +} + +fn getPlainDate(handle: u32) !Temporal.PlainDate { + ensurePlainDates(); + if (handle == 0 or handle > plain_dates.items.len) return PolyfillError.InvalidHandle; + return plain_dates.items[handle - 1] orelse PolyfillError.InvalidHandle; +} + +fn removePlainDate(handle: u32) void { + if (!plain_dates_init or handle == 0 or handle > plain_dates.items.len) return; + if (plain_dates.items[handle - 1]) |date| { + date.deinit(); + plain_dates.items[handle - 1] = null; + } +} + +fn clearLastError() void { + if (last_error) |msg| { + wasm_allocator.free(msg); + last_error = null; + } +} + +fn setLastError(err: anyerror) void { + clearLastError(); + const msg = std.fmt.allocPrint(wasm_allocator, "{s}", .{@errorName(err)}) catch return; + last_error = msg; +} + +fn setLastErrorMessage(msg: []const u8) void { + clearLastError(); + const owned = wasm_allocator.alloc(u8, msg.len) catch return; + std.mem.copyForwards(u8, owned, msg); + last_error = owned; +} + +fn packPtrLen(ptr: [*]u8, len: usize) u64 { + const ptr_u32: u32 = @intCast(@intFromPtr(ptr)); + const len_u32: u32 = @intCast(len); + return (@as(u64, ptr_u32) << 32) | @as(u64, len_u32); +} + +fn unpackI128Hi(value: i128) i64 { + const bits: u128 = @bitCast(value); + const hi_bits: u64 = @intCast(bits >> 64); + return @bitCast(hi_bits); +} + +fn unpackI128Lo(value: i128) u64 { + const bits: u128 = @bitCast(value); + return @intCast(bits & 0xFFFFFFFFFFFFFFFF); +} + +fn joinI128(hi: i64, lo: u64) i128 { + const hi_bits: u64 = @bitCast(hi); + const value: u128 = (@as(u128, hi_bits) << 64) | @as(u128, lo); + return @bitCast(value); +} + +fn unitFromCode(code: u8) ?Temporal.Duration.Unit { + return switch (code) { + 1 => .nanosecond, + 2 => .microsecond, + 3 => .millisecond, + 4 => .second, + 5 => .minute, + 6 => .hour, + 7 => .day, + 8 => .week, + 9 => .month, + 10 => .year, + 11 => .auto, + else => null, + }; +} + +fn roundingModeFromCode(code: u8) ?Temporal.Duration.RoundingMode { + return switch (code) { + 1 => .ceil, + 2 => .floor, + 3 => .expand, + 4 => .trunc, + 5 => .half_ceil, + 6 => .half_floor, + 7 => .half_expand, + 8 => .half_trunc, + 9 => .half_even, + else => null, + }; +} + +export fn temporalz_last_error_ptr() usize { + return if (last_error) |msg| @intFromPtr(msg.ptr) else 0; +} + +export fn temporalz_last_error_len() usize { + return if (last_error) |msg| msg.len else 0; +} + +export fn temporalz_last_error_clear() void { + clearLastError(); +} + +export fn temporalz_alloc(len: usize) usize { + clearLastError(); + const buf = wasm_allocator.alloc(u8, len) catch |err| { + setLastError(err); + return 0; + }; + return @intFromPtr(buf.ptr); +} + +export fn temporalz_free(ptr: usize, len: usize) void { + if (ptr == 0 or len == 0) return; + const slice = @as([*]u8, @ptrFromInt(ptr))[0..len]; + wasm_allocator.free(slice); +} + +export fn temporalz_string_free(ptr: usize, len: usize) void { + temporalz_free(ptr, len); +} + +export fn temporalz_instant_from_utf8(ptr: [*]const u8, len: usize) u32 { + clearLastError(); + const text = ptr[0..len]; + const inst = Temporal.Instant.from(text) catch |err| { + setLastError(err); + return 0; + }; + return addInstant(inst); +} + +export fn temporalz_instant_from_epoch_milliseconds(epoch_ms: i64) u32 { + clearLastError(); + const inst = Temporal.Instant.fromEpochMilliseconds(epoch_ms) catch |err| { + setLastError(err); + return 0; + }; + return addInstant(inst); +} + +export fn temporalz_instant_from_epoch_nanoseconds_parts(hi: i64, lo: u64) u32 { + clearLastError(); + const epoch_ns = joinI128(hi, lo); + const inst = Temporal.Instant.fromEpochNanoseconds(epoch_ns) catch |err| { + setLastError(err); + return 0; + }; + return addInstant(inst); +} + +export fn temporalz_instant_epoch_milliseconds(handle: u32) i64 { + clearLastError(); + const inst = getInstant(handle) catch |err| { + setLastError(err); + return 0; + }; + return inst.epochMilliseconds(); +} + +export fn temporalz_instant_epoch_nanoseconds_hi(handle: u32) i64 { + clearLastError(); + const inst = getInstant(handle) catch |err| { + setLastError(err); + return 0; + }; + return unpackI128Hi(inst.epochNanoseconds()); +} + +export fn temporalz_instant_epoch_nanoseconds_lo(handle: u32) u64 { + clearLastError(); + const inst = getInstant(handle) catch |err| { + setLastError(err); + return 0; + }; + return unpackI128Lo(inst.epochNanoseconds()); +} + +export fn temporalz_instant_to_string(handle: u32) u64 { + clearLastError(); + const inst = getInstant(handle) catch |err| { + setLastError(err); + return 0; + }; + const text = inst.toString(wasm_allocator, .{}) catch |err| { + setLastError(err); + return 0; + }; + return packPtrLen(text.ptr, text.len); +} + +export fn temporalz_instant_add(handle: u32, duration_handle: u32) u32 { + clearLastError(); + const inst = getInstant(handle) catch |err| { + setLastError(err); + return 0; + }; + var dur = getDuration(duration_handle) catch |err| { + setLastError(err); + return 0; + }; + const res = inst.add(&dur) catch |err| { + setLastError(err); + return 0; + }; + return addInstant(res); +} + +export fn temporalz_instant_subtract(handle: u32, duration_handle: u32) u32 { + clearLastError(); + const inst = getInstant(handle) catch |err| { + setLastError(err); + return 0; + }; + var dur = getDuration(duration_handle) catch |err| { + setLastError(err); + return 0; + }; + const res = inst.subtract(&dur) catch |err| { + setLastError(err); + return 0; + }; + return addInstant(res); +} + +export fn temporalz_instant_compare(handle_a: u32, handle_b: u32) i32 { + clearLastError(); + const a = getInstant(handle_a) catch |err| { + setLastError(err); + return 0; + }; + const b = getInstant(handle_b) catch |err| { + setLastError(err); + return 0; + }; + return @intCast(Temporal.Instant.compare(a, b)); +} + +export fn temporalz_instant_equals(handle_a: u32, handle_b: u32) u8 { + clearLastError(); + const a = getInstant(handle_a) catch |err| { + setLastError(err); + return 0; + }; + const b = getInstant(handle_b) catch |err| { + setLastError(err); + return 0; + }; + return if (Temporal.Instant.equals(a, b)) 1 else 0; +} + +export fn temporalz_instant_round( + handle: u32, + smallest_unit: u8, + rounding_mode: u8, + rounding_increment: u32, +) u32 { + clearLastError(); + const inst = getInstant(handle) catch |err| { + setLastError(err); + return 0; + }; + + var opts = Temporal.Instant.RoundingOptions{}; + if (smallest_unit != 255) { + opts.smallest_unit = unitFromCode(smallest_unit) orelse { + setLastErrorMessage("Invalid smallestUnit"); + return 0; + }; + } + if (rounding_mode != 255) { + opts.rounding_mode = roundingModeFromCode(rounding_mode) orelse { + setLastErrorMessage("Invalid roundingMode"); + return 0; + }; + } + if (rounding_increment != 0) { + opts.rounding_increment = rounding_increment; + } + + const res = inst.round(opts) catch |err| { + setLastError(err); + return 0; + }; + return addInstant(res); +} + +export fn temporalz_instant_destroy(handle: u32) void { + removeInstant(handle); +} + +export fn temporalz_duration_from_utf8(ptr: [*]const u8, len: usize) u32 { + clearLastError(); + const text = ptr[0..len]; + const dur = Temporal.Duration.from(text) catch |err| { + setLastError(err); + return 0; + }; + return addDuration(dur); +} + +export fn temporalz_duration_from_parts( + mask: u32, + years: i64, + months: i64, + weeks: i64, + days: i64, + hours: i64, + minutes: i64, + seconds: i64, + milliseconds: i64, + microseconds: f64, + nanoseconds: f64, +) u32 { + clearLastError(); + var partial = Temporal.Duration.PartialDuration{}; + if ((mask & 0x1) != 0) partial.years = years; + if ((mask & 0x2) != 0) partial.months = months; + if ((mask & 0x4) != 0) partial.weeks = weeks; + if ((mask & 0x8) != 0) partial.days = days; + if ((mask & 0x10) != 0) partial.hours = hours; + if ((mask & 0x20) != 0) partial.minutes = minutes; + if ((mask & 0x40) != 0) partial.seconds = seconds; + if ((mask & 0x80) != 0) partial.milliseconds = milliseconds; + if ((mask & 0x100) != 0) partial.microseconds = microseconds; + if ((mask & 0x200) != 0) partial.nanoseconds = nanoseconds; + + const dur = Temporal.Duration.from(partial) catch |err| { + setLastError(err); + return 0; + }; + return addDuration(dur); +} + +export fn temporalz_duration_init( + years: i64, + months: i64, + weeks: i64, + days: i64, + hours: i64, + minutes: i64, + seconds: i64, + milliseconds: i64, + microseconds: f64, + nanoseconds: f64, +) u32 { + clearLastError(); + const dur = Temporal.Duration.init( + years, + months, + weeks, + days, + hours, + minutes, + seconds, + milliseconds, + microseconds, + nanoseconds, + ) catch |err| { + setLastError(err); + return 0; + }; + return addDuration(dur); +} + +export fn temporalz_duration_to_string(handle: u32) u64 { + clearLastError(); + const dur = getDuration(handle) catch |err| { + setLastError(err); + return 0; + }; + const text = dur.toString(wasm_allocator, .{}) catch |err| { + setLastError(err); + return 0; + }; + return packPtrLen(text.ptr, text.len); +} + +export fn temporalz_duration_years(handle: u32) i64 { + clearLastError(); + const dur = getDuration(handle) catch |err| { + setLastError(err); + return 0; + }; + return dur.years(); +} + +export fn temporalz_duration_months(handle: u32) i64 { + clearLastError(); + const dur = getDuration(handle) catch |err| { + setLastError(err); + return 0; + }; + return dur.months(); +} + +export fn temporalz_duration_weeks(handle: u32) i64 { + clearLastError(); + const dur = getDuration(handle) catch |err| { + setLastError(err); + return 0; + }; + return dur.weeks(); +} + +export fn temporalz_duration_days(handle: u32) i64 { + clearLastError(); + const dur = getDuration(handle) catch |err| { + setLastError(err); + return 0; + }; + return dur.days(); +} + +export fn temporalz_duration_hours(handle: u32) i64 { + clearLastError(); + const dur = getDuration(handle) catch |err| { + setLastError(err); + return 0; + }; + return dur.hours(); +} + +export fn temporalz_duration_minutes(handle: u32) i64 { + clearLastError(); + const dur = getDuration(handle) catch |err| { + setLastError(err); + return 0; + }; + return dur.minutes(); +} + +export fn temporalz_duration_seconds(handle: u32) i64 { + clearLastError(); + const dur = getDuration(handle) catch |err| { + setLastError(err); + return 0; + }; + return dur.seconds(); +} + +export fn temporalz_duration_milliseconds(handle: u32) i64 { + clearLastError(); + const dur = getDuration(handle) catch |err| { + setLastError(err); + return 0; + }; + return dur.milliseconds(); +} + +export fn temporalz_duration_microseconds(handle: u32) f64 { + clearLastError(); + const dur = getDuration(handle) catch |err| { + setLastError(err); + return 0; + }; + return dur.microseconds(); +} + +export fn temporalz_duration_nanoseconds(handle: u32) f64 { + clearLastError(); + const dur = getDuration(handle) catch |err| { + setLastError(err); + return 0; + }; + return dur.nanoseconds(); +} + +export fn temporalz_duration_sign(handle: u32) i32 { + clearLastError(); + const dur = getDuration(handle) catch |err| { + setLastError(err); + return 0; + }; + return switch (dur.sign()) { + .positive => 1, + .zero => 0, + .negative => -1, + }; +} + +export fn temporalz_duration_blank(handle: u32) u8 { + clearLastError(); + const dur = getDuration(handle) catch |err| { + setLastError(err); + return 0; + }; + return if (dur.blank()) 1 else 0; +} + +export fn temporalz_duration_abs(handle: u32) u32 { + clearLastError(); + const dur = getDuration(handle) catch |err| { + setLastError(err); + return 0; + }; + return addDuration(dur.abs()); +} + +export fn temporalz_duration_negated(handle: u32) u32 { + clearLastError(); + const dur = getDuration(handle) catch |err| { + setLastError(err); + return 0; + }; + return addDuration(dur.negated()); +} + +export fn temporalz_duration_add(handle_a: u32, handle_b: u32) u32 { + clearLastError(); + const a = getDuration(handle_a) catch |err| { + setLastError(err); + return 0; + }; + const b = getDuration(handle_b) catch |err| { + setLastError(err); + return 0; + }; + const res = a.add(b) catch |err| { + setLastError(err); + return 0; + }; + return addDuration(res); +} + +export fn temporalz_duration_subtract(handle_a: u32, handle_b: u32) u32 { + clearLastError(); + const a = getDuration(handle_a) catch |err| { + setLastError(err); + return 0; + }; + const b = getDuration(handle_b) catch |err| { + setLastError(err); + return 0; + }; + const res = a.subtract(b) catch |err| { + setLastError(err); + return 0; + }; + return addDuration(res); +} + +export fn temporalz_duration_compare(handle_a: u32, handle_b: u32) i32 { + clearLastError(); + const a = getDuration(handle_a) catch |err| { + setLastError(err); + return 0; + }; + const b = getDuration(handle_b) catch |err| { + setLastError(err); + return 0; + }; + const res = a.compare(b, .{}) catch |err| { + setLastError(err); + return 0; + }; + return @intCast(res); +} + +export fn temporalz_duration_total(handle: u32, unit_code: u8) f64 { + clearLastError(); + const unit = unitFromCode(unit_code) orelse { + setLastErrorMessage("Invalid unit"); + return 0; + }; + const dur = getDuration(handle) catch |err| { + setLastError(err); + return 0; + }; + return dur.total(.{ .unit = unit }) catch |err| { + setLastError(err); + return 0; + }; +} + +export fn temporalz_duration_round( + handle: u32, + smallest_unit: u8, + largest_unit: u8, + rounding_mode: u8, + rounding_increment: u32, +) u32 { + clearLastError(); + const dur = getDuration(handle) catch |err| { + setLastError(err); + return 0; + }; + + var opts = Temporal.Duration.RoundingOptions{}; + if (smallest_unit != 255) { + opts.smallest_unit = unitFromCode(smallest_unit) orelse { + setLastErrorMessage("Invalid smallestUnit"); + return 0; + }; + } + if (largest_unit != 255) { + opts.largest_unit = unitFromCode(largest_unit) orelse { + setLastErrorMessage("Invalid largestUnit"); + return 0; + }; + } + if (rounding_mode != 255) { + opts.rounding_mode = roundingModeFromCode(rounding_mode) orelse { + setLastErrorMessage("Invalid roundingMode"); + return 0; + }; + } + if (rounding_increment != 0) { + opts.rounding_increment = rounding_increment; + } + + const res = dur.round(opts) catch |err| { + setLastError(err); + return 0; + }; + return addDuration(res); +} + +export fn temporalz_duration_compare_plain_date(handle_a: u32, handle_b: u32, date_handle: u32) i32 { + clearLastError(); + const a = getDuration(handle_a) catch |err| { + setLastError(err); + return 0; + }; + const b = getDuration(handle_b) catch |err| { + setLastError(err); + return 0; + }; + const date = getPlainDate(date_handle) catch |err| { + setLastError(err); + return 0; + }; + const res = a.compare(b, .{ .relative_to = .{ .plain_date = date } }) catch |err| { + setLastError(err); + return 0; + }; + return @intCast(res); +} + +export fn temporalz_duration_total_plain_date(handle: u32, unit_code: u8, date_handle: u32) f64 { + clearLastError(); + const unit = unitFromCode(unit_code) orelse { + setLastErrorMessage("Invalid unit"); + return 0; + }; + const dur = getDuration(handle) catch |err| { + setLastError(err); + return 0; + }; + const date = getPlainDate(date_handle) catch |err| { + setLastError(err); + return 0; + }; + return dur.total(.{ .unit = unit, .relative_to = .{ .plain_date = date } }) catch |err| { + setLastError(err); + return 0; + }; +} + +export fn temporalz_duration_round_plain_date( + handle: u32, + smallest_unit: u8, + largest_unit: u8, + rounding_mode: u8, + rounding_increment: u32, + date_handle: u32, +) u32 { + clearLastError(); + const dur = getDuration(handle) catch |err| { + setLastError(err); + return 0; + }; + const date = getPlainDate(date_handle) catch |err| { + setLastError(err); + return 0; + }; + + var opts = Temporal.Duration.RoundingOptions{}; + if (smallest_unit != 255) { + opts.smallest_unit = unitFromCode(smallest_unit) orelse { + setLastErrorMessage("Invalid smallestUnit"); + return 0; + }; + } + if (largest_unit != 255) { + opts.largest_unit = unitFromCode(largest_unit) orelse { + setLastErrorMessage("Invalid largestUnit"); + return 0; + }; + } + if (rounding_mode != 255) { + opts.rounding_mode = roundingModeFromCode(rounding_mode) orelse { + setLastErrorMessage("Invalid roundingMode"); + return 0; + }; + } + if (rounding_increment != 0) { + opts.rounding_increment = rounding_increment; + } + opts.relative_to = .{ .plain_date = date }; + + const res = dur.round(opts) catch |err| { + setLastError(err); + return 0; + }; + return addDuration(res); +} + +export fn temporalz_plain_date_from_utf8(ptr: [*]const u8, len: usize) u32 { + clearLastError(); + const text = ptr[0..len]; + const date = Temporal.PlainDate.from(text) catch |err| { + setLastError(err); + return 0; + }; + return addPlainDate(date); +} + +export fn temporalz_plain_date_init(year: i32, month: u8, day: u8) u32 { + clearLastError(); + const date = Temporal.PlainDate.init(year, month, day) catch |err| { + setLastError(err); + return 0; + }; + return addPlainDate(date); +} + +export fn temporalz_plain_date_to_string(handle: u32) u64 { + clearLastError(); + const date = getPlainDate(handle) catch |err| { + setLastError(err); + return 0; + }; + const text = date.toString(wasm_allocator, .{}) catch |err| { + setLastError(err); + return 0; + }; + return packPtrLen(text.ptr, text.len); +} + +export fn temporalz_plain_date_destroy(handle: u32) void { + removePlainDate(handle); +} + +export fn temporalz_duration_destroy(handle: u32) void { + removeDuration(handle); +} + extern fn console(ptr: [*]u8, len: u32) void; fn logFn(comptime _: anytype, comptime _: anytype, comptime format: []const u8, args: anytype) void { diff --git a/test/test262/duration.zig b/test/test262/duration.zig deleted file mode 100644 index 6f5a405..0000000 --- a/test/test262/duration.zig +++ /dev/null @@ -1,824 +0,0 @@ -const std = @import("std"); -const Temporal = @import("temporalz"); - -// Mirrors test262 test/built-ins/Temporal/Duration/basic.js (constructor fields and sign) -test "Temporal.Duration constructor components" { - const pos = try Temporal.Duration.init(5, 5, 5, 5, 5, 5, 5, 5, 5, 0); - defer pos.deinit(); - try std.testing.expectEqual(@as(i64, 5), pos.years()); - try std.testing.expectEqual(@as(i64, 5), pos.months()); - try std.testing.expectEqual(@as(i64, 5), pos.weeks()); - try std.testing.expectEqual(@as(i64, 5), pos.days()); - try std.testing.expectEqual(@as(i64, 5), pos.hours()); - try std.testing.expectEqual(@as(i64, 5), pos.minutes()); - try std.testing.expectEqual(@as(i64, 5), pos.seconds()); - try std.testing.expectEqual(@as(i64, 5), pos.milliseconds()); - try std.testing.expectEqual(@as(f64, 5), pos.microseconds()); - try std.testing.expectEqual(@as(f64, 0), pos.nanoseconds()); - try std.testing.expectEqual(Temporal.Duration.Sign.positive, pos.sign()); - - const neg = try Temporal.Duration.init(-5, -5, -5, -5, -5, -5, -5, -5, -5, 0); - defer neg.deinit(); - try std.testing.expectEqual(@as(i64, -5), neg.years()); - try std.testing.expectEqual(@as(i64, -5), neg.months()); - try std.testing.expectEqual(@as(i64, -5), neg.weeks()); - try std.testing.expectEqual(@as(i64, -5), neg.days()); - try std.testing.expectEqual(@as(i64, -5), neg.hours()); - try std.testing.expectEqual(@as(i64, -5), neg.minutes()); - try std.testing.expectEqual(@as(i64, -5), neg.seconds()); - try std.testing.expectEqual(@as(i64, -5), neg.milliseconds()); - try std.testing.expectEqual(@as(f64, -5), neg.microseconds()); - try std.testing.expectEqual(@as(f64, 0), neg.nanoseconds()); - try std.testing.expectEqual(Temporal.Duration.Sign.negative, neg.sign()); - - const neg_zero = try Temporal.Duration.init(0, 0, 0, 0, 0, 0, 0, 0, 0.0, 0.0); - defer neg_zero.deinit(); - try std.testing.expectEqual(@as(i64, 0), neg_zero.years()); - try std.testing.expectEqual(@as(i64, 0), neg_zero.months()); - try std.testing.expectEqual(@as(i64, 0), neg_zero.weeks()); - try std.testing.expectEqual(@as(i64, 0), neg_zero.days()); - try std.testing.expectEqual(@as(i64, 0), neg_zero.hours()); - try std.testing.expectEqual(@as(i64, 0), neg_zero.minutes()); - try std.testing.expectEqual(@as(i64, 0), neg_zero.seconds()); - try std.testing.expectEqual(@as(i64, 0), neg_zero.milliseconds()); - try std.testing.expectEqual(@as(f64, 0), neg_zero.microseconds()); - try std.testing.expectEqual(@as(f64, 0), neg_zero.nanoseconds()); - try std.testing.expectEqual(Temporal.Duration.Sign.zero, neg_zero.sign()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/compare/basic.js -test "Temporal.Duration compare time-only" { - const td1 = try Temporal.Duration.init(0, 0, 0, 0, 5, 5, 5, 5, 5, 5); - defer td1.deinit(); - const td2 = try Temporal.Duration.init(0, 0, 0, 0, 5, 4, 5, 5, 5, 5); - defer td2.deinit(); - const td1_neg = try Temporal.Duration.init(0, 0, 0, 0, -5, -5, -5, -5, -5, -5); - defer td1_neg.deinit(); - const td2_neg = try Temporal.Duration.init(0, 0, 0, 0, -5, -4, -5, -5, -5, -5); - defer td2_neg.deinit(); - - try std.testing.expectEqual(@as(i8, 0), try td1.compare(td1, .{})); - try std.testing.expectEqual(@as(i8, -1), try td2.compare(td1, .{})); - try std.testing.expectEqual(@as(i8, 1), try td1.compare(td2, .{})); - try std.testing.expectEqual(@as(i8, 0), try td1_neg.compare(td1_neg, .{})); - try std.testing.expectEqual(@as(i8, 1), try td2_neg.compare(td1_neg, .{})); - try std.testing.expectEqual(@as(i8, -1), try td1_neg.compare(td2_neg, .{})); - try std.testing.expectEqual(@as(i8, -1), try td1_neg.compare(td2, .{})); - try std.testing.expectEqual(@as(i8, 1), try td1.compare(td2_neg, .{})); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/from/argument-string.js -test "Temporal.Duration from string parsing" { - // P1D - const d1 = try Temporal.Duration.from("P1D"); - defer d1.deinit(); - try std.testing.expectEqual(@as(i64, 0), d1.years()); - try std.testing.expectEqual(@as(i64, 0), d1.months()); - try std.testing.expectEqual(@as(i64, 0), d1.weeks()); - try std.testing.expectEqual(@as(i64, 1), d1.days()); - try std.testing.expectEqual(@as(i64, 0), d1.hours()); - try std.testing.expectEqual(@as(i64, 0), d1.minutes()); - try std.testing.expectEqual(@as(i64, 0), d1.seconds()); - try std.testing.expectEqual(@as(i64, 0), d1.milliseconds()); - - // p1y1m1dt1h1m1s - const d2 = try Temporal.Duration.from("p1y1m1dt1h1m1s"); - defer d2.deinit(); - try std.testing.expectEqual(@as(i64, 1), d2.years()); - try std.testing.expectEqual(@as(i64, 1), d2.months()); - try std.testing.expectEqual(@as(i64, 0), d2.weeks()); - try std.testing.expectEqual(@as(i64, 1), d2.days()); - try std.testing.expectEqual(@as(i64, 1), d2.hours()); - try std.testing.expectEqual(@as(i64, 1), d2.minutes()); - try std.testing.expectEqual(@as(i64, 1), d2.seconds()); - - // P1Y1M1W1DT1H1M1.123456789S - const d3 = try Temporal.Duration.from("P1Y1M1W1DT1H1M1.123456789S"); - defer d3.deinit(); - try std.testing.expectEqual(@as(i64, 1), d3.years()); - try std.testing.expectEqual(@as(i64, 1), d3.months()); - try std.testing.expectEqual(@as(i64, 1), d3.weeks()); - try std.testing.expectEqual(@as(i64, 1), d3.days()); - try std.testing.expectEqual(@as(i64, 1), d3.hours()); - try std.testing.expectEqual(@as(i64, 1), d3.minutes()); - try std.testing.expectEqual(@as(i64, 1), d3.seconds()); - try std.testing.expectEqual(@as(i64, 123), d3.milliseconds()); - try std.testing.expectEqual(@as(f64, 456), d3.microseconds()); - try std.testing.expectEqual(@as(f64, 789), d3.nanoseconds()); - - // P1DT0.5M (0.5 minutes) - const d4 = try Temporal.Duration.from("P1DT0.5M"); - defer d4.deinit(); - try std.testing.expectEqual(@as(i64, 0), d4.years()); - try std.testing.expectEqual(@as(i64, 0), d4.months()); - try std.testing.expectEqual(@as(i64, 0), d4.weeks()); - try std.testing.expectEqual(@as(i64, 1), d4.days()); - try std.testing.expectEqual(@as(i64, 0), d4.hours()); - try std.testing.expectEqual(@as(i64, 0), d4.minutes()); - try std.testing.expectEqual(@as(i64, 30), d4.seconds()); - - // -P1Y1M1W1DT1H1M1.123456789S (negative) - const d5 = try Temporal.Duration.from("-P1Y1M1W1DT1H1M1.123456789S"); - defer d5.deinit(); - try std.testing.expectEqual(@as(i64, -1), d5.years()); - try std.testing.expectEqual(@as(i64, -1), d5.months()); - try std.testing.expectEqual(@as(i64, -1), d5.weeks()); - try std.testing.expectEqual(@as(i64, -1), d5.days()); - try std.testing.expectEqual(@as(i64, -1), d5.hours()); - try std.testing.expectEqual(@as(i64, -1), d5.minutes()); - try std.testing.expectEqual(@as(i64, -1), d5.seconds()); - - // PT100M - const d6 = try Temporal.Duration.from("PT100M"); - defer d6.deinit(); - try std.testing.expectEqual(@as(i64, 0), d6.years()); - try std.testing.expectEqual(@as(i64, 0), d6.months()); - try std.testing.expectEqual(@as(i64, 0), d6.weeks()); - try std.testing.expectEqual(@as(i64, 0), d6.days()); - try std.testing.expectEqual(@as(i64, 0), d6.hours()); - try std.testing.expectEqual(@as(i64, 100), d6.minutes()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/prototype/abs/basic.js -test "Temporal.Duration abs" { - // blank duration - const d1 = try Temporal.Duration.init(0, 0, 0, 0, 0, 0, 0, 0, 0, 0); - defer d1.deinit(); - const abs1 = d1.abs(); - defer abs1.deinit(); - try std.testing.expectEqual(@as(i64, 0), abs1.years()); - try std.testing.expectEqual(@as(i64, 0), abs1.months()); - try std.testing.expectEqual(@as(i64, 0), abs1.weeks()); - try std.testing.expectEqual(@as(i64, 0), abs1.days()); - - // positive values - const d2 = try Temporal.Duration.init(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); - defer d2.deinit(); - const abs2 = d2.abs(); - defer abs2.deinit(); - try std.testing.expectEqual(@as(i64, 1), abs2.years()); - try std.testing.expectEqual(@as(i64, 2), abs2.months()); - try std.testing.expectEqual(@as(i64, 3), abs2.weeks()); - try std.testing.expectEqual(@as(i64, 4), abs2.days()); - try std.testing.expectEqual(@as(i64, 5), abs2.hours()); - try std.testing.expectEqual(@as(i64, 6), abs2.minutes()); - try std.testing.expectEqual(@as(i64, 7), abs2.seconds()); - try std.testing.expectEqual(@as(i64, 8), abs2.milliseconds()); - try std.testing.expectEqual(@as(f64, 9), abs2.microseconds()); - try std.testing.expectEqual(@as(f64, 10), abs2.nanoseconds()); - - // negative values - const d3 = try Temporal.Duration.init(-1, -2, -3, -4, -5, -6, -7, -8, -9, -10); - defer d3.deinit(); - const abs3 = d3.abs(); - defer abs3.deinit(); - try std.testing.expectEqual(@as(i64, 1), abs3.years()); - try std.testing.expectEqual(@as(i64, 2), abs3.months()); - try std.testing.expectEqual(@as(i64, 3), abs3.weeks()); - try std.testing.expectEqual(@as(i64, 4), abs3.days()); - try std.testing.expectEqual(@as(i64, 5), abs3.hours()); - try std.testing.expectEqual(@as(i64, 6), abs3.minutes()); - try std.testing.expectEqual(@as(i64, 7), abs3.seconds()); - try std.testing.expectEqual(@as(i64, 8), abs3.milliseconds()); - try std.testing.expectEqual(@as(f64, 9), abs3.microseconds()); - try std.testing.expectEqual(@as(f64, 10), abs3.nanoseconds()); - - // some zeros - const d4 = try Temporal.Duration.init(1, 0, 3, 0, 5, 0, 7, 0, 9, 0); - defer d4.deinit(); - const abs4 = d4.abs(); - defer abs4.deinit(); - try std.testing.expectEqual(@as(i64, 1), abs4.years()); - try std.testing.expectEqual(@as(i64, 0), abs4.months()); - try std.testing.expectEqual(@as(i64, 3), abs4.weeks()); - try std.testing.expectEqual(@as(i64, 0), abs4.days()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/prototype/negated/basic.js -test "Temporal.Duration negated" { - // blank duration - const d1 = try Temporal.Duration.init(0, 0, 0, 0, 0, 0, 0, 0, 0, 0); - defer d1.deinit(); - const neg1 = d1.negated(); - defer neg1.deinit(); - try std.testing.expectEqual(@as(i64, 0), neg1.years()); - try std.testing.expectEqual(Temporal.Duration.Sign.zero, neg1.sign()); - - // positive values - const d2 = try Temporal.Duration.init(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); - defer d2.deinit(); - const neg2 = d2.negated(); - defer neg2.deinit(); - try std.testing.expectEqual(@as(i64, -1), neg2.years()); - try std.testing.expectEqual(@as(i64, -2), neg2.months()); - try std.testing.expectEqual(@as(i64, -3), neg2.weeks()); - try std.testing.expectEqual(@as(i64, -4), neg2.days()); - try std.testing.expectEqual(@as(i64, -5), neg2.hours()); - try std.testing.expectEqual(@as(i64, -6), neg2.minutes()); - try std.testing.expectEqual(@as(i64, -7), neg2.seconds()); - try std.testing.expectEqual(@as(i64, -8), neg2.milliseconds()); - try std.testing.expectEqual(@as(f64, -9), neg2.microseconds()); - try std.testing.expectEqual(@as(f64, -10), neg2.nanoseconds()); - try std.testing.expectEqual(Temporal.Duration.Sign.negative, neg2.sign()); - - // negative values - const d3 = try Temporal.Duration.init(-1, -2, -3, -4, -5, -6, -7, -8, -9, -10); - defer d3.deinit(); - const neg3 = d3.negated(); - defer neg3.deinit(); - try std.testing.expectEqual(@as(i64, 1), neg3.years()); - try std.testing.expectEqual(@as(i64, 2), neg3.months()); - try std.testing.expectEqual(@as(i64, 3), neg3.weeks()); - try std.testing.expectEqual(@as(i64, 4), neg3.days()); - try std.testing.expectEqual(Temporal.Duration.Sign.positive, neg3.sign()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/prototype/add/basic.js -test "Temporal.Duration add time-only" { - // Simple time additions - const td1 = try Temporal.Duration.init(0, 0, 0, 0, 1, 2, 3, 4, 5, 6); - defer td1.deinit(); - const td2 = try Temporal.Duration.init(0, 0, 0, 0, 1, 2, 3, 4, 5, 6); - defer td2.deinit(); - const result = try td1.add(td2); - defer result.deinit(); - try std.testing.expectEqual(@as(i64, 0), result.years()); - try std.testing.expectEqual(@as(i64, 0), result.months()); - try std.testing.expectEqual(@as(i64, 0), result.weeks()); - try std.testing.expectEqual(@as(i64, 0), result.days()); - try std.testing.expectEqual(@as(i64, 2), result.hours()); - try std.testing.expectEqual(@as(i64, 4), result.minutes()); - try std.testing.expectEqual(@as(i64, 6), result.seconds()); - try std.testing.expectEqual(@as(i64, 8), result.milliseconds()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/prototype/subtract/basic.js -test "Temporal.Duration subtract time-only" { - // Simple time subtraction - const td1 = try Temporal.Duration.init(0, 0, 0, 0, 5, 5, 5, 5, 5, 5); - defer td1.deinit(); - const td2 = try Temporal.Duration.init(0, 0, 0, 0, 1, 1, 1, 1, 1, 1); - defer td2.deinit(); - const result = try td1.subtract(td2); - defer result.deinit(); - try std.testing.expectEqual(@as(i64, 4), result.hours()); - try std.testing.expectEqual(@as(i64, 4), result.minutes()); - try std.testing.expectEqual(@as(i64, 4), result.seconds()); - try std.testing.expectEqual(@as(i64, 4), result.milliseconds()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/prototype/blank/basic.js -test "Temporal.Duration blank" { - // Blank duration - const blank = try Temporal.Duration.init(0, 0, 0, 0, 0, 0, 0, 0, 0, 0); - defer blank.deinit(); - try std.testing.expectEqual(true, blank.blank()); - - // Non-blank durations - const d1 = try Temporal.Duration.init(1, 0, 0, 0, 0, 0, 0, 0, 0, 0); - defer d1.deinit(); - try std.testing.expectEqual(false, d1.blank()); - - const d2 = try Temporal.Duration.init(0, 1, 0, 0, 0, 0, 0, 0, 0, 0); - defer d2.deinit(); - try std.testing.expectEqual(false, d2.blank()); - - const d3 = try Temporal.Duration.init(0, 0, 1, 0, 0, 0, 0, 0, 0, 0); - defer d3.deinit(); - try std.testing.expectEqual(false, d3.blank()); - - const d4 = try Temporal.Duration.init(0, 0, 0, 1, 0, 0, 0, 0, 0, 0); - defer d4.deinit(); - try std.testing.expectEqual(false, d4.blank()); - - const d5 = try Temporal.Duration.init(0, 0, 0, 0, 1, 0, 0, 0, 0, 0); - defer d5.deinit(); - try std.testing.expectEqual(false, d5.blank()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/prototype/toString/basic.js -test "Temporal.Duration toString" { - const allocator = std.testing.allocator; - - // Blank duration - const blank = try Temporal.Duration.init(0, 0, 0, 0, 0, 0, 0, 0, 0, 0); - defer blank.deinit(); - const str1 = try blank.toString(allocator, .{}); - defer allocator.free(str1); - try std.testing.expectEqualSlices(u8, "PT0S", str1); - - // Hours and minutes - const td1 = try Temporal.Duration.init(0, 0, 0, 0, 1, 30, 0, 0, 0, 0); - defer td1.deinit(); - const str2 = try td1.toString(allocator, .{}); - defer allocator.free(str2); - try std.testing.expectEqualSlices(u8, "PT1H30M", str2); - - // Full duration - const td2 = try Temporal.Duration.init(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); - defer td2.deinit(); - const str3 = try td2.toString(allocator, .{}); - defer allocator.free(str3); - // Result should contain Y, M, W, D, H, M, S, etc. - try std.testing.expect(std.mem.containsAtLeast(u8, str3, 1, "Y")); - try std.testing.expect(std.mem.containsAtLeast(u8, str3, 1, "W")); - try std.testing.expect(std.mem.containsAtLeast(u8, str3, 1, "D")); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/prototype/toJSON/basic.js -test "Temporal.Duration toJSON" { - const allocator = std.testing.allocator; - - const blank = try Temporal.Duration.init(0, 0, 0, 0, 0, 0, 0, 0, 0, 0); - defer blank.deinit(); - const json = try blank.toJSON(allocator); - defer allocator.free(json); - try std.testing.expectEqualSlices(u8, "PT0S", json); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/prototype/total/basic.js -test "Temporal.Duration total" { - const td = try Temporal.Duration.init(0, 0, 0, 0, 1, 30, 0, 0, 0, 0); - defer td.deinit(); - - // total hours - const hours = try td.total(.{ .unit = Temporal.Duration.Unit.hour }); - try std.testing.expectApproxEqAbs(@as(f64, 1.5), hours, 0.0001); - - // total minutes - const minutes = try td.total(.{ .unit = Temporal.Duration.Unit.minute }); - try std.testing.expectApproxEqAbs(@as(f64, 90), minutes, 0.0001); - - // total seconds - const seconds = try td.total(.{ .unit = Temporal.Duration.Unit.second }); - try std.testing.expectApproxEqAbs(@as(f64, 5400), seconds, 0.0001); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/constructor.js -test "Temporal.Duration cannot be called without new" { - // Note: In Zig we use a function, not a constructor, so this test is about ensuring proper init - // Testing that we can successfully create a duration - const dur = try Temporal.Duration.init(1, 1, 1, 1, 1, 1, 1, 1, 1, 1); - defer dur.deinit(); - try std.testing.expectEqual(@as(i64, 1), dur.years()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/days-undefined.js -test "Temporal.Duration with specific fields from ISO string" { - const d1 = try Temporal.Duration.from("P1Y"); - defer d1.deinit(); - try std.testing.expectEqual(@as(i64, 1), d1.years()); - try std.testing.expectEqual(@as(i64, 0), d1.months()); - try std.testing.expectEqual(@as(i64, 0), d1.days()); - try std.testing.expectEqual(@as(i64, 0), d1.hours()); - - const d2 = try Temporal.Duration.from("PT1H"); - defer d2.deinit(); - try std.testing.expectEqual(@as(i64, 0), d2.years()); - try std.testing.expectEqual(@as(i64, 0), d2.days()); - try std.testing.expectEqual(@as(i64, 1), d2.hours()); - try std.testing.expectEqual(@as(i64, 0), d2.minutes()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/mixed.js -test "Temporal.Duration mixed components" { - const d1 = try Temporal.Duration.init(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); - defer d1.deinit(); - try std.testing.expectEqual(@as(i64, 1), d1.years()); - try std.testing.expectEqual(@as(i64, 2), d1.months()); - try std.testing.expectEqual(@as(i64, 3), d1.weeks()); - try std.testing.expectEqual(@as(i64, 4), d1.days()); - try std.testing.expectEqual(@as(i64, 5), d1.hours()); - try std.testing.expectEqual(@as(i64, 6), d1.minutes()); - try std.testing.expectEqual(@as(i64, 7), d1.seconds()); - try std.testing.expectEqual(@as(i64, 8), d1.milliseconds()); - try std.testing.expectEqual(@as(f64, 9), d1.microseconds()); - try std.testing.expectEqual(@as(f64, 10), d1.nanoseconds()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/prototype/sign/basic.js -test "Temporal.Duration sign property" { - const blank = try Temporal.Duration.init(0, 0, 0, 0, 0, 0, 0, 0, 0, 0); - defer blank.deinit(); - try std.testing.expectEqual(Temporal.Duration.Sign.zero, blank.sign()); - - const pos = try Temporal.Duration.init(1, 0, 0, 0, 0, 0, 0, 0, 0, 0); - defer pos.deinit(); - try std.testing.expectEqual(Temporal.Duration.Sign.positive, pos.sign()); - - const neg = try Temporal.Duration.init(-1, 0, 0, 0, 0, 0, 0, 0, 0, 0); - defer neg.deinit(); - try std.testing.expectEqual(Temporal.Duration.Sign.negative, neg.sign()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/large-number.js -test "Temporal.Duration with large numbers" { - const d = try Temporal.Duration.init(100000, 100000, 100000, 100000, 100000, 100000, 100000, 100000, 100000, 100000); - defer d.deinit(); - try std.testing.expectEqual(@as(i64, 100000), d.years()); - try std.testing.expectEqual(@as(i64, 100000), d.months()); - try std.testing.expectEqual(@as(i64, 100000), d.weeks()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/from/blank-duration.js -test "Temporal.Duration blank from string" { - const blank1 = try Temporal.Duration.from("PT0S"); - defer blank1.deinit(); - try std.testing.expectEqual(true, blank1.blank()); - - const blank2 = try Temporal.Duration.from("P0D"); - defer blank2.deinit(); - try std.testing.expectEqual(true, blank2.blank()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/length.js -test "Temporal.Duration constructor has correct arity" { - // In Zig, we test that init can be called with the expected parameters - const d = try Temporal.Duration.init(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); - defer d.deinit(); - try std.testing.expect(true); // If we get here, init works -} - -// Mirrors test262 test/built-ins/Temporal/Duration/from/length.js -test "Temporal.Duration from has correct arity" { - const d = try Temporal.Duration.from("P1D"); - defer d.deinit(); - try std.testing.expect(true); // If we get here, from works -} - -// Mirrors test262 test/built-ins/Temporal/Duration/prototype/add/basic.js with normal values -test "Temporal.Duration add with time fields" { - const d1 = try Temporal.Duration.init(0, 0, 0, 0, 1, 1, 1, 1, 0, 0); - defer d1.deinit(); - const d2 = try Temporal.Duration.init(0, 0, 0, 0, 2, 2, 2, 2, 0, 0); - defer d2.deinit(); - - const result = try d1.add(d2); - defer result.deinit(); - try std.testing.expectEqual(@as(i64, 0), result.years()); - try std.testing.expectEqual(@as(i64, 3), result.hours()); - try std.testing.expectEqual(@as(i64, 3), result.minutes()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/prototype/add/basic.js with date and time -test "Temporal.Duration add with date and time components" { - // Note: Adding durations with date components together requires a RelativeTo context - // which is not always available, causing a RangeError - if (true) return error.SkipZigTest; - - const d1 = try Temporal.Duration.init(100, 100, 100, 100, 100, 100, 100, 100, 100, 100); - defer d1.deinit(); - const d2 = try Temporal.Duration.init(100, 100, 100, 100, 100, 100, 100, 100, 100, 100); - defer d2.deinit(); - - const result = try d1.add(d2); - defer result.deinit(); - try std.testing.expectEqual(@as(i64, 200), result.years()); - try std.testing.expectEqual(@as(i64, 200), result.months()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/prototype/subtract/basic.js with normal values -test "Temporal.Duration subtract with time fields" { - const d1 = try Temporal.Duration.init(0, 0, 0, 0, 5, 5, 5, 5, 0, 0); - defer d1.deinit(); - const d2 = try Temporal.Duration.init(0, 0, 0, 0, 2, 2, 2, 2, 0, 0); - defer d2.deinit(); - - const result = try d1.subtract(d2); - defer result.deinit(); - try std.testing.expectEqual(@as(i64, 0), result.years()); - try std.testing.expectEqual(@as(i64, 3), result.hours()); - try std.testing.expectEqual(@as(i64, 3), result.minutes()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/prototype/subtract/basic.js with date and time -test "Temporal.Duration subtract with date and time components" { - // Note: Subtracting durations with date components together requires a RelativeTo context - // which is not always available, causing a RangeError - if (true) return error.SkipZigTest; - - const d1 = try Temporal.Duration.init(200, 200, 200, 200, 200, 200, 200, 200, 200, 200); - defer d1.deinit(); - const d2 = try Temporal.Duration.init(100, 100, 100, 100, 100, 100, 100, 100, 100, 100); - defer d2.deinit(); - - const result = try d1.subtract(d2); - defer result.deinit(); - try std.testing.expectEqual(@as(i64, 100), result.years()); - try std.testing.expectEqual(@as(i64, 100), result.months()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/name.js -test "Temporal.Duration constructor name" { - // In Zig we can't directly test function name, but we test that the constructor exists and works - const d = try Temporal.Duration.init(0, 0, 0, 0, 0, 0, 0, 0, 0, 0); - defer d.deinit(); - try std.testing.expect(true); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/from/argument-duration.js -test "Temporal.Duration from duration object" { - const d1 = try Temporal.Duration.init(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); - defer d1.deinit(); - - // In Zig binding, we'd typically test string parsing instead - const d2 = try Temporal.Duration.from("P1Y2M3W4DT5H6M7.008009010S"); - defer d2.deinit(); - try std.testing.expectEqual(@as(i64, 1), d2.years()); - try std.testing.expectEqual(@as(i64, 2), d2.months()); - try std.testing.expectEqual(@as(i64, 3), d2.weeks()); - try std.testing.expectEqual(@as(i64, 4), d2.days()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/prototype/with/basic.js (approximation) -test "Temporal.Duration with method basic" { - // Skip - requires with() method which may not be available - if (true) return error.SkipZigTest; -} - -// Mirrors test262 test/built-ins/Temporal/Duration/prototype/with/all-positive.js -test "Temporal.Duration with positive components" { - // Skip - requires with() method - if (true) return error.SkipZigTest; -} - -// Mirrors test262 test/built-ins/Temporal/Duration/prototype/round/basic.js -test "Temporal.Duration round method" { - // Skip - requires round() method with RoundingOptions - if (true) return error.SkipZigTest; -} - -// Mirrors test262 test/built-ins/Temporal/Duration/prototype/round/largest-unit.js -test "Temporal.Duration round with largest unit" { - // Skip - requires round() method - if (true) return error.SkipZigTest; -} - -// Mirrors test262 test/built-ins/Temporal/Duration/prototype/round/smallest-unit.js -test "Temporal.Duration round with smallest unit" { - // Skip - requires round() method - if (true) return error.SkipZigTest; -} - -// Mirrors test262 test/built-ins/Temporal/Duration/years-undefined.js -test "Temporal.Duration years property undefined behavior" { - const d = try Temporal.Duration.from("PT1H"); - defer d.deinit(); - try std.testing.expectEqual(@as(i64, 0), d.years()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/months-undefined.js -test "Temporal.Duration months property undefined behavior" { - const d = try Temporal.Duration.from("PT1H"); - defer d.deinit(); - try std.testing.expectEqual(@as(i64, 0), d.months()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/weeks-undefined.js -test "Temporal.Duration weeks property undefined behavior" { - const d = try Temporal.Duration.from("PT1H"); - defer d.deinit(); - try std.testing.expectEqual(@as(i64, 0), d.weeks()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/days-undefined.js (property access) -test "Temporal.Duration days property undefined behavior" { - const d = try Temporal.Duration.from("PT1H"); - defer d.deinit(); - try std.testing.expectEqual(@as(i64, 0), d.days()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/hours-undefined.js -test "Temporal.Duration hours property undefined behavior" { - const d = try Temporal.Duration.from("P1Y"); - defer d.deinit(); - try std.testing.expectEqual(@as(i64, 0), d.hours()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/minutes-undefined.js -test "Temporal.Duration minutes property undefined behavior" { - const d = try Temporal.Duration.from("P1Y"); - defer d.deinit(); - try std.testing.expectEqual(@as(i64, 0), d.minutes()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/seconds-undefined.js -test "Temporal.Duration seconds property undefined behavior" { - const d = try Temporal.Duration.from("P1Y"); - defer d.deinit(); - try std.testing.expectEqual(@as(i64, 0), d.seconds()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/milliseconds-undefined.js -test "Temporal.Duration milliseconds property undefined behavior" { - const d = try Temporal.Duration.from("P1Y"); - defer d.deinit(); - try std.testing.expectEqual(@as(i64, 0), d.milliseconds()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/microseconds-undefined.js -test "Temporal.Duration microseconds property undefined behavior" { - const d = try Temporal.Duration.from("P1Y"); - defer d.deinit(); - try std.testing.expectEqual(@as(f64, 0), d.microseconds()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/nanoseconds-undefined.js -test "Temporal.Duration nanoseconds property undefined behavior" { - const d = try Temporal.Duration.from("P1Y"); - defer d.deinit(); - try std.testing.expectEqual(@as(f64, 0), d.nanoseconds()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/max.js -test "Temporal.Duration max value handling" { - // Test that very large durations can be created - const d = try Temporal.Duration.init(999999999, 999999999, 999999999, 999999999, 999999999, 999999999, 999999999, 999999999, 999999999, 999999999); - defer d.deinit(); - try std.testing.expectEqual(@as(i64, 999999999), d.years()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/lower-limit.js -test "Temporal.Duration lower limit handling" { - // Test negative duration handling - const d = try Temporal.Duration.init(-999999999, -999999999, -999999999, -999999999, -999999999, -999999999, -999999999, -999999999, -999999999, -999999999); - defer d.deinit(); - try std.testing.expectEqual(@as(i64, -999999999), d.years()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/out-of-range.js -test "Temporal.Duration out of range behavior" { - // Skip - would require error handling for invalid ranges - if (true) return error.SkipZigTest; -} - -// Mirrors test262 test/built-ins/Temporal/Duration/invalid-type.js -test "Temporal.Duration invalid type handling" { - // Skip - type validation would be at binding level - if (true) return error.SkipZigTest; -} - -// Mirrors test262 test/built-ins/Temporal/Duration/fractional-throws-rangeerror.js -test "Temporal.Duration fractional range error" { - // Skip - fractional components validation - if (true) return error.SkipZigTest; -} - -// Mirrors test262 test/built-ins/Temporal/Duration/infinity-throws-rangeerror.js -test "Temporal.Duration infinity range error" { - // Skip - infinity handling at binding level - if (true) return error.SkipZigTest; -} - -// Mirrors test262 test/built-ins/Temporal/Duration/negative-infinity-throws-rangeerror.js -test "Temporal.Duration negative infinity range error" { - // Skip - infinity handling at binding level - if (true) return error.SkipZigTest; -} - -// Mirrors test262 test/built-ins/Temporal/Duration/builtin.js -test "Temporal.Duration builtin verification" { - const d = try Temporal.Duration.init(0, 0, 0, 0, 0, 0, 0, 0, 0, 0); - defer d.deinit(); - try std.testing.expect(true); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/call-builtin.js -test "Temporal.Duration call as builtin" { - // In Zig, Duration is created via init function, not called as builtin - const d = try Temporal.Duration.init(1, 0, 0, 0, 0, 0, 0, 0, 0, 0); - defer d.deinit(); - try std.testing.expectEqual(@as(i64, 1), d.years()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/prototype/valueOf/basic.js -test "Temporal.Duration valueOf method" { - // Skip - valueOf may not be directly comparable in Zig - if (true) return error.SkipZigTest; -} - -// Mirrors test262 test/built-ins/Temporal/Duration/compare/blank-duration.js -test "Temporal.Duration compare blank durations" { - const blank1 = try Temporal.Duration.init(0, 0, 0, 0, 0, 0, 0, 0, 0, 0); - defer blank1.deinit(); - const blank2 = try Temporal.Duration.init(0, 0, 0, 0, 0, 0, 0, 0, 0, 0); - defer blank2.deinit(); - - const result = try blank1.compare(blank2, .{}); - try std.testing.expectEqual(@as(i8, 0), result); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/from/blank-duration.js (property test) -test "Temporal.Duration blank property from string" { - const d = try Temporal.Duration.from("PT0S"); - defer d.deinit(); - try std.testing.expectEqual(true, d.blank()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/prototype/abs/new-object.js -test "Temporal.Duration abs creates new object" { - const d1 = try Temporal.Duration.init(-5, -5, -5, -5, -5, -5, -5, -5, -5, -5); - defer d1.deinit(); - const d2 = d1.abs(); - defer d2.deinit(); - - // Verify they have different signs - try std.testing.expectEqual(Temporal.Duration.Sign.negative, d1.sign()); - try std.testing.expectEqual(Temporal.Duration.Sign.positive, d2.sign()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/prototype/negated/basic.js (zero handling) -test "Temporal.Duration negated zero duration" { - const zero = try Temporal.Duration.init(0, 0, 0, 0, 0, 0, 0, 0, 0, 0); - defer zero.deinit(); - const negated = zero.negated(); - defer negated.deinit(); - - try std.testing.expectEqual(Temporal.Duration.Sign.zero, negated.sign()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/from/string-with-skipped-units.js -test "Temporal.Duration from string with skipped units" { - const d1 = try Temporal.Duration.from("P1Y3M"); - defer d1.deinit(); - try std.testing.expectEqual(@as(i64, 1), d1.years()); - try std.testing.expectEqual(@as(i64, 3), d1.months()); - try std.testing.expectEqual(@as(i64, 0), d1.weeks()); - try std.testing.expectEqual(@as(i64, 0), d1.days()); - - const d2 = try Temporal.Duration.from("PT2H30M"); - defer d2.deinit(); - try std.testing.expectEqual(@as(i64, 2), d2.hours()); - try std.testing.expectEqual(@as(i64, 30), d2.minutes()); - try std.testing.expectEqual(@as(i64, 0), d2.seconds()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/prototype/add/blank-duration.js -test "Temporal.Duration add blank duration" { - if (true) return error.SkipZigTest; - const d1 = try Temporal.Duration.init(1, 2, 3, 4, 5, 6, 7, 8, 0, 0); - defer d1.deinit(); - const blank = try Temporal.Duration.init(0, 0, 0, 0, 0, 0, 0, 0, 0, 0); - defer blank.deinit(); - - const result = try d1.add(blank); - defer result.deinit(); - try std.testing.expectEqual(@as(i64, 1), result.years()); - try std.testing.expectEqual(@as(i64, 5), result.hours()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/prototype/subtract/blank-duration.js -test "Temporal.Duration subtract blank duration" { - if (true) return error.SkipZigTest; - - const d1 = try Temporal.Duration.init(1, 2, 3, 4, 5, 6, 7, 8, 0, 0); - defer d1.deinit(); - const blank = try Temporal.Duration.init(0, 0, 0, 0, 0, 0, 0, 0, 0, 0); - defer blank.deinit(); - - const result = try d1.subtract(blank); - defer result.deinit(); - try std.testing.expectEqual(@as(i64, 1), result.years()); - try std.testing.expectEqual(@as(i64, 5), result.hours()); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/prototype/toString/blank-duration-precision.js -test "Temporal.Duration toString blank with precision" { - const allocator = std.testing.allocator; - const blank = try Temporal.Duration.init(0, 0, 0, 0, 0, 0, 0, 0, 0, 0); - defer blank.deinit(); - const str = try blank.toString(allocator, .{}); - defer allocator.free(str); - try std.testing.expectEqualSlices(u8, "PT0S", str); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/prototype/toJSON/blank-duration.js (approx) -test "Temporal.Duration toJSON blank" { - const allocator = std.testing.allocator; - const blank = try Temporal.Duration.init(0, 0, 0, 0, 0, 0, 0, 0, 0, 0); - defer blank.deinit(); - const json = try blank.toJSON(allocator); - defer allocator.free(json); - try std.testing.expectEqualSlices(u8, "PT0S", json); -} - -// Mirrors test262 test/built-ins/Temporal/Duration/prop-desc.js -test "Temporal.Duration property descriptor" { - // JS-specific test for property descriptors (writable, enumerable, configurable) - // Not directly applicable to Zig bindings - if (true) return error.SkipZigTest; -} - -// Mirrors test262 test/built-ins/Temporal/Duration/subclass.js -test "Temporal.Duration subclassing" { - // JS-specific test for subclassing Temporal.Duration - // Zig does not support class inheritance in the same way - if (true) return error.SkipZigTest; -} - -// Mirrors test262 test/built-ins/Temporal/Duration/get-prototype-from-constructor-throws.js -test "Temporal.Duration get prototype from constructor throws" { - // JS-specific test for OrdinaryCreateFromConstructor prototype handling - // Not applicable to Zig bindings - if (true) return error.SkipZigTest; -} diff --git a/test/test262/polyfill.js b/test/test262/polyfill.js new file mode 100644 index 0000000..0405e9a --- /dev/null +++ b/test/test262/polyfill.js @@ -0,0 +1,683 @@ +// This polyfill is executed in a vm.Script context by test262-runner, +// so it must be synchronous and self-contained. +// The wasm bytes are injected as global.__TEMPORALZ_WASM_BYTES__ by the runner. +(function () { + if (!globalThis.__TEMPORALZ_WASM_BYTES__) { + throw new Error("WASM bytes not injected into test context"); + } + + const wasmBinary = globalThis.__TEMPORALZ_WASM_BYTES__; + const wasmModule = new WebAssembly.Module(wasmBinary); + const wasmInstance = new WebAssembly.Instance(wasmModule, { + env: { + console(ptr, len) { + const bytes = new Uint8Array(memory.buffer, ptr, len); + globalThis.console.log(decoder.decode(bytes)); + }, + }, + }); + + const exports = wasmInstance.exports; + const memory = exports.memory; + + const encoder = { + encode(str) { + const buf = []; + for (let i = 0; i < str.length; i++) { + const code = str.charCodeAt(i); + if (code < 0x80) { + buf.push(code); + } else if (code < 0x800) { + buf.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f)); + } else if (code < 0xd800 || code >= 0xe000) { + buf.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f)); + } else { + const code2 = str.charCodeAt(++i); + const codePoint = 0x10000 + (((code & 0x3ff) << 10) | (code2 & 0x3ff)); + buf.push( + 0xf0 | (codePoint >> 18), + 0x80 | ((codePoint >> 12) & 0x3f), + 0x80 | ((codePoint >> 6) & 0x3f), + 0x80 | (codePoint & 0x3f) + ); + } + } + return new Uint8Array(buf); + }, + }; + + const decoder = { + decode(bytes) { + let str = ""; + for (let i = 0; i < bytes.length; i++) { + const byte = bytes[i]; + if (byte < 0x80) { + str += String.fromCharCode(byte); + } else if ((byte & 0xe0) === 0xc0) { + str += String.fromCharCode(((byte & 0x1f) << 6) | (bytes[++i] & 0x3f)); + } else if ((byte & 0xf0) === 0xe0) { + str += String.fromCharCode( + ((byte & 0x0f) << 12) | ((bytes[++i] & 0x3f) << 6) | (bytes[++i] & 0x3f) + ); + } else if ((byte & 0xf8) === 0xf0) { + const codePoint = + ((byte & 0x07) << 18) | + ((bytes[++i] & 0x3f) << 12) | + ((bytes[++i] & 0x3f) << 6) | + (bytes[++i] & 0x3f); + const high = ((codePoint - 0x10000) >> 10) + 0xd800; + const low = ((codePoint - 0x10000) & 0x3ff) + 0xdc00; + str += String.fromCharCode(high, low); + } + } + return str; + }, + }; + + function readString(ptr, len) { + return decoder.decode(new Uint8Array(memory.buffer, ptr, len)); + } + + function lastError() { + const ptr = exports.temporalz_last_error_ptr(); + const len = exports.temporalz_last_error_len(); + if (!ptr || !len) return new Error("temporalz error"); + const msg = readString(ptr, len); + exports.temporalz_last_error_clear(); + if (msg.includes("Range")) return new RangeError(msg); + if (msg.includes("Type")) return new TypeError(msg); + return new Error(msg); + } + + function requireHandle(handle) { + if (!handle) throw lastError(); + return handle; + } + + function allocString(text) { + const bytes = encoder.encode(text); + const ptr = exports.temporalz_alloc(bytes.length); + if (!ptr) throw lastError(); + new Uint8Array(memory.buffer, ptr, bytes.length).set(bytes); + return { ptr, len: bytes.length }; + } + + function takeString(packed) { + if (!packed) throw lastError(); + const value = BigInt(packed); + const ptr = Number(value >> 32n); + const len = Number(value & 0xffffffffn); + const text = readString(ptr, len); + exports.temporalz_string_free(ptr, len); + return text; + } + + function splitI128(value) { + const v = typeof value === "bigint" ? value : BigInt(value); + const mask = (1n << 64n) - 1n; + return { hi: v >> 64n, lo: v & mask }; + } + + function joinI128(hi, lo) { + const mask = (1n << 64n) - 1n; + return (BigInt(hi) << 64n) | (BigInt(lo) & mask); + } + + const unitCodes = { + nanosecond: 1, + microsecond: 2, + millisecond: 3, + second: 4, + minute: 5, + hour: 6, + day: 7, + week: 8, + month: 9, + year: 10, + auto: 11, + }; + + const roundingModeCodes = { + ceil: 1, + floor: 2, + expand: 3, + trunc: 4, + halfCeil: 5, + halfFloor: 6, + halfExpand: 7, + halfTrunc: 8, + halfEven: 9, + }; + + function toUnitCode(value, name) { + if (value === undefined || value === null) return 255; + const code = unitCodes[value]; + if (!code) throw new RangeError(`Invalid ${name}`); + return code; + } + + function toRoundingModeCode(value) { + if (value === undefined || value === null) return 255; + const code = roundingModeCodes[value]; + if (!code) throw new RangeError("Invalid roundingMode"); + return code; + } + + function toIntegerBigInt(value, name) { + if (typeof value === "bigint") return value; + const number = Number(value); + if (!isFiniteNumber(number)) throw new RangeError(`${name} must be finite`); + if (!Number.isInteger(number)) throw new RangeError(`${name} must be an integer`); + return BigInt(number); + } + + function toNumber(value, name) { + const number = Number(value); + if (!isFiniteNumber(number)) throw new RangeError(`${name} must be finite`); + return number; + } + + function toInteger(value, name) { + const number = Number(value); + if (!isFiniteNumber(number)) throw new RangeError(`${name} must be finite`); + if (!Number.isInteger(number)) throw new RangeError(`${name} must be an integer`); + return number; + } + + function isFiniteNumber(value) { + return value === value && value !== Infinity && value !== -Infinity; + } + + function unimplemented(name) { + return function () { + throw new Error(`${name} is not implemented yet`); + }; + } + + class Instant { + constructor(handle) { + this._handle = handle; + } + + static _fromHandle(handle) { + return new Instant(handle); + } + + static fromEpochMilliseconds(epochMs) { + const number = Number(epochMs); + if (!isFiniteNumber(number)) throw new RangeError("epochMilliseconds must be finite"); + const handle = exports.temporalz_instant_from_epoch_milliseconds(number); + return Instant._fromHandle(requireHandle(handle)); + } + + static fromEpochNanoseconds(epochNs) { + const parts = splitI128(epochNs); + const handle = exports.temporalz_instant_from_epoch_nanoseconds_parts( + parts.hi, + parts.lo + ); + return Instant._fromHandle(requireHandle(handle)); + } + + static from(value) { + if (value instanceof Instant) return value; + if (typeof value === "string") { + const text = allocString(value); + const handle = exports.temporalz_instant_from_utf8(text.ptr, text.len); + exports.temporalz_free(text.ptr, text.len); + return Instant._fromHandle(requireHandle(handle)); + } + if (value && typeof value === "object") { + if (value.epochNanoseconds !== undefined) { + return Instant.fromEpochNanoseconds(value.epochNanoseconds); + } + if (value.epochMilliseconds !== undefined) { + return Instant.fromEpochMilliseconds(value.epochMilliseconds); + } + } + throw new TypeError("Instant.from expects a string or object"); + } + + get epochMilliseconds() { + return exports.temporalz_instant_epoch_milliseconds(this._handle); + } + + get epochNanoseconds() { + const hi = exports.temporalz_instant_epoch_nanoseconds_hi(this._handle); + const lo = exports.temporalz_instant_epoch_nanoseconds_lo(this._handle); + return joinI128(hi, lo); + } + + toString() { + return takeString(exports.temporalz_instant_to_string(this._handle)); + } + + toJSON() { + return this.toString(); + } + + add(durationLike) { + const dur = Duration.from(durationLike); + const handle = exports.temporalz_instant_add(this._handle, dur._handle); + return Instant._fromHandle(requireHandle(handle)); + } + + subtract(durationLike) { + const dur = Duration.from(durationLike); + const handle = exports.temporalz_instant_subtract(this._handle, dur._handle); + return Instant._fromHandle(requireHandle(handle)); + } + + round(options) { + if (!options || typeof options !== "object") { + throw new TypeError("round options must be an object"); + } + const smallestUnit = toUnitCode(options.smallestUnit, "smallestUnit"); + if (smallestUnit === 255) throw new RangeError("smallestUnit is required"); + const roundingMode = toRoundingModeCode(options.roundingMode); + + let roundingIncrement = 0; + if (options.roundingIncrement !== undefined) { + const inc = Number(options.roundingIncrement); + if (!isFiniteNumber(inc) || !Number.isInteger(inc) || inc <= 0) { + throw new RangeError("Invalid roundingIncrement"); + } + roundingIncrement = inc; + } + + const handle = exports.temporalz_instant_round( + this._handle, + smallestUnit, + roundingMode, + roundingIncrement + ); + return Instant._fromHandle(requireHandle(handle)); + } + + equals(other) { + const rhs = Instant.from(other); + return exports.temporalz_instant_equals(this._handle, rhs._handle) === 1; + } + + toLocaleString() { + return this.toString(); + } + + valueOf() { + throw new TypeError("Cannot convert Temporal.Instant to a number"); + } + + static compare(a, b) { + const left = Instant.from(a); + const right = Instant.from(b); + return Number(exports.temporalz_instant_compare(left._handle, right._handle)); + } + } + + const plainDateHandleToken = Symbol("PlainDateHandle"); + + class PlainDate { + constructor(year, month, day) { + if (year === plainDateHandleToken) { + this._handle = month; + return; + } + + const yearValue = toInteger(year ?? 0, "year"); + const monthValue = toInteger(month ?? 0, "month"); + const dayValue = toInteger(day ?? 0, "day"); + const handle = exports.temporalz_plain_date_init(yearValue, monthValue, dayValue); + this._handle = requireHandle(handle); + } + + static _fromHandle(handle) { + return new PlainDate(plainDateHandleToken, handle); + } + + static from(value) { + if (value instanceof PlainDate) return value; + if (typeof value === "string") { + const text = allocString(value); + const handle = exports.temporalz_plain_date_from_utf8(text.ptr, text.len); + exports.temporalz_free(text.ptr, text.len); + return PlainDate._fromHandle(requireHandle(handle)); + } + if (value === null || typeof value !== "object") { + throw new TypeError("PlainDate.from expects a string or object"); + } + + const year = toInteger(value.year, "year"); + const month = toInteger(value.month, "month"); + const day = toInteger(value.day, "day"); + const handle = exports.temporalz_plain_date_init(year, month, day); + return PlainDate._fromHandle(requireHandle(handle)); + } + + toString() { + return takeString(exports.temporalz_plain_date_to_string(this._handle)); + } + } + + const durationHandleToken = Symbol("DurationHandle"); + + class Duration { + constructor( + years, + months, + weeks, + days, + hours, + minutes, + seconds, + milliseconds, + microseconds, + nanoseconds + ) { + if (years === durationHandleToken) { + this._handle = months; + return; + } + + const yearsValue = toIntegerBigInt(years ?? 0, "years"); + const monthsValue = toIntegerBigInt(months ?? 0, "months"); + const weeksValue = toIntegerBigInt(weeks ?? 0, "weeks"); + const daysValue = toIntegerBigInt(days ?? 0, "days"); + const hoursValue = toIntegerBigInt(hours ?? 0, "hours"); + const minutesValue = toIntegerBigInt(minutes ?? 0, "minutes"); + const secondsValue = toIntegerBigInt(seconds ?? 0, "seconds"); + const millisecondsValue = toIntegerBigInt(milliseconds ?? 0, "milliseconds"); + const microsecondsValue = toNumber(microseconds ?? 0, "microseconds"); + const nanosecondsValue = toNumber(nanoseconds ?? 0, "nanoseconds"); + + const created = exports.temporalz_duration_init( + yearsValue, + monthsValue, + weeksValue, + daysValue, + hoursValue, + minutesValue, + secondsValue, + millisecondsValue, + microsecondsValue, + nanosecondsValue + ); + this._handle = requireHandle(created); + } + + static _fromHandle(handle) { + return new Duration(durationHandleToken, handle); + } + + static from(value) { + if (value instanceof Duration) return value; + if (typeof value === "string") { + const text = allocString(value); + const handle = exports.temporalz_duration_from_utf8(text.ptr, text.len); + exports.temporalz_free(text.ptr, text.len); + return Duration._fromHandle(requireHandle(handle)); + } + if (value === null || typeof value !== "object") { + throw new TypeError("Duration.from expects a string or object"); + } + + const fields = [ + "years", + "months", + "weeks", + "days", + "hours", + "minutes", + "seconds", + "milliseconds", + "microseconds", + "nanoseconds", + ]; + + let mask = 0; + const values = [0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0, 0]; + + fields.forEach((field, index) => { + if (value[field] !== undefined) { + mask |= 1 << index; + if (field === "microseconds" || field === "nanoseconds") { + values[index] = toNumber(value[field], field); + } else { + values[index] = toIntegerBigInt(value[field], field); + } + } + }); + + const handle = exports.temporalz_duration_from_parts( + mask, + values[0], + values[1], + values[2], + values[3], + values[4], + values[5], + values[6], + values[7], + values[8], + values[9] + ); + return Duration._fromHandle(requireHandle(handle)); + } + + static compare(a, b, options) { + const left = Duration.from(a); + const right = Duration.from(b); + return Duration.compareWithOptions(left, right, options); + } + + static compareWithOptions(left, right, options) { + if (options !== undefined && (options === null || typeof options !== "object")) { + throw new TypeError("options must be an object"); + } + const relativeTo = toRelativeTo(options); + if (!relativeTo && (hasYearMonthWeek(left) || hasYearMonthWeek(right))) { + throw new RangeError("relativeTo is required for calendar units"); + } + if (relativeTo) { + return Number( + exports.temporalz_duration_compare_plain_date( + left._handle, + right._handle, + relativeTo._handle + ) + ); + } + return Number(exports.temporalz_duration_compare(left._handle, right._handle)); + } + + add(other) { + const rhs = Duration.from(other); + const handle = exports.temporalz_duration_add(this._handle, rhs._handle); + return Duration._fromHandle(requireHandle(handle)); + } + + subtract(other) { + const rhs = Duration.from(other); + const handle = exports.temporalz_duration_subtract(this._handle, rhs._handle); + return Duration._fromHandle(requireHandle(handle)); + } + + abs() { + const handle = exports.temporalz_duration_abs(this._handle); + return Duration._fromHandle(requireHandle(handle)); + } + + negated() { + const handle = exports.temporalz_duration_negated(this._handle); + return Duration._fromHandle(requireHandle(handle)); + } + + round(options = {}) { + if (options === undefined) options = {}; + if (options === null || typeof options !== "object") { + throw new TypeError("round options must be an object"); + } + const relativeTo = toRelativeTo(options); + if (!relativeTo && hasCalendarUnits(this)) { + throw new RangeError("relativeTo is required for calendar units"); + } + const smallestUnit = toUnitCode(options.smallestUnit, "smallestUnit"); + const largestUnit = toUnitCode(options.largestUnit, "largestUnit"); + const roundingMode = toRoundingModeCode(options.roundingMode); + + let roundingIncrement = 0; + if (options.roundingIncrement !== undefined) { + const inc = Number(options.roundingIncrement); + if (!Number.isFinite(inc) || !Number.isInteger(inc) || inc <= 0) { + throw new RangeError("Invalid roundingIncrement"); + } + roundingIncrement = inc; + } + + const handle = relativeTo + ? exports.temporalz_duration_round_plain_date( + this._handle, + smallestUnit, + largestUnit, + roundingMode, + roundingIncrement, + relativeTo._handle + ) + : exports.temporalz_duration_round( + this._handle, + smallestUnit, + largestUnit, + roundingMode, + roundingIncrement + ); + return Duration._fromHandle(requireHandle(handle)); + } + + total(options) { + if (!options || typeof options !== "object") { + throw new TypeError("total options must be an object"); + } + const relativeTo = toRelativeTo(options); + if (!relativeTo && hasCalendarUnits(this)) { + throw new RangeError("relativeTo is required for calendar units"); + } + const unit = toUnitCode(options.unit, "unit"); + if (unit === 255) throw new RangeError("Invalid unit"); + const result = relativeTo + ? exports.temporalz_duration_total_plain_date( + this._handle, + unit, + relativeTo._handle + ) + : exports.temporalz_duration_total(this._handle, unit); + if (!Number.isFinite(result)) throw lastError(); + return result; + } + + get sign() { + return Number(exports.temporalz_duration_sign(this._handle)); + } + + get blank() { + return exports.temporalz_duration_blank(this._handle) === 1; + } + + toString() { + return takeString(exports.temporalz_duration_to_string(this._handle)); + } + + toJSON() { + return this.toString(); + } + + toLocaleString() { + return this.toString(); + } + + valueOf() { + throw new TypeError("Cannot convert Temporal.Duration to a number"); + } + + get years() { + return Number(exports.temporalz_duration_years(this._handle)); + } + + get months() { + return Number(exports.temporalz_duration_months(this._handle)); + } + + get weeks() { + return Number(exports.temporalz_duration_weeks(this._handle)); + } + + get days() { + return Number(exports.temporalz_duration_days(this._handle)); + } + + get hours() { + return Number(exports.temporalz_duration_hours(this._handle)); + } + + get minutes() { + return Number(exports.temporalz_duration_minutes(this._handle)); + } + + get seconds() { + return Number(exports.temporalz_duration_seconds(this._handle)); + } + + get milliseconds() { + return Number(exports.temporalz_duration_milliseconds(this._handle)); + } + + get microseconds() { + return Number(exports.temporalz_duration_microseconds(this._handle)); + } + + get nanoseconds() { + return Number(exports.temporalz_duration_nanoseconds(this._handle)); + } + } + + function hasCalendarUnits(duration) { + return ( + duration.years !== 0 || + duration.months !== 0 || + duration.weeks !== 0 || + duration.days !== 0 + ); + } + + function hasYearMonthWeek(duration) { + return duration.years !== 0 || duration.months !== 0 || duration.weeks !== 0; + } + + function toRelativeTo(options) { + if (!options || options.relativeTo === undefined) return null; + const rel = options.relativeTo; + if (rel instanceof PlainDate) return rel; + if (typeof rel === "string") return PlainDate.from(rel); + if (rel && typeof rel === "object") return PlainDate.from(rel); + throw new TypeError("Invalid relativeTo"); + } + + const Temporal = { + Instant, + Duration, + Now: { + instant: unimplemented("Temporal.Now.instant"), + plainDateISO: unimplemented("Temporal.Now.plainDateISO"), + plainDateTimeISO: unimplemented("Temporal.Now.plainDateTimeISO"), + plainTimeISO: unimplemented("Temporal.Now.plainTimeISO"), + timeZoneId: unimplemented("Temporal.Now.timeZoneId"), + zonedDateTimeISO: unimplemented("Temporal.Now.zonedDateTimeISO"), + }, + PlainDate, + PlainTime: unimplemented("Temporal.PlainTime"), + PlainDateTime: unimplemented("Temporal.PlainDateTime"), + PlainYearMonth: unimplemented("Temporal.PlainYearMonth"), + PlainMonthDay: unimplemented("Temporal.PlainMonthDay"), + ZonedDateTime: unimplemented("Temporal.ZonedDateTime"), + }; + + globalThis.Temporal = Temporal; +})(); diff --git a/test/test262/root.zig b/test/test262/root.zig deleted file mode 100644 index ef6cc79..0000000 --- a/test/test262/root.zig +++ /dev/null @@ -1,3 +0,0 @@ -test { - _ = @import("duration.zig"); -} diff --git a/test/test262/runner.mjs b/test/test262/runner.mjs new file mode 100644 index 0000000..3c8526d --- /dev/null +++ b/test/test262/runner.mjs @@ -0,0 +1,35 @@ +import runTest262 from "../temporal-test262-runner/index.mjs"; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const wasmPath = + process.env.TEMPORALZ_WASM || + path.resolve(__dirname, "../../zig-out/bin/temporalz.wasm"); +const wasmBytes = fs.readFileSync(wasmPath); + +const polyfillPath = path.resolve(__dirname, "../test262/polyfill.js"); +let polyfillCode = fs.readFileSync(polyfillPath, "utf-8"); + +const bytesArray = JSON.stringify(Array.from(wasmBytes)); +// TextEncoder/TextDecoder need to be manually constructed and passed via context +// Since we can't serialize them, we inject polyfill-compatible shim versions +const injection = `globalThis.__TEMPORALZ_WASM_BYTES__ = new Uint8Array(${bytesArray}); +`; +polyfillCode = injection + polyfillCode; + +const tempPolyfillPath = path.resolve(__dirname, "../test262/polyfill-injected.js"); +fs.writeFileSync(tempPolyfillPath, polyfillCode); + +const result = runTest262({ + test262Dir: "test/temporal-test262-runner/test262", + polyfillCodeFile: tempPolyfillPath, + testGlobs: process.argv.slice(2), +}); + +fs.unlinkSync(tempPolyfillPath); + +process.exit(result ? 0 : 1);