Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions packages/codemode/interpreter-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@ the ultimate source of truth. Upstream test262 files run verbatim from `test/tes
synchronous iterators, and confined synchronous generators. Abrupt completion invokes the iterator's optional `return()`.
- [x] `for...in` over own keys of plain objects, arrays, strings, and tool references. `null`, `undefined`, and other
non-objects iterate nothing. An un-awaited promise throws rather than iterating.
- [ ] `for...in` over inherited enumerable keys (`Object.create(proto)`), and skipping keys deleted during the loop.
- [x] `for...in` over inherited enumerable keys (`Object.create(proto)`): own keys first, then each prototype's, skipping
keys a nearer level already has, even as a non-enumerable property.
- [ ] `for...in` skipping keys deleted during the loop.
- [x] Unlabeled `break` and `continue`.
- [x] `try`, `catch`, optional catch bindings, and `finally`.
- [x] `throw` with arbitrary values.
Expand Down Expand Up @@ -337,8 +339,8 @@ reject }` object.
(`Object.getPrototypeOf("a") === String.prototype`); `null`, `undefined`, and the two confined symbols throw a
`TypeError`.
- [x] `Object.create(proto)` with an object or `null` prototype; any other prototype is a `TypeError`
(`Object prototype may only be an Object or null`). Inherited reads, `in`, `hasOwnProperty`, and own-only
`Object.keys` follow the chain as in JS, but `for...in` still enumerates own keys only. A second `properties`
(`Object prototype may only be an Object or null`). Inherited reads, `in`, `hasOwnProperty`, own-only
`Object.keys`, and `for...in` follow the chain as in JS. A second `properties`
argument other than `undefined` throws a `TypeError`: property descriptors are not supported (there is no
`Object.defineProperty` either).
- [x] `Object.freeze`, `Object.seal`, and `Object.preventExtensions`, with `isFrozen`, `isSealed`, and `isExtensible`.
Expand Down
17 changes: 15 additions & 2 deletions packages/codemode/src/interpreter/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ import {
IteratorObj,
type Cursor,
has,
hasOwn,
hidden,
hasPrototype,
keys,
Expand Down Expand Up @@ -1092,11 +1093,23 @@ class Frame<R> {
throw typeError(`${context} must be a function.`, node)
}

// for...in over null/undefined iterates nothing, like JS.
// for...in over null/undefined iterates nothing, like JS. After the own keys come each prototype's enumerable
// string keys, skipping any key a nearer level has, even as a non-enumerable property.
private enumerableKeys(value: Value, node: AstNode): Array<string> {
if (value instanceof ToolReference) return [...this.ctx.tools.keys(value.path)]
if (value === null || value === undefined) return []
return keys(enumerableSource(this.ctx, "for...in", value, node))
const source = enumerableSource(this.ctx, "for...in", value, node)
const chain = [source]
// A string's characters stand in an array, but its prototype is String.prototype.
let proto = primitivePrototype(this.ctx.builtins, value) ?? source.proto
while (proto !== null) {
chain.push(proto)
proto = proto.proto
}
return chain.flatMap((target, depth) => {
const nearer = chain.slice(0, depth)
return keys(target).filter((key) => !nearer.some((level) => hasOwn(level, key)))
})
}

private *evaluateForInStatement(node: ForInStatement, labels?: ReadonlySet<string>): Eval<StatementResult> {
Expand Down
48 changes: 48 additions & 0 deletions packages/codemode/test/enumeration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,52 @@ describe("for...in", () => {
"un-awaited Promise",
)
})

const forIn = (target: string, setup = "") =>
value(`${setup}; const out = []; for (const key in ${target}) out.push(key); return out`)

test("visits inherited enumerable keys after own keys", async () => {
expect(await forIn("o", "const p = { inh: 1 }; const o = Object.create(p); o.own = 2")).toEqual(["own", "inh"])
})

test("walks every level, skipping keys a nearer level already has", async () => {
expect(
await forIn(
"o",
`const top = { a: 1, top: 1 }
const mid = Object.create(top); mid.a = 2; mid.mid = 2
const o = Object.create(mid); o.own = 3; o.mid = 3`,
),
).toEqual(["own", "mid", "a", "top"])
})

test("orders integer-like keys first on each level", async () => {
expect(
await forIn("o", `const p = { b: 1, 10: 1, 2: 1 }; const o = Object.create(p); o.z = 1; o[5] = 1; o[1] = 1`),
).toEqual(["1", "5", "z", "2", "10", "b"])
})

test("a non-enumerable nearer property shadows an enumerable inherited one", async () => {
// An array's own length and Array.prototype.push are non-enumerable.
const setup = "Object.prototype.length = 1; Object.prototype.push = 2; Object.prototype.extra = 3"
expect(await forIn("[5]", setup)).toEqual(["0", "extra"])
expect(await forIn("Object.create([5])", setup)).toEqual(["0", "extra"])
})

test("a null prototype ends the walk", async () => {
expect(await forIn("o", "const o = Object.create(null); o.a = 1")).toEqual(["a"])
expect(await forIn("Object.create(null)")).toEqual([])
})

test("arrays and strings only visit their indexes, since built-in methods are non-enumerable", async () => {
expect(await forIn("[7, 8]")).toEqual(["0", "1"])
expect(await forIn(`"ab"`)).toEqual(["0", "1"])
})

test("visits enumerable properties a program adds to a built-in prototype", async () => {
expect(await forIn("[7, 8]", "Array.prototype.extra = 1")).toEqual(["0", "1", "extra"])
expect(await forIn(`"ab"`, "Array.prototype.extra = 1")).toEqual(["0", "1"])
expect(await forIn(`"ab"`, "String.prototype.extra = 1")).toEqual(["0", "1", "extra"])
expect(await forIn("5", "Number.prototype.extra = 1")).toEqual(["extra"])
})
})
4 changes: 2 additions & 2 deletions packages/codemode/test/parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1382,7 +1382,7 @@ describe("Object.getPrototypeOf and Object.create", () => {
expect((await error(`Object.getPrototypeOf(Symbol.iterator)`)).message).toContain("cannot convert a symbol")
})

test("Object.create links the prototype: inherited reads, in, and own-only keys", async () => {
test("Object.create links the prototype: inherited reads, in, own-only keys, and inherited for...in", async () => {
expect(
await value(`
const p = { greet(name) { return "hi " + name }, a: 1 }
Expand All @@ -1392,7 +1392,7 @@ describe("Object.getPrototypeOf and Object.create", () => {
for (const key in c) seen.push(key)
return ["greet" in c, Object.keys(c), c.hasOwnProperty("a"), c.a, c.greet(c.name), Object.getPrototypeOf(c) === p, Object.getPrototypeOf(Object.create(null)), seen]
`),
).toEqual([true, ["name"], false, 1, "hi x", true, null, ["name"]])
).toEqual([true, ["name"], false, 1, "hi x", true, null, ["name", "greet", "a"]])
})

test("Object.create rejects non-object prototypes and property descriptors", async () => {
Expand Down
Loading