feat(codemode): complete date parity (#37960)
This commit is contained in:
parent
6d50023457
commit
98c29075b4
6 changed files with 401 additions and 12 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,25 @@
|
|||
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
|
||||
import { CodeModeDate } from "../values.js"
|
||||
import { coerceToNumber, coerceToString } from "./value.js"
|
||||
|
||||
const dateSetterArguments = new Map<string, number>([
|
||||
["setTime", 1],
|
||||
["setMilliseconds", 1],
|
||||
["setUTCMilliseconds", 1],
|
||||
["setSeconds", 2],
|
||||
["setUTCSeconds", 2],
|
||||
["setMinutes", 3],
|
||||
["setUTCMinutes", 3],
|
||||
["setHours", 4],
|
||||
["setUTCHours", 4],
|
||||
["setDate", 1],
|
||||
["setUTCDate", 1],
|
||||
["setMonth", 2],
|
||||
["setUTCMonth", 2],
|
||||
["setFullYear", 3],
|
||||
["setUTCFullYear", 3],
|
||||
])
|
||||
|
||||
export const dateMethods = new Set([
|
||||
"getTime",
|
||||
"valueOf",
|
||||
|
|
@ -23,6 +45,7 @@ export const dateMethods = new Set([
|
|||
"getUTCSeconds",
|
||||
"getUTCMilliseconds",
|
||||
"getTimezoneOffset",
|
||||
...dateSetterArguments.keys(),
|
||||
])
|
||||
|
||||
export const dateStatics = new Set(["now", "parse", "UTC"])
|
||||
|
|
@ -40,8 +63,16 @@ export const invokeDateStatic = (name: string, args: Array<unknown>, node: AstNo
|
|||
}
|
||||
}
|
||||
|
||||
export const invokeDateMethod = (value: CodeModeDate, name: string, node: AstNode): unknown => {
|
||||
const hosted = new Date(value.time)
|
||||
export const dateSetterArgumentCount = (name: string): number | undefined => dateSetterArguments.get(name)
|
||||
|
||||
export const invokeDateMethod = (
|
||||
value: CodeModeDate,
|
||||
name: string,
|
||||
args: Array<number>,
|
||||
node: AstNode,
|
||||
initialTime = value.time,
|
||||
): unknown => {
|
||||
const hosted = new Date(initialTime)
|
||||
switch (name) {
|
||||
case "getTime":
|
||||
case "valueOf":
|
||||
|
|
@ -90,10 +121,60 @@ export const invokeDateMethod = (value: CodeModeDate, name: string, node: AstNod
|
|||
return hosted.getUTCMilliseconds()
|
||||
case "getTimezoneOffset":
|
||||
return hosted.getTimezoneOffset()
|
||||
case "setTime":
|
||||
return updateDate(value, hosted.setTime(args[0]))
|
||||
case "setMilliseconds":
|
||||
return updateDate(value, hosted.setMilliseconds(args[0]))
|
||||
case "setUTCMilliseconds":
|
||||
return updateDate(value, hosted.setUTCMilliseconds(args[0]))
|
||||
case "setSeconds":
|
||||
if (args.length < 2) return updateDate(value, hosted.setSeconds(args[0]))
|
||||
return updateDate(value, hosted.setSeconds(args[0], args[1]))
|
||||
case "setUTCSeconds":
|
||||
if (args.length < 2) return updateDate(value, hosted.setUTCSeconds(args[0]))
|
||||
return updateDate(value, hosted.setUTCSeconds(args[0], args[1]))
|
||||
case "setMinutes":
|
||||
if (args.length < 2) return updateDate(value, hosted.setMinutes(args[0]))
|
||||
if (args.length < 3) return updateDate(value, hosted.setMinutes(args[0], args[1]))
|
||||
return updateDate(value, hosted.setMinutes(args[0], args[1], args[2]))
|
||||
case "setUTCMinutes":
|
||||
if (args.length < 2) return updateDate(value, hosted.setUTCMinutes(args[0]))
|
||||
if (args.length < 3) return updateDate(value, hosted.setUTCMinutes(args[0], args[1]))
|
||||
return updateDate(value, hosted.setUTCMinutes(args[0], args[1], args[2]))
|
||||
case "setHours":
|
||||
if (args.length < 2) return updateDate(value, hosted.setHours(args[0]))
|
||||
if (args.length < 3) return updateDate(value, hosted.setHours(args[0], args[1]))
|
||||
if (args.length < 4) return updateDate(value, hosted.setHours(args[0], args[1], args[2]))
|
||||
return updateDate(value, hosted.setHours(args[0], args[1], args[2], args[3]))
|
||||
case "setUTCHours":
|
||||
if (args.length < 2) return updateDate(value, hosted.setUTCHours(args[0]))
|
||||
if (args.length < 3) return updateDate(value, hosted.setUTCHours(args[0], args[1]))
|
||||
if (args.length < 4) return updateDate(value, hosted.setUTCHours(args[0], args[1], args[2]))
|
||||
return updateDate(value, hosted.setUTCHours(args[0], args[1], args[2], args[3]))
|
||||
case "setDate":
|
||||
return updateDate(value, hosted.setDate(args[0]))
|
||||
case "setUTCDate":
|
||||
return updateDate(value, hosted.setUTCDate(args[0]))
|
||||
case "setMonth":
|
||||
if (args.length < 2) return updateDate(value, hosted.setMonth(args[0]))
|
||||
return updateDate(value, hosted.setMonth(args[0], args[1]))
|
||||
case "setUTCMonth":
|
||||
if (args.length < 2) return updateDate(value, hosted.setUTCMonth(args[0]))
|
||||
return updateDate(value, hosted.setUTCMonth(args[0], args[1]))
|
||||
case "setFullYear":
|
||||
if (args.length < 2) return updateDate(value, hosted.setFullYear(args[0]))
|
||||
if (args.length < 3) return updateDate(value, hosted.setFullYear(args[0], args[1]))
|
||||
return updateDate(value, hosted.setFullYear(args[0], args[1], args[2]))
|
||||
case "setUTCFullYear":
|
||||
if (args.length < 2) return updateDate(value, hosted.setUTCFullYear(args[0]))
|
||||
if (args.length < 3) return updateDate(value, hosted.setUTCFullYear(args[0], args[1]))
|
||||
return updateDate(value, hosted.setUTCFullYear(args[0], args[1], args[2]))
|
||||
default:
|
||||
throw new InterpreterRuntimeError(`Date method '${name}' is not available in CodeMode.`, node)
|
||||
}
|
||||
}
|
||||
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
|
||||
import { CodeModeDate } from "../values.js"
|
||||
import { coerceToNumber, coerceToString } from "./value.js"
|
||||
|
||||
const updateDate = (value: CodeModeDate, time: number): number => {
|
||||
value.time = time
|
||||
return time
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ export class CodeModePromise {
|
|||
}
|
||||
|
||||
export class CodeModeDate {
|
||||
constructor(readonly time: number) {}
|
||||
constructor(public time: number) {}
|
||||
}
|
||||
|
||||
export class CodeModeRegExp {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue