diff --git a/packages/codemode/interpreter-support.md b/packages/codemode/interpreter-support.md index 9d9f911d787..2cb4c358e1f 100644 --- a/packages/codemode/interpreter-support.md +++ b/packages/codemode/interpreter-support.md @@ -41,8 +41,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262 - [x] A trailing comma after a rest parameter is a syntax error, with or without `"use strict"`. - [x] A program that begins with `"use strict"` rejects `yield` as an identifier and duplicate parameter names at parse time. Without it, `yield` is an ordinary binding. -- [ ] Duplicate parameter names in non-strict code throw when the function is called, instead of binding the last - parameter as JavaScript does. +- [x] Duplicate parameter names in non-strict code bind the last parameter, as in JS (`function f(a, a)` called + with `(1, 2)` sees `a === 2`). ## Values and literals @@ -89,7 +89,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262 sources are rejected. - [x] Destructuring reads through the prototype chain like member access: `const { constructor } = error` and `const { slice } = values` find the inherited built-in. -- [ ] Member expressions as `for...in` targets (`for (x.y in obj)`). +- [x] Any assignment target as a `for...in` head, like `for...of`: `for (x.y in obj)`, `for (a[i++] in obj)`, and + destructuring patterns. ## Statements and control flow @@ -354,11 +355,12 @@ reject }` object. native JS, `split(undefined)` returns the whole string, and `includes`/`startsWith`/`endsWith` reject regular expressions with a native-style `TypeError`. Opaque runtime references still reject as data errors, and `repeat` still requires a finite non-negative count. -- [x] Native no-argument parity for `match()`, `matchAll()`, and `search()`; all behave as an empty pattern. Present - arguments must still be a regular expression or string pattern. +- [x] Native no-argument parity for `match()`, `matchAll()`, and `search()`; all behave as an empty pattern. - [x] `String.raw`, on a template object or any `{ raw }` object; raw strings and substitutions coerce through their own `toString`. -- [ ] `match`, `search`, and `split` accept any value and coerce it (objects via `toString`), like JavaScript. +- [x] `match`, `matchAll`, `search`, and `split` read any non-RegExp argument as a pattern string, as `new RegExp(arg)` + would: `"a1b".match(1)` matches `/1/`, `search(null)` looks for `"null"`, and `undefined` is the empty pattern. + Objects use their built-in string form until ToPrimitive lands. ## Numbers and Math diff --git a/packages/codemode/src/interpreter/interpreter.ts b/packages/codemode/src/interpreter/interpreter.ts index cae0206f990..e3c99a97b11 100644 --- a/packages/codemode/src/interpreter/interpreter.ts +++ b/packages/codemode/src/interpreter/interpreter.ts @@ -937,10 +937,10 @@ class Frame { const keys = self.enumerableKeys(right, node.right) - if (left.type !== "Identifier" && left.type !== "VariableDeclaration") { + if (left.type === "RestElement" || left.type === "AssignmentPattern") { throw typeError("Unsupported for...in binding.", left) } - const assignmentName = left.type === "Identifier" ? left.name : undefined + const assignment = left.type === "VariableDeclaration" ? undefined : left for (const key of keys) { const result = yield* Effect.gen(function* () { @@ -950,8 +950,8 @@ class Frame { yield* self.declarePattern(declared.pattern, key, declared.mutable, left, true) } else if (declared) { yield* self.assignPattern(declared.pattern, key, left) - } else if (assignmentName) { - self.scopes.set(assignmentName, key, left) + } else if (assignment) { + yield* self.assignPattern(assignment, key, left) } return yield* self.evaluateStatement(node.body) }).pipe( @@ -1672,6 +1672,9 @@ class Frame { paramScope.set(name, { mutable: true, value: undefined, initialized: false }) } } + const parameters = fn.parameters.map((parameter) => + parameter.type === "Identifier" ? parameter.name : undefined, + ) const bind = Effect.gen(function* () { for (const [index, parameter] of fn.parameters.entries()) { if (parameter.type === "RestElement") { @@ -1684,6 +1687,8 @@ class Frame { ) break } + // A sloppy simple parameter list may repeat a name; the last occurrence wins, as in JS. + if (parameter.type === "Identifier" && parameters.lastIndexOf(parameter.name) !== index) continue yield* invocation.declarePattern(parameter, args[index], true, parameter, true) } }) diff --git a/packages/codemode/src/stdlib/regexp.ts b/packages/codemode/src/stdlib/regexp.ts index 9f81ce5cc0b..93283ba2911 100644 --- a/packages/codemode/src/stdlib/regexp.ts +++ b/packages/codemode/src/stdlib/regexp.ts @@ -33,21 +33,16 @@ const escapeRegexHint = 'To match special characters like ( ) [ ] { } + * ? . literally, escape them with a backslash (e.g. "\\\\(") or test for them with String.includes instead.' export const toHostRegex = (arg: Value, method: string, extraFlags = ""): RegExp => { - // Native parity: an undefined pattern behaves as an empty pattern. - if (arg === undefined) return new RegExp("", extraFlags) if (arg instanceof RegExpObj) return arg.regex - if (typeof arg === "string") { - try { - return new RegExp(arg, extraFlags) - } catch (error) { - throw syntaxError( - `String.${method} received the string ${JSON.stringify(arg)}, which is not a valid regular expression pattern (${regexFailureReason(error)}). ${escapeRegexHint}`, - ) - } + // Anything else is a pattern string, as `new RegExp(arg)` would read it: undefined is the empty pattern. + const source = arg === undefined ? "" : coerceToString(arg) + try { + return new RegExp(source, extraFlags) + } catch (error) { + throw syntaxError( + `String.${method} received the string ${JSON.stringify(source)}, which is not a valid regular expression pattern (${regexFailureReason(error)}). ${escapeRegexHint}`, + ) } - throw typeError( - `String.${method} expects a regular expression (a /pattern/flags literal or new RegExp(...)) or a string pattern, not ${arg === null ? "null" : typeof arg}.`, - ) } export const matchToValue = (builtins: Builtins, match: RegExpMatchArray): Arr => { diff --git a/packages/codemode/test/parity.test.ts b/packages/codemode/test/parity.test.ts index 3519f3024f3..6e9bd472d95 100644 --- a/packages/codemode/test/parity.test.ts +++ b/packages/codemode/test/parity.test.ts @@ -1156,3 +1156,25 @@ describe("String.raw", () => { expect(failure.message).toContain("String.raw expects a template object with a raw array") }) }) + +describe("sloppy duplicate parameters and for...in targets", () => { + test("a repeated parameter name binds the last argument", async () => { + expect(await value(`function f(a, b, a) { return [a, b] } return [f(1, 2, 3), f(1)]`)).toEqual([ + [3, 2], + [null, null], + ]) + }) + + test("for...in assigns to any target: members, computed members, and patterns", async () => { + expect( + await value(` + const x = {}, seen = [], a = [] + let i = 0, first + for (x.y in { p: 1, q: 2 }) seen.push(x.y) + for (a[i++] in { p: 1, q: 2 }); + for ([first] in { ab: 1 }); + return [seen, x.y, a, first] + `), + ).toEqual([["p", "q"], "q", ["p", "q"], "a"]) + }) +}) diff --git a/packages/codemode/test/stdlib.test.ts b/packages/codemode/test/stdlib.test.ts index ba1a85bc84a..69c7447081e 100644 --- a/packages/codemode/test/stdlib.test.ts +++ b/packages/codemode/test/stdlib.test.ts @@ -452,10 +452,12 @@ describe("RegExp", () => { expect((await error(`return "aa".matchAll(/a/)`)).message).toContain("write /a/g, or use String.match") }) - test("a non-pattern argument names the expected shapes", async () => { - const err = await error(`return "abc".match(42)`) - expect(err.message).toContain("expects a regular expression") - expect(err.message).toContain("not number") + test("any argument is a pattern string, as new RegExp(arg) reads it", async () => { + expect( + await value( + `return ["a42b".match(42)[0], "xnullx".search(null), "abc".match(undefined), [..."1a1".matchAll(1)].length]`, + ), + ).toEqual(["42", 1, [""], 2]) }) test("source and flags properties read through", async () => { diff --git a/packages/codemode/test/test262/skipped.txt b/packages/codemode/test/test262/skipped.txt index 5bd32f14ead..a8166f30309 100644 --- a/packages/codemode/test/test262/skipped.txt +++ b/packages/codemode/test/test262/skipped.txt @@ -215,7 +215,6 @@ language/statements/for-await-of/async-gen-decl-dstr-obj-empty-num.js # TypeErr 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/head-lhs-member.js # Unsupported for...in binding. language/statements/for-of/dstr/array-elem-iter-rtrn-close-err.js # Iterator next must be a function. language/statements/for-of/dstr/array-elem-iter-thrw-close-err.js # Expected SameValue(«1», «0») to be true language/statements/for-of/dstr/array-elem-trlg-iter-list-thrw-close-err.js # Expected SameValue(«1», «0») to be true