Skip to content

Closure-captured numeric params read as 0 inside object-literal : Date method (@perryts/mysql MyDateTime.toDate shape) #858

Description

@proggeramlug

Repro (MySQL-free, no Fastify, 12 lines)

Drop into /tmp/repro.ts, perry compile repro.ts -o repro && ./repro:

function show(label: string, d: Date) {
  console.log(label, "getTime=", d.getTime(), " iso=", d.toISOString());
}
function makeDT(y: number) {
  return { toDate(): Date {
    return new Date(Date.UTC(y, 4, 15, 17, 29, 35, 402));   // captured `y` reads as 0 inside this method
  } };
}
const d = makeDT(2026).toDate();
console.log("INLINE:", d.getTime());
show("HELPER:", d);

Observed (perry 0.5.914)

INLINE: -62155492224598
HELPER: getTime= -62155492224598  iso= 0000-05-15T17:29:36.598Z

Expected (tsx / node — same source)

INLINE: 1778866175402
HELPER: getTime= 1778866175402  iso= 2026-05-15T17:29:35.402Z

The reconstructed milliseconds -62155492224598 is exactly Date.UTC(0, 4, 15, 17, 29, 35, 402) — i.e. the captured y is read as 0 inside toDate() regardless of the value passed to makeDT(...). All other Date.UTC args are literals and arrive intact; only the closure-captured numeric y is corrupt.

Adding a NaN-guard branch (if (y===0) return new Date(NaN);) confirms it from the other side — makeDT(2026).toDate() returns Invalid Date because the guard sees y === 0.

Where this surfaces in real code

node_modules/@perryts/mysql/src/types/datetime.ts::makeMyDateTime is structurally identical:

function makeMyDateTime(y, mo, d, h, mi, sec, micros, raw) {
  return { year:y, ..., toDate(): Date {
    if (y===0 && mo===0 && d===0) return new Date(NaN);
    return new Date(Date.UTC(y, mo-1, d, h, mi, sec, Math.round(micros/1000)));
  }};
}

So every DATETIME column decoded by @perryts/mysql produces a MyDateTime whose .toDate() returns either Invalid Date (the all-zero guard wins because every captured field reads as 0) or a year-0 garbage Date. Consumers like skelpo-shop-admin's toUser(row) then either crash in downstream formatters or silently store wrong timestamps. This is the proximate cause of the new SIGBUS in the shop-admin signup path at v0.5.914 (filed separately).

Notes from bisecting

  • The bug only fires in certain file contexts — a tighter single-function file (function makeDT(...); console.log(makeDT(2026).toDate().getTime()) with no other declarations) compiles correctly. Adding a second function that declares a : Date parameter type, or a few other shapes, makes it manifest reliably. So it's a type-analysis interaction across the module, not a property of the makeDT definition alone.
  • Object-literal method with : Date return annotation + closure capture of a numeric outer param is the consistent shape.
  • The captured non-numeric references (raw: string in the real makeMyDateTime) appear unaffected — only numeric captures zero out.
  • Counterpart shapes that DO work: toDate: (): Date => new Date(Date.UTC(y, ...)) (arrow-property), toDate: function(): Date { ... } (fn-prop), bare function f(y): Date { ... } (standalone fn). The bug is specific to the shorthand-method-in-object-literal-with-: Date-return-and-closure-captured-numeric-params combination.

Context

Surfaced while verifying the v0.5.912 / v0.5.914 #748 fixes against the shop-admin native server. The wait_for_promise condvar fix and the Invalid-Date sentinel both landed correctly — confirmed via the previous minimal repros. This is a separate downstream bug in the same : Date / closure-capture region that's exposed once the Invalid-Date path is correct.

Reproduces deterministically at perry 0.5.914 (HEAD e18cdb07). Happy to add PERRY_DEBUG_* traces if useful — the dynamic-dispatch path for Date.UTC(...) inside the method's body is where I'd start looking (whether captured-arg slots get the f64 they should at the call site).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugConfirmed defect or regression

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions