Skip to content

runtime: match Date.UTC defaults, year rebasing, and invalid-argument semantics #2826

Description

@andrewtdiz

Summary

Date.UTC is implemented, but its argument normalization does not match Node for important edge cases:

  • missing day defaults to 1, not 0;
  • no arguments should return NaN because the required year is undefined;
  • years 0..99 are interpreted as 1900 + year;
  • component overflow/underflow should be normalized by the Date algorithm.

Perry currently pads every missing argument with 0 and casts components directly before calling components_to_timestamp.

Expected Node behavior

Local Node v25.9.0 probe:

Date.UTC(); // NaN

Date.UTC(2020);       // 1577836800000 (2020-01-01T00:00:00.000Z)
Date.UTC(2020, 0);    // 1577836800000
Date.UTC(2020, 0, 1); // 1577836800000
Date.UTC(2020, 0, 0); // 1577750400000 (2019-12-31T00:00:00.000Z)

Date.UTC(0, 0, 1);   // -2208988800000 (1900-01-01T00:00:00.000Z)
Date.UTC(99, 0, 1);  // 915148800000 (1999-01-01T00:00:00.000Z)
Date.UTC(100, 0, 1); // -59011459200000 (0100-01-01T00:00:00.000Z)

Date.UTC(2020, 12, 1); // 1609459200000 (2021-01-01T00:00:00.000Z)

Current Perry implementation

Source evidence from current tree:

  • crates/perry-hir/src/lower/expr_call/module_static.rs lowers any Date.UTC(...) call to Expr::DateUtc(args).
  • crates/perry-codegen/src/expr/misc_methods.rs lowers up to seven args and pads every missing slot with double_literal(0.0). That makes Date.UTC() and Date.UTC(2020) indistinguishable from explicit zeroes for missing components, but Node treats missing year as NaN and missing day as 1.
  • crates/perry-runtime/src/date.rs::js_date_utc() casts year as i32, converts month + 1 to u32, casts day/hour/minute/second to unsigned integers, and passes them to components_to_timestamp.
  • js_date_utc() does not apply the 0..99 => 1900 + year rule that the adjacent js_date_new_local_components() implementation documents for multi-argument new Date(...).
  • The unsigned casts make negative/overflow component normalization unlikely to match Node's date construction semantics for cases such as day 0, negative days/months, and month overflow.

Suggested test surface

Add parity tests for the edge cases above:

assert.equal(Number.isNaN(Date.UTC()), true);
assert.equal(Date.UTC(2020), Date.UTC(2020, 0, 1));
assert.equal(Date.UTC(2020, 0), Date.UTC(2020, 0, 1));
assert.equal(Date.UTC(2020, 0, 0), Date.parse("2019-12-31T00:00:00.000Z"));
assert.equal(Date.UTC(0, 0, 1), Date.parse("1900-01-01T00:00:00.000Z"));
assert.equal(Date.UTC(99, 0, 1), Date.parse("1999-01-01T00:00:00.000Z"));
assert.equal(Date.UTC(100, 0, 1), Date.parse("0100-01-01T00:00:00.000Z"));
assert.equal(Date.UTC(2020, 12, 1), Date.parse("2021-01-01T00:00:00.000Z"));

Scope / non-goals

This is not the older Date corruption work around #748/#858/#859 or the Date reference/mutation fix #2089. It is specifically about the static Date.UTC argument defaulting and normalization contract.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions