diff --git a/packages/codemode/interpreter-support.md b/packages/codemode/interpreter-support.md index 064db10f551..33e298eea74 100644 --- a/packages/codemode/interpreter-support.md +++ b/packages/codemode/interpreter-support.md @@ -85,8 +85,9 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262 - [x] Array binding and assignment destructuring from strings, Maps, Sets, URLSearchParams, custom synchronous iterators, and synchronous generators, including stepwise elisions/rest and `IteratorClose` on early completion or binding/default failure. -- [ ] Object destructuring from primitives follows ToObject (`const { length } = "abc"`, `const {} = 1`); non-object - sources are rejected. +- [x] Object destructuring from primitives follows ToObject: `const { length } = "abc"` is `3`, `const { toFixed } = 1` + finds the built-in, `const {} = 1` is a no-op, and a rest element copies a string's indexes (`{ 1: "y", 2: "z" }`). + Only `null` and `undefined` sources throw (`Cannot destructure null as it is null.`). - [x] Destructuring reads through the prototype chain like member access: `const { constructor } = error` and `const { slice } = values` find the inherited built-in. - [x] Any assignment target as a `for...in` head, like `for...of`: `for (x.y in obj)`, `for (a[i++] in obj)`, and @@ -240,9 +241,9 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262 throws: `{ valueOf() { return 7 } } * 2` is `14`, `` `${{ toString() { return "x" } }}` `` is `"x"`, and `[1, 2]` with `arr.toString = () => "x"` makes `arr + ""` `"x"`. Dates keep their `Symbol.toPrimitive` behavior (`date + 1` concatenates, `date - date` subtracts). -- [ ] ToPrimitive elsewhere: multi-argument `Date` construction, `Date.UTC`, `Error.prototype.toString` on an object - `message`, and numeric built-in arguments outside `Math` (`at`, `indexOf` start, `toFixed` digits) still use the - built-in form (`NaN`, `"[object Object]"`) and ignore own methods. +- [ ] ToPrimitive elsewhere: `Error.prototype.toString` on an object `message` and numeric built-in arguments outside + `Math` and `Date` (`at`, `indexOf` start, `toFixed` digits) still use the built-in form (`NaN`, + `"[object Object]"`) and ignore own methods. - [x] Property keys follow ToPropertyKey: `x[null]`, `x[true]`, and objects (via their built-in string form) become string keys. @@ -460,15 +461,15 @@ reject }` object. - [x] `getTimezoneOffset`, arithmetic, relational comparison, and `instanceof Date`. - [x] Date values serialize to ISO strings; invalid dates serialize to `null`. - [x] Local and UTC Date setters, including native argument coercion, mutation, rollover, invalid-Date recovery, and - `TimeClip` behavior. + `TimeClip` behavior. On an invalid Date every setter but `setTime` and `set(UTC)FullYear` answers `NaN` without + writing, so a time set inside an argument's `valueOf` survives. - [x] `Date.prototype.toUTCString` and its `toGMTString` alias. - [x] `toDateString` and `toTimeString` in the host's local timezone. - [x] `toLocaleString`, `toLocaleDateString`, and `toLocaleTimeString` always format as `en-US` in UTC (`"1/1/1970, 12:00:00 AM"`) so output does not depend on the host. - [x] Native one-argument Date coercion for supported values, including booleans, null, arrays, and plain objects. -- [x] Date setters and one-argument construction coerce object arguments through their own `valueOf`/`toString` and - surface their throws. -- [ ] Multi-argument construction and `Date.UTC` coerce object arguments the same way (see ToPrimitive above). +- [x] Date setters, construction, and `Date.UTC` coerce object arguments through their own `valueOf`/`toString` in + argument order and surface their throws; only the first seven components are converted. - [x] Native Date loose-equality and default primitive-coercion semantics, using CodeMode's deterministic ISO string representation for the string primitive. - [x] Native `RangeError` branding for invalid `toISOString()` calls. diff --git a/packages/codemode/src/interpreter/interpreter.ts b/packages/codemode/src/interpreter/interpreter.ts index e48bc44d851..bec9efa4eb3 100644 --- a/packages/codemode/src/interpreter/interpreter.ts +++ b/packages/codemode/src/interpreter/interpreter.ts @@ -1136,18 +1136,15 @@ class Frame { } if (pattern.type === "ObjectPattern") { - if (!(value instanceof Obj)) { - throw typeError( - `Object destructuring requires a data object or array value, received ${describeValue(value)}.`, - pattern, - ) + if (value === null || value === undefined) { + throw typeError(`Cannot destructure ${describeValue(value)} as it is ${value}.`, pattern) } const consumed = new Set() for (const property of pattern.properties) { if (property.type === "RestElement") { const rest = new Obj(self.ctx.builtins.Object) - assign(rest, value, consumed) + assign(rest, enumerableSource(self.ctx, "Object destructuring", value, pattern), consumed) yield* self.declarePattern(property.argument, rest, mutable, property, initialize) continue } @@ -1156,7 +1153,7 @@ class Frame { consumed.add(typeof key === "symbol" ? key : String(key)) yield* self.declarePattern( property.value, - self.readProperty(value, key, property), + self.destructuredProperty(value, key, property), mutable, property, initialize, @@ -1195,24 +1192,21 @@ class Frame { } if (pattern.type === "ObjectPattern") { - if (!(value instanceof Obj)) { - throw invalidData( - `Object destructuring requires a data object or array value, received ${describeValue(value)}.`, - pattern, - ) + if (value === null || value === undefined) { + throw typeError(`Cannot destructure ${describeValue(value)} as it is ${value}.`, pattern) } const consumed = new Set() for (const property of pattern.properties) { if (property.type === "RestElement") { const rest = new Obj(self.ctx.builtins.Object) - assign(rest, value, consumed) + assign(rest, enumerableSource(self.ctx, "Object destructuring", value, pattern), consumed) yield* self.assignPattern(property.argument, rest, property) continue } const key = yield* self.destructuringPropertyKey(property) consumed.add(typeof key === "symbol" ? key : String(key)) - yield* self.assignPattern(property.value, self.readProperty(value, key, property), property) + yield* self.assignPattern(property.value, self.destructuredProperty(value, key, property), property) } return } @@ -2189,32 +2183,48 @@ class Frame { : propertyNode.type === "Identifier" ? propertyNode.name : self.toPropertyKey(yield* self.evaluateExpression(propertyNode)) - - if (objectValue instanceof ToolReference) { - if (typeof key !== "string") { - throw typeError("Tool paths must use string property names.", propertyNode) - } - return new ToolReference([...objectValue.path, key]) - } - - if (objectValue instanceof Obj) return { target: objectValue, key, receiver: objectValue } - - // Strings own length and indexes; every other primitive property reads through the wrapper prototype. - if (typeof objectValue === "string") { - if (key === "length") return { value: objectValue.length } - const index = typeof key === "symbol" ? undefined : parseArrayIndex(key) - if (index !== undefined) return { value: objectValue[index] } - } - const proto = primitivePrototype(self.ctx.builtins, objectValue) - if (proto !== undefined) return { target: proto, key, receiver: objectValue } - - if (objectValue === null || objectValue === undefined) { - throw typeError(`Cannot read properties of ${objectValue} (reading '${String(key)}').`, objectNode) - } - throw typeError("Cannot access a property on a non-object value.", objectNode) + return self.resolveProperty(objectValue, key, objectNode, propertyNode) }) } + private resolveProperty( + objectValue: Value, + key: PropertyKey, + objectNode: AstNode, + propertyNode: AstNode, + ): MemberReference | ToolReference | { value: Value } { + if (objectValue instanceof ToolReference) { + if (typeof key !== "string") { + throw typeError("Tool paths must use string property names.", propertyNode) + } + return new ToolReference([...objectValue.path, key]) + } + + if (objectValue instanceof Obj) return { target: objectValue, key, receiver: objectValue } + + // Strings own length and indexes; every other primitive property reads through the wrapper prototype. + if (typeof objectValue === "string") { + if (key === "length") return { value: objectValue.length } + const index = typeof key === "symbol" ? undefined : parseArrayIndex(key) + if (index !== undefined) return { value: objectValue[index] } + } + const proto = primitivePrototype(this.ctx.builtins, objectValue) + if (proto !== undefined) return { target: proto, key, receiver: objectValue } + + if (objectValue === null || objectValue === undefined) { + throw typeError(`Cannot read properties of ${objectValue} (reading '${String(key)}').`, objectNode) + } + throw typeError("Cannot access a property on a non-object value.", objectNode) + } + + // One destructured property, read the way a member expression would read it (primitives use their prototype). + private destructuredProperty(source: Value, key: PropertyKey, node: AstNode): Value { + const reference = this.resolveProperty(source, key, node, node) + if (reference instanceof ToolReference) return reference + if ("value" in reference) return reference.value + return this.readProperty(reference.target, reference.key, node, reference.receiver) + } + private readReference(reference: MemberReference, node: MemberExpression): Value { // Reject unknown promise properties so a missing await cannot hide. if (reference.target instanceof PromiseObj && !has(reference.target, reference.key)) { diff --git a/packages/codemode/src/stdlib/date.ts b/packages/codemode/src/stdlib/date.ts index 57285ddf994..09a51c369f5 100644 --- a/packages/codemode/src/stdlib/date.ts +++ b/packages/codemode/src/stdlib/date.ts @@ -16,8 +16,11 @@ const constructDate = (ctx: Interpreter, args: Array, proto: Obj) = : new DateObj(proto, new Date(coerceToNumber(value)).getTime()), ) } - const parts = args.map((arg) => coerceToNumber(arg)) - return Effect.succeed(new DateObj(proto, new Date(...(parts as [number, number])).getTime())) + // The spec converts at most seven components, in order, so extra arguments never run program code. + return Effect.map( + Effect.forEach(args.slice(0, 7), (arg) => toPrimitiveNumber(ctx, arg), { concurrency: 1 }), + (parts) => new DateObj(proto, new Date(...(parts as [number, number])).getTime()), + ) } type Getter = keyof { @@ -79,7 +82,15 @@ export const dateGlobal = (ctx: Interpreter) => { methods(builtins, date, [ ["now", 0, () => Date.now()], ["parse", 1, (_, args) => Date.parse(coerceToString(args[0]))], - ["UTC", 7, (_, args) => Date.UTC(...(args.map((arg) => coerceToNumber(arg)) as Parameters))], + [ + "UTC", + 7, + (_, args) => + Effect.map( + Effect.forEach(args.slice(0, 7), (arg) => toPrimitiveNumber(ctx, arg), { concurrency: 1 }), + (parts) => Date.UTC(...(parts as Parameters)), + ), + ], ]) const self = (thisValue: Value, name: string) => receiver(DateObj, thisValue, `Date.prototype.${name}`) @@ -125,6 +136,8 @@ export const dateGlobal = (ctx: Interpreter) => { concurrency: 1, }), (values) => { + // Every setter but setTime and setFullYear leaves an invalid Date untouched and answers NaN. + if (Number.isNaN(hosted.getTime()) && name !== "setTime" && !name.endsWith("FullYear")) return NaN target.time = hosted[name](...(values as [number, number, number, number])) return target.time }, diff --git a/packages/codemode/test/parity.test.ts b/packages/codemode/test/parity.test.ts index 371e3dddbd6..d05b5ce2ee8 100644 --- a/packages/codemode/test/parity.test.ts +++ b/packages/codemode/test/parity.test.ts @@ -1688,3 +1688,52 @@ describe("ToPrimitive: operators and conversions honor program valueOf and toStr expect(failure.message).toContain("Cannot convert object to primitive value") }) }) + +describe("object destructuring from primitives", () => { + test("reads through the primitive's prototype like member access", async () => { + expect( + await value(` + const { length, 0: first, toUpperCase } = "abc" + const { toFixed } = 1.5 + const {} = true + const { 0: a, ...rest } = "xyz" + const { ...none } = 42 + let n + ;({ length: n } = "hello") + return [length, first, toUpperCase.call("q"), toFixed.call(2.345, 1), a, rest, none, n] + `), + ).toEqual([3, "a", "Q", "2.3", "x", { 1: "y", 2: "z" }, {}, 5]) + }) + + test("only null and undefined sources throw", async () => { + expect((await error(`const { a } = null`)).message).toContain("Cannot destructure null as it is null") + expect((await error(`const {} = undefined`)).message).toContain("Cannot destructure undefined") + expect((await error(`let a; ({ a } = undefined)`)).message).toContain("Cannot destructure undefined") + }) +}) + +describe("Date components convert through ToPrimitive", () => { + test("construction and Date.UTC ask each of the first seven arguments in order", async () => { + expect( + await value(` + const seen = [] + const part = (n) => ({ valueOf() { seen.push(n); return n } }) + const time = new Date(part(2024), part(1), part(2), part(3), part(4), part(5), part(6), part(99)).getTime() + const utc = Date.UTC(2024, { valueOf() { return 0 } }, 15) + return [seen, time === new Date(2024, 1, 2, 3, 4, 5, 6).getTime(), utc === Date.UTC(2024, 0, 15)] + `), + ).toEqual([[2024, 1, 2, 3, 4, 5, 6], true, true]) + expect((await error(`new Date(2024, { valueOf() { throw new RangeError("boom") } })`)).message).toContain("boom") + }) + + test("setters on an invalid Date answer NaN without overwriting a time set during coercion", async () => { + expect( + await value(` + const d = new Date(NaN) + const result = d.setDate({ valueOf() { d.setTime(0); return 1 } }) + const y = new Date(NaN) + return [Number.isNaN(result), d.getTime(), y.setFullYear(2020) === Date.UTC(2020, 0, 1) - y.getTimezoneOffset() * 60000] + `), + ).toEqual([true, 0, true]) + }) +}) diff --git a/packages/codemode/test/test262/manifest.json b/packages/codemode/test/test262/manifest.json index e2e430410e0..b3cfa41b5e7 100644 --- a/packages/codemode/test/test262/manifest.json +++ b/packages/codemode/test/test262/manifest.json @@ -2,6 +2,7 @@ "revision": "250f204f23a9249ff204be2baec29600faae7b75", "directories": [ "built-ins/Array/prototype", + "built-ins/Date", "built-ins/Function/prototype/apply", "built-ins/Function/prototype/bind", "built-ins/Function/prototype/call", @@ -17,6 +18,7 @@ "built-ins/parseFloat", "built-ins/parseInt", "language/arguments-object", + "language/expressions/assignment", "language/expressions/does-not-equals", "language/expressions/equals", "language/expressions/exponentiation", diff --git a/packages/codemode/test/test262/skipped.txt b/packages/codemode/test/test262/skipped.txt index 769fcad561d..94cddd30b43 100644 --- a/packages/codemode/test/test262/skipped.txt +++ b/packages/codemode/test/test262/skipped.txt @@ -925,16 +925,6 @@ language/statements/async-generator/dstr/dflt-ary-ptrn-elem-id-iter-val-array-pr language/statements/async-generator/return-undefined-implicit-and-explicit.js # Actual ["tick 1", "tick 2", "g1 ret", "g2 ret", "g3 ret", "g4 ret"] and expected ["tick 1", "g1 ret" language/statements/const/dstr/ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all language/statements/const/dstr/ary-ptrn-elem-id-iter-val-array-prototype.js # Expected SameValue(«3», «42») to be true -language/statements/for-await-of/async-func-decl-dstr-obj-empty-bool.js # TypeError: Object destructuring requires a data object or array value, received a boolean. -language/statements/for-await-of/async-func-decl-dstr-obj-empty-num.js # TypeError: Object destructuring requires a data object or array value, received a number. -language/statements/for-await-of/async-func-decl-dstr-obj-empty-string.js # TypeError: Object destructuring requires a data object or array value, received a string. -language/statements/for-await-of/async-func-decl-dstr-obj-rest-number.js # TypeError: Object destructuring requires a data object or array value, received a number. -language/statements/for-await-of/async-func-decl-dstr-obj-rest-str-val.js # TypeError: Object destructuring requires a data object or array value, received a string. -language/statements/for-await-of/async-gen-decl-dstr-obj-empty-bool.js # TypeError: Object destructuring requires a data object or array value, received a boolean. -language/statements/for-await-of/async-gen-decl-dstr-obj-empty-num.js # TypeError: Object destructuring requires a data object or array value, received a number. -language/statements/for-await-of/async-gen-decl-dstr-obj-empty-string.js # TypeError: Object destructuring requires a data object or array value, received a string. -language/statements/for-await-of/async-gen-decl-dstr-obj-rest-number.js # TypeError: Object destructuring requires a data object or array value, received a number. -language/statements/for-await-of/async-gen-decl-dstr-obj-rest-str-val.js # TypeError: Object destructuring requires a data object or array value, received a string. language/statements/for-in/S12.6.4_A7_T2.js # for...in visits keys deleted during the loop language/statements/for-in/order-enumerable-shadowed.js # Object.create property descriptors are not supported; assign the fields after creating the object. language/statements/for-in/S12.6.4_A6.1.js # TypeError: FACTORY cannot be constructed: user-defined constructors and classes are not supported. C @@ -959,11 +949,6 @@ language/statements/for-of/dstr/const-ary-init-iter-get-err-array-prototype.js language/statements/for-of/dstr/const-ary-ptrn-elem-id-iter-val-array-prototype.js # Expected SameValue(«3», «42») to be true language/statements/for-of/dstr/let-ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all language/statements/for-of/dstr/let-ary-ptrn-elem-id-iter-val-array-prototype.js # Expected SameValue(«3», «42») to be true -language/statements/for-of/dstr/obj-empty-bool.js # Object destructuring requires a data object or array value, received a boolean. -language/statements/for-of/dstr/obj-empty-num.js # Object destructuring requires a data object or array value, received a number. -language/statements/for-of/dstr/obj-empty-string.js # Object destructuring requires a data object or array value, received a string. -language/statements/for-of/dstr/obj-rest-number.js # Object destructuring requires a data object or array value, received a number. -language/statements/for-of/dstr/obj-rest-str-val.js # Object destructuring requires a data object or array value, received a string. language/statements/for-of/dstr/var-ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all language/statements/for-of/dstr/var-ary-ptrn-elem-id-iter-val-array-prototype.js # Expected SameValue(«3», «42») to be true language/statements/for-of/throw-from-catch.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct @@ -1097,3 +1082,35 @@ built-ins/Array/prototype/some/15.4.4.17-7-c-i-26.js # TypeError: Array.prototy built-ins/Array/prototype/some/15.4.4.17-7-c-i-27.js # TypeError: Array.prototype.some called on incompatible receiver an Arguments. built-ins/parseFloat/S15.1.2.3_A7.5.js # TypeError: Cannot read properties of undefined (reading '…'). built-ins/parseInt/S15.1.2.2_A9.5.js # TypeError: Cannot read properties of undefined (reading '…'). +built-ins/Date/S15.9.3.1_A4_T0.js # `new Date(new PoisonedValueOf(1))` throws a Test262Error exception Expected a Test262Error but got a +built-ins/Date/S15.9.3.1_A4_T1.js # `new Date(new PoisonedValueOf(1), new PoisonedValueOf(2))` throws a Test262Error exception Expected +built-ins/Date/S15.9.3.1_A4_T2.js # `new Date(new PoisonedValueOf(1), new PoisonedValueOf(2), new PoisonedValueOf(3))` throws a Test262E +built-ins/Date/S15.9.3.1_A4_T3.js # `new Date(new PoisonedValueOf(1), new PoisonedValueOf(2), new PoisonedValueOf(3), new PoisonedValueO +built-ins/Date/S15.9.3.1_A4_T4.js # `new Date(new PoisonedValueOf(1), new PoisonedValueOf(2), new PoisonedValueOf(3), new PoisonedValueO +built-ins/Date/S15.9.3.1_A4_T5.js # `new Date(new PoisonedValueOf(1), new PoisonedValueOf(2), new PoisonedValueOf(3), new PoisonedValueO +built-ins/Date/S15.9.3.1_A4_T6.js # `new Date(new PoisonedValueOf(1), new PoisonedValueOf(2), new PoisonedValueOf(3), new PoisonedValueO +built-ins/Date/prototype/toJSON/invoke-result.js # TypeError: Date.prototype.toJSON called on incompatible receiver a data object. +built-ins/Date/prototype/toJSON/to-primitive-value-of.js # TypeError: Date.prototype.toJSON called on incompatible receiver a data object. +built-ins/Date/prototype/toString/format.js # Expected SameValue(«null», «null») to be false +built-ins/Date/prototype/toString/negative-year.js # Date.prototype.toString serializes year -1 to "-0001" Expected SameValue(«undefined», «"-0001"») to +language/expressions/assignment/dstr/array-elem-iter-rtrn-close-err.js # TypeError: Iterator next must be a function. +language/expressions/assignment/dstr/array-elem-iter-thrw-close-err.js # Expected SameValue(«1», «0») to be true +language/expressions/assignment/dstr/array-elem-iter-thrw-close.js # Expected SameValue(«1», «0») to be true +language/expressions/assignment/dstr/array-elem-trlg-iter-list-thrw-close-err.js # Expected SameValue(«1», «0») to be true +language/expressions/assignment/dstr/array-elem-trlg-iter-list-thrw-close.js # Expected SameValue(«1», «0») to be true +language/expressions/assignment/dstr/array-elem-trlg-iter-rest-rtrn-close-err.js # Expected a Test262Error to be thrown but no exception was thrown at all +language/expressions/assignment/dstr/array-elem-trlg-iter-rest-rtrn-close-null.js # Expected a TypeError to be thrown but no exception was thrown at all +language/expressions/assignment/dstr/array-elem-trlg-iter-rest-rtrn-close.js # Expected SameValue(«11», «1») to be true +language/expressions/assignment/dstr/array-elem-trlg-iter-rest-thrw-close-err.js # Expected SameValue(«11», «1») to be true +language/expressions/assignment/dstr/array-elem-trlg-iter-rest-thrw-close.js # Expected SameValue(«11», «1») to be true +language/expressions/assignment/dstr/array-rest-iter-rtrn-close-err.js # TypeError: Iterator next must be a function. +language/expressions/assignment/dstr/array-rest-iter-rtrn-close-null.js # TypeError: Iterator next must be a function. +language/expressions/assignment/dstr/array-rest-iter-rtrn-close.js # Test262Error +language/expressions/assignment/dstr/array-rest-iter-thrw-close-err.js # Expected SameValue(«11», «0») to be true +language/expressions/assignment/dstr/array-rest-iter-thrw-close.js # Expected SameValue(«11», «0») to be true +language/expressions/assignment/dstr/array-rest-lref-err.js # Expected SameValue(«1», «0») to be true +language/expressions/assignment/target-member-computed-reference-null.js # Expected a DummyError but got a TypeError +language/expressions/assignment/target-member-computed-reference-undefined.js # Expected a DummyError but got a TypeError +language/expressions/assignment/target-member-computed-reference.js # Expected a DummyError but got a TypeError +language/expressions/assignment/target-member-identifier-reference-null.js # Expected SameValue(«0», «1») to be true +language/expressions/assignment/target-member-identifier-reference-undefined.js # Expected SameValue(«0», «1») to be true