feat(codemode): complete date parity (#37960)

This commit is contained in:
Aiden Cline 2026-07-20 12:13:16 -05:00 committed by GitHub
commit 98c29075b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 401 additions and 12 deletions

View file

@ -22,8 +22,9 @@ import {
CodeModeSet,
CodeModeURL,
CodeModeURLSearchParams,
isCodeModeValue,
} from "../values.js"
import { invokeDateMethod, invokeDateStatic } from "../stdlib/date.js"
import { dateSetterArgumentCount, invokeDateMethod, invokeDateStatic } from "../stdlib/date.js"
import { invokeJsonMethod } from "../stdlib/json.js"
import { invokeMathMethod } from "../stdlib/math.js"
import { invokeNumberMethod, invokeNumberStatic } from "../stdlib/number.js"
@ -96,7 +97,17 @@ export const invokeIntrinsic = <R>(
return invokeArrayMethod(runner, ref.receiver, ref.name, args, node)
}
if (ref.receiver instanceof CodeModeDate) {
return Effect.succeed(invokeDateMethod(ref.receiver, ref.name, node))
const target = ref.receiver
const argumentCount = dateSetterArgumentCount(ref.name)
if (argumentCount === undefined) return Effect.succeed(invokeDateMethod(target, ref.name, [], node))
// Native setters read the current time before argument coercion, whose callbacks may mutate the Date.
const initialTime = target.time
return Effect.map(
Effect.forEach(args.slice(0, argumentCount), (arg) => coerceDateSetterArgument(runner, arg, node), {
concurrency: 1,
}),
(values) => invokeDateMethod(target, ref.name, values, node, initialTime),
)
}
if (ref.receiver instanceof CodeModeRegExp) {
return Effect.succeed(invokeRegExpMethod(ref.receiver, ref.name, args, node))
@ -116,6 +127,33 @@ export const invokeIntrinsic = <R>(
throw new InterpreterRuntimeError(`Method '${ref.name}' is not available in CodeMode.`, node)
}
const coerceDateSetterArgument = <R>(
runner: CallbackRunner<R>,
value: unknown,
node: AstNode,
): Effect.Effect<number, unknown, R> => {
if (value === null || typeof value !== "object" || Array.isArray(value) || isCodeModeValue(value)) {
return Effect.succeed(coerceToNumber(value))
}
const object = value as Record<string, unknown>
return Effect.gen(function* () {
if (Object.hasOwn(object, "valueOf") && typeofValue(object.valueOf) === "function") {
const result = yield* runner.invokeCallable(object.valueOf, [], node)
if (result === null || (typeof result !== "object" && typeof result !== "function")) {
return coerceToNumber(result)
}
}
if (!Object.hasOwn(object, "toString")) return coerceToNumber(value)
if (typeofValue(object.toString) === "function") {
const result = yield* runner.invokeCallable(object.toString, [], node)
if (result === null || (typeof result !== "object" && typeof result !== "function")) {
return coerceToNumber(result)
}
}
throw new InterpreterRuntimeError("Cannot convert object to primitive value.", node).as("TypeError")
})
}
export const invokeGlobalMethod = (ref: GlobalMethodReference, args: Array<unknown>, node: AstNode): unknown => {
if (ref.namespace === "console")
throw new InterpreterRuntimeError(`console.${ref.name} is not available in CodeMode.`, node)

View file

@ -1324,9 +1324,11 @@ export class Interpreter<R> {
throw new InterpreterRuntimeError("Binary operators require data values in CodeMode.", node, "InvalidDataValue")
}
// Null-prototype data needs explicit primitive coercion; identity and `in` retain raw objects.
// Dates use string coercion for `+` and epoch time elsewhere.
// Dates use their default string hint for addition and loose equality, and epoch time elsewhere.
const coerceOperand = (operand: unknown): unknown => {
if (operand instanceof CodeModeDate) return operator === "+" ? coerceToString(operand) : operand.time
if (operand instanceof CodeModeDate) {
return operator === "+" || operator === "==" || operator === "!=" ? coerceToString(operand) : operand.time
}
return operand !== null && typeof operand === "object" ? coerceToString(operand) : operand
}
const bothObjects = lhs !== null && typeof lhs === "object" && rhs !== null && typeof rhs === "object"