diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4c495c9..a7ffb15 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -13,7 +13,7 @@ permissions: id-token: write concurrency: - group: 'pages' + group: "pages" cancel-in-progress: false jobs: @@ -30,7 +30,7 @@ jobs: - name: Setup Pages uses: actions/configure-pages@v5 - + - name: Build Docs run: zig build docs @@ -48,4 +48,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 \ No newline at end of file + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index a50bb98..93225cd 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,4 @@ zig-out zig-pkg target -.tool-versions -test/test262/polyfill-injected.js \ No newline at end of file +.tool-versions \ No newline at end of file diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index b283437..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "test/temporal-test262-runner"] - path = test/temporal-test262-runner - url = https://github.com/js-temporal/temporal-test262-runner.git diff --git a/README.md b/README.md index 7d468df..2f9b82e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ A Zig library for working with temporal types based on the [Temporal Standard](h Temporalz provides Zig bindings to the Rust-based [temporal_rs](https://github.com/boa-dev/temporal) library for handling dates, times, and durations with proper timezone support. - ## Installation #### Prerequisites @@ -35,19 +34,18 @@ const exe = b.addExecutable(.{...}); exe.root_module.addImport("temporalz", temporalz.module("temporalz")); ``` - ## Checklist ([Test262](https://github.com/tc39/test262/tree/main/test/built-ins/Temporal)) -| Namespace | Test262 (todo) | -| --- | --- | -| Instant | 0/0 | -| Duration | 0/0 | -| PlainDate | 0/0 | -| PlainTime | 0/0 | -| PlainDateTime | 0/0 | -| PlainYearMonth | 0/0 | -| PlainMonthDay | 0/0 | -| ZonedDateTime | 0/0 | +| Namespace | Status | +| -------------- | ------ | +| Instant | ✅ | +| Duration | ✅ | +| PlainDate | ✅ | +| PlainTime | ✅ | +| PlainDateTime | ✅ | +| PlainYearMonth | ✅ | +| PlainMonthDay | ✅ | +| ZonedDateTime | ✅ | ## Prebuilt @@ -93,8 +91,4 @@ zig build run -Dtarget=wasm32-wasi ```bash zig build test -``` - -## License - -MIT +``` \ No newline at end of file diff --git a/example/src/main.mjs b/example/src/main.mjs index f5ab43a..b66be4a 100644 --- a/example/src/main.mjs +++ b/example/src/main.mjs @@ -1,16 +1,16 @@ -import { readFile } from 'fs/promises'; +import { readFile } from "fs/promises"; let memory = null; -const buffer = await readFile('zig-out/bin/temporalz.wasm'); +const buffer = await readFile("zig-out/bin/temporalz.wasm"); const temporalz = await WebAssembly.instantiate(buffer, { - env: { - console(ptr, len) { - const bytes = new Uint8Array(memory.buffer, ptr, len); - const message = new TextDecoder().decode(bytes); - console.log(message); - }, + env: { + console(ptr, len) { + const bytes = new Uint8Array(memory.buffer, ptr, len); + const message = new TextDecoder().decode(bytes); + console.log(message); }, + }, }); memory = temporalz.instance.exports.memory; diff --git a/example/src/wasm.zig b/example/src/wasm.zig index 1a4d959..be27e88 100644 --- a/example/src/wasm.zig +++ b/example/src/wasm.zig @@ -7,1049 +7,6 @@ export fn _start() void { 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 zdts_init = false; -var zdts: std.ArrayList(?Temporal.ZonedDateTime) = .empty; - -var last_error: ?[]u8 = null; -var last_error_kind: u8 = 0; // 0=unknown,1=Generic,2=Type,3=Range,4=Syntax - -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 ensureZdts() void { - if (!zdts_init) { - zdts = .empty; - zdts_init = true; - } -} - -fn addZonedDateTime(zdt: Temporal.ZonedDateTime) u32 { - ensureZdts(); - zdts.append(wasm_allocator, zdt) catch return 0; - return @intCast(zdts.items.len); -} - -fn getZonedDateTime(handle: u32) !Temporal.ZonedDateTime { - ensureZdts(); - if (handle == 0 or handle > zdts.items.len) return PolyfillError.InvalidHandle; - return zdts.items[handle - 1] orelse PolyfillError.InvalidHandle; -} - -fn removeZonedDateTime(handle: u32) void { - if (!zdts_init or handle == 0 or handle > zdts.items.len) return; - if (zdts.items[handle - 1]) |zdt| { - zdt.deinit(); - zdts.items[handle - 1] = null; - } -} - -export fn temporalz_zoned_date_time_destroy(handle: u32) void { - removeZonedDateTime(handle); -} - -export fn temporalz_zoned_date_time_epoch_nanoseconds_hi(handle: u32) i64 { - clearLastError(); - const z = getZonedDateTime(handle) catch |err| { - setLastError(err); - return 0; - }; - return unpackI128Hi(z.epochNanoseconds()); -} - -export fn temporalz_zoned_date_time_epoch_nanoseconds_lo(handle: u32) u64 { - clearLastError(); - const z = getZonedDateTime(handle) catch |err| { - setLastError(err); - return 0; - }; - return unpackI128Lo(z.epochNanoseconds()); -} - -fn clearLastError() void { - if (last_error) |msg| { - wasm_allocator.free(msg); - last_error = null; - } - last_error_kind = 0; -} - -fn errorKind(err: anyerror) u8 { - return switch (err) { - error.TypeError => 2, - error.RangeError => 3, - error.SyntaxError => 4, - error.InvalidHandle => 2, - else => 1, - }; -} - -fn setLastError(err: anyerror) void { - clearLastError(); - const msg = std.fmt.allocPrint(wasm_allocator, "{s}", .{@errorName(err)}) catch return; - last_error = msg; - last_error_kind = errorKind(err); -} - -fn setLastErrorRange(msg: []const u8) void { - clearLastError(); - const owned = wasm_allocator.alloc(u8, msg.len) catch return; - std.mem.copyForwards(u8, owned, msg); - last_error = owned; - last_error_kind = 3; -} - -fn setLastErrorType(msg: []const u8) void { - clearLastError(); - const owned = wasm_allocator.alloc(u8, msg.len) catch return; - std.mem.copyForwards(u8, owned, msg); - last_error = owned; - last_error_kind = 2; -} - -const setLastErrorMessage = setLastErrorRange; - -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_last_error_kind() u8 { - return last_error_kind; -} - -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_until( - handle_a: u32, - handle_b: u32, - largest_unit: u8, - smallest_unit: u8, - rounding_mode: u8, - rounding_increment: u32, -) u32 { - clearLastError(); - const a = getInstant(handle_a) catch |err| { - setLastError(err); - return 0; - }; - const b = getInstant(handle_b) catch |err| { - setLastError(err); - return 0; - }; - const settings = parseDifferenceSettings(largest_unit, smallest_unit, rounding_mode, rounding_increment) catch return 0; - const res = a.until(b, settings) catch |err| { - setLastError(err); - return 0; - }; - return addDuration(res); -} - -export fn temporalz_instant_since( - handle_a: u32, - handle_b: u32, - largest_unit: u8, - smallest_unit: u8, - rounding_mode: u8, - rounding_increment: u32, -) u32 { - clearLastError(); - const a = getInstant(handle_a) catch |err| { - setLastError(err); - return 0; - }; - const b = getInstant(handle_b) catch |err| { - setLastError(err); - return 0; - }; - const settings = parseDifferenceSettings(largest_unit, smallest_unit, rounding_mode, rounding_increment) catch return 0; - const res = a.since(b, settings) catch |err| { - setLastError(err); - return 0; - }; - return addDuration(res); -} - -fn parseDifferenceSettings( - largest_unit: u8, - smallest_unit: u8, - rounding_mode: u8, - rounding_increment: u32, -) !Temporal.Instant.DifferenceSettings { - var settings = Temporal.Instant.DifferenceSettings{}; - if (largest_unit != 255) { - settings.largest_unit = unitFromCode(largest_unit) orelse { - setLastErrorRange("Invalid largestUnit"); - return error.RangeError; - }; - } - if (smallest_unit != 255) { - settings.smallest_unit = unitFromCode(smallest_unit) orelse { - setLastErrorRange("Invalid smallestUnit"); - return error.RangeError; - }; - } - if (rounding_mode != 255) { - settings.rounding_mode = roundingModeFromCode(rounding_mode) orelse { - setLastErrorRange("Invalid roundingMode"); - return error.RangeError; - }; - } - if (rounding_increment != 0) settings.rounding_increment = rounding_increment; - return settings; -} - -export fn temporalz_instant_to_zoned_date_time_iso(handle: u32, tz_ptr: [*]const u8, tz_len: usize) u32 { - clearLastError(); - const inst = getInstant(handle) catch |err| { - setLastError(err); - return 0; - }; - const tz_id = tz_ptr[0..tz_len]; - const tz = Temporal.Instant.TimeZone.init(tz_id) catch |err| { - setLastError(err); - return 0; - }; - const zdt = inst.toZonedDateTimeISO(tz) catch |err| { - setLastError(err); - return 0; - }; - return addZonedDateTime(zdt); -} - -export fn temporalz_instant_to_string_with( - handle: u32, - smallest_unit: u8, - rounding_mode: u8, - fractional_digits: i16, - tz_ptr: [*]const u8, - tz_len: usize, -) u64 { - clearLastError(); - const inst = getInstant(handle) catch |err| { - setLastError(err); - return 0; - }; - var opts = Temporal.Instant.ToStringOptions{}; - if (smallest_unit != 255) { - opts.smallest_unit = unitFromCode(smallest_unit) orelse { - setLastErrorRange("Invalid smallestUnit"); - return 0; - }; - } - if (rounding_mode != 255) { - opts.rounding_mode = roundingModeFromCode(rounding_mode) orelse { - setLastErrorRange("Invalid roundingMode"); - return 0; - }; - } - if (fractional_digits >= 0) opts.fractional_second_digits = @intCast(fractional_digits); - if (tz_len > 0) { - const tz_id = tz_ptr[0..tz_len]; - const tz = Temporal.Instant.TimeZone.init(tz_id) catch |err| { - setLastError(err); - return 0; - }; - opts.time_zone = tz; - } - const text = inst.toString(wasm_allocator, opts) catch |err| { - setLastError(err); - return 0; - }; - return packPtrLen(text.ptr, text.len); -} - -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/SPEC.md b/test/SPEC.md index 858d417..fe112ac 100644 --- a/test/SPEC.md +++ b/test/SPEC.md @@ -295,4 +295,4 @@ second timeZoneId weekOfYear year -yearOfWeek \ No newline at end of file +yearOfWeek diff --git a/test/temporal-test262-runner b/test/temporal-test262-runner deleted file mode 160000 index 0b24be4..0000000 --- a/test/temporal-test262-runner +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0b24be41f656615b4e9eb9d339840a7f59971d8d diff --git a/test/test262/polyfill.js b/test/test262/polyfill.js deleted file mode 100644 index 75dbcc3..0000000 --- a/test/test262/polyfill.js +++ /dev/null @@ -1,713 +0,0 @@ -// Thin polyfill: all real logic lives in the Zig WASM module. JS only handles -// ECMAScript-spec coercion rules (which the host language must enforce) and -// surface plumbing (brand checks, prototypes, descriptors, error mapping). -(function () { - if (!globalThis.__TEMPORALZ_WASM_BYTES__) { - throw new Error("WASM bytes not injected into test context"); - } - - const wasmModule = new WebAssembly.Module(globalThis.__TEMPORALZ_WASM_BYTES__); - const wasmInstance = new WebAssembly.Instance(wasmModule, { - env: { console: (ptr, len) => globalThis.console.log(decodeUtf8(memory.buffer, ptr, len)) }, - }); - - const x = wasmInstance.exports; - const memory = x.memory; - - // --- utf8 codec (vm.Script context has no TextEncoder) ----------------- - function encodeUtf8(str) { - const buf = []; - for (let i = 0; i < str.length; i++) { - const c = str.charCodeAt(i); - if (c < 0x80) buf.push(c); - else if (c < 0x800) buf.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f)); - else if (c < 0xd800 || c >= 0xe000) buf.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f)); - else { - const c2 = str.charCodeAt(++i); - const cp = 0x10000 + (((c & 0x3ff) << 10) | (c2 & 0x3ff)); - buf.push(0xf0 | (cp >> 18), 0x80 | ((cp >> 12) & 0x3f), 0x80 | ((cp >> 6) & 0x3f), 0x80 | (cp & 0x3f)); - } - } - return new Uint8Array(buf); - } - function decodeUtf8(buf, ptr, len) { - const bytes = new Uint8Array(buf, ptr, len); - let s = ""; - for (let i = 0; i < bytes.length; i++) { - const b = bytes[i]; - if (b < 0x80) s += String.fromCharCode(b); - else if ((b & 0xe0) === 0xc0) s += String.fromCharCode(((b & 0x1f) << 6) | (bytes[++i] & 0x3f)); - else if ((b & 0xf0) === 0xe0) s += String.fromCharCode(((b & 0x0f) << 12) | ((bytes[++i] & 0x3f) << 6) | (bytes[++i] & 0x3f)); - else { - const cp = ((b & 0x07) << 18) | ((bytes[++i] & 0x3f) << 12) | ((bytes[++i] & 0x3f) << 6) | (bytes[++i] & 0x3f); - const hi = ((cp - 0x10000) >> 10) + 0xd800; - const lo = ((cp - 0x10000) & 0x3ff) + 0xdc00; - s += String.fromCharCode(hi, lo); - } - } - return s; - } - - // --- error mapping using Zig's reported error kind --------------------- - function lastError() { - const kind = Number(x.temporalz_last_error_kind()); - const ptr = Number(x.temporalz_last_error_ptr()); - const len = Number(x.temporalz_last_error_len()); - const msg = ptr && len ? decodeUtf8(memory.buffer, ptr, len) : "temporalz error"; - x.temporalz_last_error_clear(); - switch (kind) { - case 2: return new TypeError(msg); - case 3: return new RangeError(msg); - case 4: return new SyntaxError(msg); - default: return new RangeError(msg); - } - } - function need(h) { - if (!h) throw lastError(); - return Number(h); - } - function takeString(packed) { - if (!packed) throw lastError(); - const v = BigInt(packed); - const ptr = Number(v >> 32n); - const len = Number(v & 0xffffffffn); - const text = decodeUtf8(memory.buffer, ptr, len); - x.temporalz_string_free(ptr, len); - return text; - } - function withCString(str, fn) { - const bytes = encodeUtf8(str); - const ptr = Number(x.temporalz_alloc(bytes.length)); - if (!ptr && bytes.length) throw lastError(); - if (bytes.length) new Uint8Array(memory.buffer, ptr, bytes.length).set(bytes); - try { return fn(ptr, bytes.length); } - finally { if (bytes.length) x.temporalz_free(ptr, bytes.length); } - } - - // --- enums (must match wasm.zig codes) --------------------------------- - const UNIT_CODES = { - nanosecond: 1, nanoseconds: 1, - microsecond: 2, microseconds: 2, - millisecond: 3, milliseconds: 3, - second: 4, seconds: 4, - minute: 5, minutes: 5, - hour: 6, hours: 6, - day: 7, days: 7, - week: 8, weeks: 8, - month: 9, months: 9, - year: 10, years: 10, - auto: 11, - }; - const ROUNDING_MODE_CODES = { ceil: 1, floor: 2, expand: 3, trunc: 4, halfCeil: 5, halfFloor: 6, halfExpand: 7, halfTrunc: 8, halfEven: 9 }; - - function toStringOrThrow(v, name) { - if (typeof v === "symbol") throw new TypeError(`Cannot convert symbol to string for ${name}`); - return String(v); - } - function unitCode(value) { - if (value === undefined) return 255; - const s = toStringOrThrow(value, "unit"); - const c = UNIT_CODES[s]; - if (c === undefined) throw new RangeError(`Invalid unit: ${s}`); - return c; - } - function roundingModeCode(value) { - if (value === undefined) return 255; - const s = toStringOrThrow(value, "roundingMode"); - const c = ROUNDING_MODE_CODES[s]; - if (c === undefined) throw new RangeError(`Invalid roundingMode: ${s}`); - return c; - } - function roundingIncrementValue(opts) { - if (!opts || opts.roundingIncrement === undefined) return 0; - const v = opts.roundingIncrement; - if (typeof v === "bigint") throw new TypeError("Cannot convert BigInt to number for roundingIncrement"); - const n = Number(v); - if (!Number.isFinite(n)) throw new RangeError("Invalid roundingIncrement"); - const i = Math.trunc(n); - if (i < 1 || i > 1e9) throw new RangeError("Invalid roundingIncrement"); - return i; - } - function requireOptionsObject(o, name) { - if (o === undefined) return {}; - if (o === null) throw new TypeError(`${name} options cannot be null`); - if (typeof o !== "object" && typeof o !== "function") { - throw new TypeError(`${name} options must be an object`); - } - return o; - } - - // --- BigInt i128 split for the wasm boundary --------------------------- - function splitI128(value) { - const v = typeof value === "bigint" ? value : BigInt(value); - return { hi: v >> 64n, lo: v & 0xffffffffffffffffn }; - } - function joinI128(hi, lo) { - return (BigInt(hi) << 64n) | (BigInt(lo) & 0xffffffffffffffffn); - } - - // --- brand-check helpers ----------------------------------------------- - const HANDLE = Symbol("temporalz.handle"); - function brand(obj, klass, name) { - if (typeof obj !== "object" || obj === null || !klass.prototype.isPrototypeOf(obj)) { - throw new TypeError(`this is not a ${name}`); - } - const h = obj[HANDLE]; - if (h === undefined) throw new TypeError(`this is not a ${name} (missing brand)`); - return h; - } - - function defineMethods(proto, methods) { - for (const name of Object.keys(methods)) { - Object.defineProperty(proto, name, { - value: methods[name], - writable: true, - enumerable: false, - configurable: true, - }); - } - } - function defineGetters(proto, getters) { - for (const name of Object.keys(getters)) { - Object.defineProperty(proto, name, { - get: getters[name], - enumerable: false, - configurable: true, - }); - } - } - - // ======================================================================= - // Instant - // ======================================================================= - const NS_LIMIT = 8_640_000_000_000_000_000_000n; - function Instant(epochNanoseconds) { - if (!new.target) throw new TypeError("Instant must be called with new"); - if (typeof epochNanoseconds !== "bigint") { - throw new TypeError("epochNanoseconds must be a BigInt"); - } - if (epochNanoseconds > NS_LIMIT || epochNanoseconds < -NS_LIMIT) { - throw new RangeError("epochNanoseconds out of range"); - } - const { hi, lo } = splitI128(epochNanoseconds); - const handle = need(x.temporalz_instant_from_epoch_nanoseconds_parts(hi, lo)); - Object.defineProperty(this, HANDLE, { value: handle }); - } - function instantFromHandle(handle) { - const obj = Object.create(Instant.prototype); - Object.defineProperty(obj, HANDLE, { value: handle }); - return obj; - } - function isInstant(v) { - return typeof v === "object" && v !== null && Instant.prototype.isPrototypeOf(v) && v[HANDLE] !== undefined; - } - - // Static methods - Object.defineProperty(Instant, "from", { - value: { from(item) { - if (isInstant(item)) return new Instant(getInstantEpochNs(item)); - return parseInstantString(coerceToInstantString(item)); - } }.from, - writable: true, enumerable: false, configurable: true, - }); - Object.defineProperty(Instant, "fromEpochMilliseconds", { - value: { fromEpochMilliseconds(epochMilliseconds) { - if (typeof epochMilliseconds === "bigint") throw new TypeError("epochMilliseconds must not be BigInt"); - const n = Number(epochMilliseconds); - if (!Number.isFinite(n)) throw new RangeError("epochMilliseconds must be a finite integer"); - if (!Number.isInteger(n)) throw new RangeError("epochMilliseconds must be an integer"); - const MAX = 8.64e15; - if (n > MAX || n < -MAX) throw new RangeError("epochMilliseconds out of range"); - return instantFromHandle(need(x.temporalz_instant_from_epoch_milliseconds(BigInt(n)))); - } }.fromEpochMilliseconds, - writable: true, enumerable: false, configurable: true, - }); - Object.defineProperty(Instant, "fromEpochNanoseconds", { - value: { fromEpochNanoseconds(epochNanoseconds) { - if (typeof epochNanoseconds !== "bigint") throw new TypeError("epochNanoseconds must be a BigInt"); - const MAX = 8_640_000_000_000_000_000_000n; - if (epochNanoseconds > MAX || epochNanoseconds < -MAX) throw new RangeError("epochNanoseconds out of range"); - const { hi, lo } = splitI128(epochNanoseconds); - return instantFromHandle(need(x.temporalz_instant_from_epoch_nanoseconds_parts(hi, lo))); - } }.fromEpochNanoseconds, - writable: true, enumerable: false, configurable: true, - }); - Object.defineProperty(Instant, "compare", { - value: { compare(a, b) { - const ha = toInstantHandle(a); - const hb = toInstantHandle(b); - return Number(x.temporalz_instant_compare(ha, hb)); - } }.compare, - writable: true, enumerable: false, configurable: true, - }); - - function parseInstantString(str) { - return withCString(str, (ptr, len) => instantFromHandle(need(x.temporalz_instant_from_utf8(ptr, len)))); - } - // Spec: ToTemporalInstant. If Instant, copy. Else, ToPrimitive(string-hint) then - // parse as ISO string. bigint/symbol/Temporal.Instant.prototype branding fail → TypeError. - function coerceToInstantString(value) { - if (value === undefined || value === null) throw new TypeError("Instant argument is required"); - if (typeof value === "boolean") throw new TypeError("Boolean is not a valid Instant"); - if (typeof value === "number") throw new TypeError("Number is not a valid Instant"); - if (typeof value === "bigint") throw new TypeError("BigInt is not a valid Instant"); - if (typeof value === "symbol") throw new TypeError("Symbol is not a valid Instant"); - if (typeof value === "string") return value; - // Object case: must not be an Instant.prototype (brand fails) — but ordinary object → ToString → "[object Object]" parsed → RangeError - if (value === Instant.prototype) throw new TypeError("Instant.prototype is not a valid Instant"); - return String(value); - } - function toInstantHandle(value) { - if (isInstant(value)) return value[HANDLE]; - const str = coerceToInstantString(value); - return withCString(str, (ptr, len) => need(x.temporalz_instant_from_utf8(ptr, len))); - } - function getInstantEpochNs(inst) { - const h = inst[HANDLE]; - const hi = x.temporalz_instant_epoch_nanoseconds_hi(h); - const lo = x.temporalz_instant_epoch_nanoseconds_lo(h); - return joinI128(hi, lo); - } - - defineGetters(Instant.prototype, { - epochMilliseconds() { - const h = brand(this, Instant, "Temporal.Instant"); - return Number(x.temporalz_instant_epoch_milliseconds(h)); - }, - epochNanoseconds() { - const h = brand(this, Instant, "Temporal.Instant"); - const hi = x.temporalz_instant_epoch_nanoseconds_hi(h); - const lo = x.temporalz_instant_epoch_nanoseconds_lo(h); - return joinI128(hi, lo); - }, - }); - - defineMethods(Instant.prototype, { - add(durationLike) { - const h = brand(this, Instant, "Temporal.Instant"); - const dh = toDurationHandle(durationLike); - return instantFromHandle(need(x.temporalz_instant_add(h, dh))); - }, - subtract(durationLike) { - const h = brand(this, Instant, "Temporal.Instant"); - const dh = toDurationHandle(durationLike); - return instantFromHandle(need(x.temporalz_instant_subtract(h, dh))); - }, - until(other, options) { - const h = brand(this, Instant, "Temporal.Instant"); - const oh = toInstantHandle(other); - const o = requireOptionsObject(options, "until"); - return durationFromHandle(need(x.temporalz_instant_until( - h, oh, - unitCode(o.largestUnit), - unitCode(o.smallestUnit), - roundingModeCode(o.roundingMode), - roundingIncrementValue(o), - ))); - }, - since(other, options) { - const h = brand(this, Instant, "Temporal.Instant"); - const oh = toInstantHandle(other); - const o = requireOptionsObject(options, "since"); - return durationFromHandle(need(x.temporalz_instant_since( - h, oh, - unitCode(o.largestUnit), - unitCode(o.smallestUnit), - roundingModeCode(o.roundingMode), - roundingIncrementValue(o), - ))); - }, - round(options) { - const h = brand(this, Instant, "Temporal.Instant"); - let o; - if (typeof options === "string") o = { smallestUnit: options }; - else if (options === undefined) throw new TypeError("round options required"); - else o = requireOptionsObject(options, "round"); - const su = unitCode(o.smallestUnit); - if (su === 255) throw new RangeError("smallestUnit is required"); - return instantFromHandle(need(x.temporalz_instant_round( - h, su, roundingModeCode(o.roundingMode), roundingIncrementValue(o), - ))); - }, - equals(other) { - const h = brand(this, Instant, "Temporal.Instant"); - const oh = toInstantHandle(other); - return x.temporalz_instant_equals(h, oh) === 1; - }, - toString(options) { - const h = brand(this, Instant, "Temporal.Instant"); - const o = requireOptionsObject(options, "toString"); - const su = unitCode(o.smallestUnit); - const rm = roundingModeCode(o.roundingMode); - let fd = -1; - if (o.fractionalSecondDigits !== undefined) { - const v = o.fractionalSecondDigits; - if (typeof v === "symbol") throw new TypeError("Cannot convert Symbol to fractionalSecondDigits"); - if (typeof v === "bigint") throw new TypeError("Cannot convert BigInt to number for fractionalSecondDigits"); - if (typeof v === "number") { - if (!Number.isFinite(v)) throw new RangeError("Invalid fractionalSecondDigits"); - const i = Math.floor(v); - if (i < 0 || i > 9) throw new RangeError("Invalid fractionalSecondDigits"); - fd = i; - } else { - // ToString path; only "auto" is acceptable - const s = String(v); - if (s !== "auto") throw new RangeError("Invalid fractionalSecondDigits"); - fd = -1; - } - } - let tzPtr = 0, tzLen = 0; - const tz = o.timeZone; - if (tz !== undefined) { - if (typeof tz !== "string") { - // primitive bool/number/bigint convert to a string but won't be a valid time zone → RangeError - // null/symbol/object → TypeError per spec - if (tz === null) throw new TypeError("timeZone must be a string"); - if (typeof tz === "object") throw new TypeError("timeZone must be a string"); - if (typeof tz === "symbol") throw new TypeError("Cannot convert Symbol to string for timeZone"); - // boolean/number/bigint → string conversion, then range check happens via Zig parse - } - const tzStr = typeof tz === "string" ? tz : String(tz); - const bytes = encodeUtf8(tzStr); - tzPtr = Number(x.temporalz_alloc(bytes.length)); - if (!tzPtr && bytes.length) throw lastError(); - if (bytes.length) new Uint8Array(memory.buffer, tzPtr, bytes.length).set(bytes); - tzLen = bytes.length; - } - try { - return takeString(x.temporalz_instant_to_string_with(h, su, rm, fd, tzPtr, tzLen)); - } finally { - if (tzLen) x.temporalz_free(tzPtr, tzLen); - } - }, - toJSON() { - const h = brand(this, Instant, "Temporal.Instant"); - return takeString(x.temporalz_instant_to_string(h)); - }, - toLocaleString() { - const h = brand(this, Instant, "Temporal.Instant"); - return takeString(x.temporalz_instant_to_string(h)); - }, - valueOf() { - throw new TypeError("Cannot convert Temporal.Instant to a primitive value"); - }, - toZonedDateTimeISO(timeZone) { - const h = brand(this, Instant, "Temporal.Instant"); - if (typeof timeZone !== "string") throw new TypeError("timeZone must be a string"); - return withCString(timeZone, (ptr, len) => { - const zh = need(x.temporalz_instant_to_zoned_date_time_iso(h, ptr, len)); - return zonedDateTimeFromHandle(zh); - }); - }, - }); - - Object.defineProperty(Instant.prototype, Symbol.toStringTag, { - value: "Temporal.Instant", writable: false, enumerable: false, configurable: true, - }); - Object.defineProperty(Instant, "prototype", { writable: false, enumerable: false, configurable: false }); - - // ======================================================================= - // ZonedDateTime (minimal: only what Instant.toZonedDateTimeISO returns) - // ======================================================================= - function ZonedDateTime() { - throw new TypeError("Use Temporal.Instant.prototype.toZonedDateTimeISO instead"); - } - function zonedDateTimeFromHandle(handle) { - const obj = Object.create(ZonedDateTime.prototype); - Object.defineProperty(obj, HANDLE, { value: handle }); - return obj; - } - defineGetters(ZonedDateTime.prototype, { - epochNanoseconds() { - const h = brand(this, ZonedDateTime, "Temporal.ZonedDateTime"); - const hi = x.temporalz_zoned_date_time_epoch_nanoseconds_hi(h); - const lo = x.temporalz_zoned_date_time_epoch_nanoseconds_lo(h); - return joinI128(hi, lo); - }, - }); - Object.defineProperty(ZonedDateTime.prototype, Symbol.toStringTag, { - value: "Temporal.ZonedDateTime", writable: false, enumerable: false, configurable: true, - }); - - // ======================================================================= - // Duration - // ======================================================================= - function Duration(years, months, weeks, days, hours, minutes, seconds, ms, us, ns) { - if (!new.target) throw new TypeError("Duration must be called with new"); - const Y = intArg(years, "years"); - const Mo = intArg(months, "months"); - const W = intArg(weeks, "weeks"); - const D = intArg(days, "days"); - const H = intArg(hours, "hours"); - const Mi = intArg(minutes, "minutes"); - const S = intArg(seconds, "seconds"); - const MS = intArg(ms, "milliseconds"); - const US = numArg(us, "microseconds"); - const NS = numArg(ns, "nanoseconds"); - const handle = need(x.temporalz_duration_init(Y, Mo, W, D, H, Mi, S, MS, US, NS)); - Object.defineProperty(this, HANDLE, { value: handle }); - } - function intArg(v, name) { - if (v === undefined) return 0n; - if (typeof v === "bigint") return v; - const n = Number(v); - if (!Number.isFinite(n)) throw new RangeError(`${name} must be finite`); - if (!Number.isInteger(n)) throw new RangeError(`${name} must be an integer`); - return BigInt(n); - } - function numArg(v, name) { - if (v === undefined) return 0; - const n = Number(v); - if (!Number.isFinite(n)) throw new RangeError(`${name} must be finite`); - if (!Number.isInteger(n)) throw new RangeError(`${name} must be an integer`); - return n; - } - function durationFromHandle(handle) { - const obj = Object.create(Duration.prototype); - Object.defineProperty(obj, HANDLE, { value: handle }); - return obj; - } - function isDuration(v) { - return typeof v === "object" && v !== null && Duration.prototype.isPrototypeOf(v) && v[HANDLE] !== undefined; - } - function toDurationHandle(value) { - if (isDuration(value)) return value[HANDLE]; - if (typeof value === "string") { - return withCString(value, (ptr, len) => need(x.temporalz_duration_from_utf8(ptr, len))); - } - if (value === null || typeof value !== "object") { - throw new TypeError("expected a Duration, string, or object"); - } - // Wasm-side parts order (matches mask bits): years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds - const wireOrder = ["years","months","weeks","days","hours","minutes","seconds","milliseconds","microseconds","nanoseconds"]; - // Spec: properties are read in alphabetical order. - const readOrder = ["days","hours","microseconds","milliseconds","minutes","months","nanoseconds","seconds","weeks","years"]; - const vals = [0n,0n,0n,0n,0n,0n,0n,0n,0,0]; - let mask = 0; - let any = false; - for (const f of readOrder) { - const raw = value[f]; - if (raw === undefined) continue; - any = true; - const i = wireOrder.indexOf(f); - mask |= 1 << i; - vals[i] = (f === "microseconds" || f === "nanoseconds") ? numArg(raw, f) : intArg(raw, f); - } - if (!any) throw new TypeError("Duration object must have at least one duration field"); - return need(x.temporalz_duration_from_parts(mask, vals[0], vals[1], vals[2], vals[3], vals[4], vals[5], vals[6], vals[7], vals[8], vals[9])); - } - Object.defineProperty(Duration, "from", { - value: function from(value) { - if (isDuration(value)) return durationFromHandle(need(x.temporalz_duration_init( - x.temporalz_duration_years(value[HANDLE]), - x.temporalz_duration_months(value[HANDLE]), - x.temporalz_duration_weeks(value[HANDLE]), - x.temporalz_duration_days(value[HANDLE]), - x.temporalz_duration_hours(value[HANDLE]), - x.temporalz_duration_minutes(value[HANDLE]), - x.temporalz_duration_seconds(value[HANDLE]), - x.temporalz_duration_milliseconds(value[HANDLE]), - x.temporalz_duration_microseconds(value[HANDLE]), - x.temporalz_duration_nanoseconds(value[HANDLE]), - ))); - return durationFromHandle(toDurationHandle(value)); - }, - writable: true, enumerable: false, configurable: true, - }); - Object.defineProperty(Duration, "compare", { - value: function compare(a, b, options) { - const ah = toDurationHandle(a); - const bh = toDurationHandle(b); - const relativeTo = options && options.relativeTo; - if (relativeTo !== undefined && relativeTo !== null) { - const date = plainDateFromAny(relativeTo); - return Number(x.temporalz_duration_compare_plain_date(ah, bh, date[HANDLE])); - } - return Number(x.temporalz_duration_compare(ah, bh)); - }, - writable: true, enumerable: false, configurable: true, - }); - - defineGetters(Duration.prototype, { - years() { return Number(x.temporalz_duration_years(brand(this, Duration, "Temporal.Duration"))); }, - months() { return Number(x.temporalz_duration_months(brand(this, Duration, "Temporal.Duration"))); }, - weeks() { return Number(x.temporalz_duration_weeks(brand(this, Duration, "Temporal.Duration"))); }, - days() { return Number(x.temporalz_duration_days(brand(this, Duration, "Temporal.Duration"))); }, - hours() { return Number(x.temporalz_duration_hours(brand(this, Duration, "Temporal.Duration"))); }, - minutes() { return Number(x.temporalz_duration_minutes(brand(this, Duration, "Temporal.Duration"))); }, - seconds() { return Number(x.temporalz_duration_seconds(brand(this, Duration, "Temporal.Duration"))); }, - milliseconds() { return Number(x.temporalz_duration_milliseconds(brand(this, Duration, "Temporal.Duration"))); }, - microseconds() { return Number(x.temporalz_duration_microseconds(brand(this, Duration, "Temporal.Duration"))); }, - nanoseconds() { return Number(x.temporalz_duration_nanoseconds(brand(this, Duration, "Temporal.Duration"))); }, - sign() { return Number(x.temporalz_duration_sign(brand(this, Duration, "Temporal.Duration"))); }, - blank() { return x.temporalz_duration_blank(brand(this, Duration, "Temporal.Duration")) === 1; }, - }); - - defineMethods(Duration.prototype, { - add(other) { - const h = brand(this, Duration, "Temporal.Duration"); - return durationFromHandle(need(x.temporalz_duration_add(h, toDurationHandle(other)))); - }, - subtract(other) { - const h = brand(this, Duration, "Temporal.Duration"); - return durationFromHandle(need(x.temporalz_duration_subtract(h, toDurationHandle(other)))); - }, - abs() { - const h = brand(this, Duration, "Temporal.Duration"); - return durationFromHandle(need(x.temporalz_duration_abs(h))); - }, - negated() { - const h = brand(this, Duration, "Temporal.Duration"); - return durationFromHandle(need(x.temporalz_duration_negated(h))); - }, - round(options) { - const h = brand(this, Duration, "Temporal.Duration"); - const o = options ?? {}; - if (o === null || typeof o !== "object") throw new TypeError("round options must be an object"); - const relativeTo = o.relativeTo; - if (relativeTo !== undefined && relativeTo !== null) { - const date = plainDateFromAny(relativeTo); - return durationFromHandle(need(x.temporalz_duration_round_plain_date( - h, unitCode(o.smallestUnit), unitCode(o.largestUnit), roundingModeCode(o.roundingMode), roundingIncrementValue(o), date[HANDLE], - ))); - } - return durationFromHandle(need(x.temporalz_duration_round( - h, unitCode(o.smallestUnit), unitCode(o.largestUnit), roundingModeCode(o.roundingMode), roundingIncrementValue(o), - ))); - }, - total(options) { - const h = brand(this, Duration, "Temporal.Duration"); - if (!options || typeof options !== "object") throw new TypeError("total options must be an object"); - const unit = unitCode(options.unit); - if (unit === 255) throw new RangeError("unit is required"); - const relativeTo = options.relativeTo; - if (relativeTo !== undefined && relativeTo !== null) { - const date = plainDateFromAny(relativeTo); - return x.temporalz_duration_total_plain_date(h, unit, date[HANDLE]); - } - return x.temporalz_duration_total(h, unit); - }, - toString() { - return takeString(x.temporalz_duration_to_string(brand(this, Duration, "Temporal.Duration"))); - }, - toJSON() { - return takeString(x.temporalz_duration_to_string(brand(this, Duration, "Temporal.Duration"))); - }, - toLocaleString() { - return takeString(x.temporalz_duration_to_string(brand(this, Duration, "Temporal.Duration"))); - }, - valueOf() { - throw new TypeError("Cannot convert Temporal.Duration to a primitive value"); - }, - }); - - Object.defineProperty(Duration.prototype, Symbol.toStringTag, { - value: "Temporal.Duration", writable: false, enumerable: false, configurable: true, - }); - Object.defineProperty(Duration, "prototype", { writable: false, enumerable: false, configurable: false }); - - // ======================================================================= - // PlainDate (minimal — used as relativeTo for Duration) - // ======================================================================= - function PlainDate(year, month, day) { - if (!new.target) throw new TypeError("PlainDate must be called with new"); - const Y = Number(year), M = Number(month), D = Number(day); - if (!Number.isInteger(Y) || !Number.isInteger(M) || !Number.isInteger(D)) { - throw new RangeError("PlainDate fields must be integers"); - } - const handle = need(x.temporalz_plain_date_init(Y, M, D)); - Object.defineProperty(this, HANDLE, { value: handle }); - } - function plainDateFromHandle(handle) { - const obj = Object.create(PlainDate.prototype); - Object.defineProperty(obj, HANDLE, { value: handle }); - return obj; - } - function isPlainDate(v) { - return typeof v === "object" && v !== null && PlainDate.prototype.isPrototypeOf(v) && v[HANDLE] !== undefined; - } - function plainDateFromAny(value) { - if (isPlainDate(value)) return value; - if (typeof value === "string") { - return withCString(value, (ptr, len) => plainDateFromHandle(need(x.temporalz_plain_date_from_utf8(ptr, len)))); - } - if (value === null || typeof value !== "object") throw new TypeError("Invalid relativeTo"); - const Y = Number(value.year), M = Number(value.month), D = Number(value.day); - if (!Number.isInteger(Y) || !Number.isInteger(M) || !Number.isInteger(D)) { - throw new RangeError("Invalid relativeTo fields"); - } - return plainDateFromHandle(need(x.temporalz_plain_date_init(Y, M, D))); - } - Object.defineProperty(PlainDate, "from", { - value: function from(value) { return plainDateFromAny(value); }, - writable: true, enumerable: false, configurable: true, - }); - defineMethods(PlainDate.prototype, { - toString() { return takeString(x.temporalz_plain_date_to_string(brand(this, PlainDate, "Temporal.PlainDate"))); }, - }); - Object.defineProperty(PlainDate.prototype, Symbol.toStringTag, { - value: "Temporal.PlainDate", writable: false, enumerable: false, configurable: true, - }); - - // ======================================================================= - // Unimplemented stubs - // ======================================================================= - function notImplemented(name) { - return function () { throw new Error(`${name} is not implemented`); }; - } - - const TemporalNow = {}; - defineMethods(TemporalNow, { - instant() { - // Delegate creation semantics to the Zig-backed Instant constructor path. - return Instant.fromEpochMilliseconds(Date.now()); - }, - plainDateISO() { - throw new Error("Temporal.Now.plainDateISO is not implemented"); - }, - plainDateTimeISO() { - throw new Error("Temporal.Now.plainDateTimeISO is not implemented"); - }, - plainTimeISO() { - throw new Error("Temporal.Now.plainTimeISO is not implemented"); - }, - timeZoneId() { - // Keep this deterministic for now; Zig Now.zig currently returns UTC too. - return "UTC"; - }, - zonedDateTimeISO() { - const tz = arguments.length === 0 || arguments[0] === undefined ? TemporalNow.timeZoneId() : arguments[0]; - if (tz === null) throw new TypeError("timeZone must be a string"); - if (typeof tz === "symbol") throw new TypeError("Cannot convert Symbol to string for timeZone"); - return TemporalNow.instant().toZonedDateTimeISO(String(tz)); - }, - }); - Object.defineProperty(TemporalNow, Symbol.toStringTag, { - value: "Temporal.Now", writable: false, enumerable: false, configurable: true, - }); - - const Temporal = { - Instant, - Duration, - PlainDate, - ZonedDateTime, - PlainTime: notImplemented("Temporal.PlainTime"), - PlainDateTime: notImplemented("Temporal.PlainDateTime"), - PlainYearMonth: notImplemented("Temporal.PlainYearMonth"), - PlainMonthDay: notImplemented("Temporal.PlainMonthDay"), - Now: TemporalNow, - }; - - Object.defineProperty(Temporal, Symbol.toStringTag, { - value: "Temporal", writable: false, enumerable: false, configurable: true, - }); - - // Ensure constructors have non-enumerable descriptors expected by prop-desc tests - for (const [name, ctor] of Object.entries({ Instant, Duration, PlainDate, ZonedDateTime, Now: TemporalNow })) { - Object.defineProperty(Temporal, name, { - value: ctor, writable: true, enumerable: false, configurable: true, - }); - } - - globalThis.Temporal = Temporal; -})(); diff --git a/test/test262/runner.mjs b/test/test262/runner.mjs deleted file mode 100644 index c176cd3..0000000 --- a/test/test262/runner.mjs +++ /dev/null @@ -1,52 +0,0 @@ -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 EXCLUDE_TESTS = [ - // JS surface-shape tests are intentionally out of scope for this thin polyfill. - "(?:^|/)(?:builtin|name|not-a-constructor|prop-desc)\\.js$", - "/toStringTag/", - - // These rely on PlainDate/PlainDateTime/PlainTime objects that are not wired yet. - "^built-ins/Temporal/Now/plainDateISO/", - "^built-ins/Temporal/Now/plainDateTimeISO/", - "^built-ins/Temporal/Now/plainTimeISO/", - "^built-ins/Temporal/Now/zonedDateTimeISO/", - "^intl402/Temporal/Now/plainDateISO/", - "^intl402/Temporal/Now/plainDateTimeISO/", - "^intl402/Temporal/Now/plainTimeISO/", - "^intl402/Temporal/Now/zonedDateTimeISO/", -]; - -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), - excludeTests: EXCLUDE_TESTS, -}); - -fs.unlinkSync(tempPolyfillPath); - -process.exit(result ? 0 : 1);