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
115 changes: 71 additions & 44 deletions packages/codemode/src/interpreter/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1804,25 +1804,31 @@ class Interpreter<R> {
if (operator === "??=" || operator === "||=" || operator === "&&=") {
return yield* self.evaluateLogicalAssignment(node, left, operator)
}
const rightValue = yield* self.evaluateExpression(getNode(node, "right"))
if (left.type === "Identifier") {
const name = getString(left, "name")
if (operator === "=") return self.setIdentifierValue(name, rightValue, left)
const next = boundedData(
self.applyCompoundAssignment(operator, self.getIdentifierValue(name, left), rightValue, node),
"Assignment result",
)
return self.setIdentifierValue(name, next, left)
}
if (left.type === "MemberExpression") {
if (operator === "=") return yield* self.writeMember(left, rightValue)
return yield* self.modifyMember(left, (current) => {
if (operator !== "=") {
const current = self.getIdentifierValue(name, left)
const rightValue = yield* self.evaluateExpression(getNode(node, "right"))
const next = boundedData(
self.applyCompoundAssignment(operator, current, rightValue, node),
"Assignment result",
)
return Effect.succeed({ write: true, next, result: next })
})
return self.setIdentifierValue(name, next, left)
}
const rightValue = yield* self.evaluateExpression(getNode(node, "right"))
if (operator === "=") return self.setIdentifierValue(name, rightValue, left)
}
if (left.type === "MemberExpression") {
return yield* self.modifyMember(left, (current) =>
Effect.map(self.evaluateExpression(getNode(node, "right")), (rightValue) => {
if (operator === "=") return { write: true, next: rightValue, result: rightValue }
const next = boundedData(
self.applyCompoundAssignment(operator, current, rightValue, node),
"Assignment result",
)
return { write: true, next, result: next }
}),
)
}
throw new InterpreterRuntimeError("Assignment target must be an Identifier or MemberExpression.", left)
})
Expand Down Expand Up @@ -1923,6 +1929,9 @@ class Interpreter<R> {
if (callable.namespace === "Object" && args[0] instanceof ToolReference) {
return self.invokeObjectMethodOnTools(callable.name, args[0] as ToolReference, node)
}
if (callable.namespace === "Object" && callable.name === "assign") {
return invokeGlobalMethod(callable, args, node)
}
return boundedData(invokeGlobalMethod(callable, args, node), `${callable.namespace}.${callable.name} result`)
}
if (callable instanceof CoercionFunction) {
Expand Down Expand Up @@ -2575,8 +2584,12 @@ class Interpreter<R> {
case "flat":
return Effect.succeed(target.flat(optNumber(args[0], "depth") ?? 1))
case "reverse":
return Effect.succeed([...target].reverse())
return Effect.succeed(target.reverse())
case "sort":
return Effect.map(this.sortArray(target, args[0], node), (sorted) => {
target.splice(0, target.length, ...sorted)
return target
})
case "toSorted":
return this.sortArray(target, args[0], node)
case "toReversed":
Expand Down Expand Up @@ -2659,53 +2672,65 @@ class Interpreter<R> {
? Effect.succeed(invokeUriFunction(callback, callbackArgs, node))
: self.invokeFunction(callback, callbackArgs)
return Effect.gen(function* () {
// Iterate a snapshot taken at call time so a callback that mutates the array can't
// self-extend the loop - matching JS, where elements appended during iteration are not visited.
const items = target.slice()
// Capture the initial length, but read the receiver live so callbacks observe mutations
// without visiting elements appended after iteration begins.
const length = target.length
switch (name) {
case "map": {
const values: Array<unknown> = []
for (const [index, item] of items.entries()) values.push(yield* apply([item, index, items]))
values.length = length
for (let index = 0; index < length; index += 1) {
if (!(index in target)) continue
values[index] = yield* apply([target[index], index, target])
}
return values
}
case "flatMap": {
const values: Array<unknown> = []
for (const [index, item] of items.entries()) {
const mapped = yield* apply([item, index, items])
for (let index = 0; index < length; index += 1) {
if (!(index in target)) continue
const mapped = yield* apply([target[index], index, target])
if (Array.isArray(mapped)) values.push(...mapped)
else values.push(mapped)
}
return values
}
case "filter": {
const values: Array<unknown> = []
for (const [index, item] of items.entries()) {
if (yield* apply([item, index, items])) values.push(item)
for (let index = 0; index < length; index += 1) {
if (!(index in target)) continue
const item = target[index]
if (yield* apply([item, index, target])) values.push(item)
}
return values
}
case "find":
for (const [index, item] of items.entries()) {
if (yield* apply([item, index, items])) return item
for (let index = 0; index < length; index += 1) {
const item = target[index]
if (yield* apply([item, index, target])) return item
}
return undefined
case "findIndex":
for (const [index, item] of items.entries()) {
if (yield* apply([item, index, items])) return index
for (let index = 0; index < length; index += 1) {
if (yield* apply([target[index], index, target])) return index
}
return -1
case "some":
for (const [index, item] of items.entries()) {
if (yield* apply([item, index, items])) return true
for (let index = 0; index < length; index += 1) {
if (!(index in target)) continue
if (yield* apply([target[index], index, target])) return true
}
return false
case "every":
for (const [index, item] of items.entries()) {
if (!(yield* apply([item, index, items]))) return false
for (let index = 0; index < length; index += 1) {
if (!(index in target)) continue
if (!(yield* apply([target[index], index, target]))) return false
}
return true
case "forEach":
for (const [index, item] of items.entries()) yield* apply([item, index, items])
for (let index = 0; index < length; index += 1) {
if (index in target) yield* apply([target[index], index, target])
}
return undefined
case "reduce": {
let accumulator: unknown
Expand All @@ -2714,13 +2739,14 @@ class Interpreter<R> {
accumulator = args[1]
start = 0
} else {
if (items.length === 0)
if (length === 0)
throw new InterpreterRuntimeError("Array.reduce of an empty array with no initial value.", node)
accumulator = items[0]
accumulator = target[0]
start = 1
}
for (let index = start; index < items.length; index += 1) {
accumulator = yield* apply([accumulator, items[index], index, items])
for (let index = start; index < length; index += 1) {
if (!(index in target)) continue
accumulator = yield* apply([accumulator, target[index], index, target])
}
return accumulator
}
Expand All @@ -2729,26 +2755,27 @@ class Interpreter<R> {
let start: number
if (args.length >= 2) {
accumulator = args[1]
start = items.length - 1
start = length - 1
} else {
if (items.length === 0)
if (length === 0)
throw new InterpreterRuntimeError("Array.reduceRight of an empty array with no initial value.", node)
accumulator = items[items.length - 1]
start = items.length - 2
accumulator = target[length - 1]
start = length - 2
}
for (let index = start; index >= 0; index -= 1) {
accumulator = yield* apply([accumulator, items[index], index, items])
if (!(index in target)) continue
accumulator = yield* apply([accumulator, target[index], index, target])
}
return accumulator
}
case "findLast":
for (let index = items.length - 1; index >= 0; index -= 1) {
if (yield* apply([items[index], index, items])) return items[index]
for (let index = length - 1; index >= 0; index -= 1) {
if (yield* apply([target[index], index, target])) return target[index]
}
return undefined
case "findLastIndex":
for (let index = items.length - 1; index >= 0; index -= 1) {
if (yield* apply([items[index], index, items])) return index
for (let index = length - 1; index >= 0; index -= 1) {
if (yield* apply([target[index], index, target])) return index
}
return -1
}
Expand Down
10 changes: 7 additions & 3 deletions packages/codemode/src/stdlib/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,14 @@ export const invokeObjectMethod = (name: string, args: Array<unknown>, node: Ast
case "hasOwn":
return Object.hasOwn(requireObject(), String(args[1]))
case "assign": {
const out: Record<string, unknown> = Object.create(null)
for (const source of args) {
const target = args[0]
if (target === null || typeof target !== "object" || Array.isArray(target) || isSandboxValue(target)) {
throw new InterpreterRuntimeError("Object.assign expects a data object target.", node)
}
const out = target as Record<string, unknown>
for (const source of args.slice(1)) {
if (source === null || source === undefined) continue
const value = boundedData(source, "Object.assign input")
const value = source
if (isSandboxValue(value)) continue
if (value === null || typeof value !== "object" || Array.isArray(value)) {
throw new InterpreterRuntimeError("Object.assign expects data objects.", node)
Expand Down
40 changes: 40 additions & 0 deletions packages/codemode/test/parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,46 @@ describe("Error values and instanceof", () => {
})

describe("array methods: splice, fill, copyWithin, keys/values/entries", () => {
test("sort and reverse mutate and return the receiver", async () => {
expect(
await value(`
const sorted = [3, 1, 2]
const sortResult = sorted.sort((a, b) => a - b)
const reversed = [1, 2, 3]
const reverseResult = reversed.reverse()
return { sorted, sameSort: sorted === sortResult, reversed, sameReverse: reversed === reverseResult }
`),
).toEqual({ sorted: [1, 2, 3], sameSort: true, reversed: [3, 2, 1], sameReverse: true })
})

test("array callbacks receive the receiver and observe later mutations", async () => {
expect(
await value(`
const values = [1, 2, 3]
const seen = values.map((value, index, receiver) => {
if (index === 0) values[1] = 9
return [value, receiver === values]
})
return seen
`),
).toEqual([
[1, true],
[9, true],
[3, true],
])
expect(
await value(`
const values = [1, 2, 3]
const seen = []
values.forEach((value, index) => {
seen.push(value)
if (index === 0) values.pop()
})
return seen
`),
).toEqual([1, 2])
})

test("splice removes in place and returns the removed elements", async () => {
expect(await value(`const a = [1,2,3,4]; const removed = a.splice(1, 2); return { removed, a }`)).toEqual({
removed: [2, 3],
Expand Down
20 changes: 20 additions & 0 deletions packages/codemode/test/stdlib.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,26 @@ describe("Set", () => {
})

describe("stdlib integration", () => {
test("Object.assign mutates and returns its target", async () => {
expect(
await value(`
const target = { a: 1 }
const result = Object.assign(target, { b: 2 })
return { target, result, same: target === result }
`),
).toEqual({ target: { a: 1, b: 2 }, result: { a: 1, b: 2 }, same: true })
expect(await value(`try { Object.assign(null, { a: 1 }); return false } catch { return true }`)).toBe(true)
})

test("assignment resolves and reads its left side before evaluating the right side", async () => {
expect(await value(`let x = 1; x += (x = 5); return x`)).toBe(6)
expect(await value(`let i = 0; const values = [9]; values[i++] = i; return [values, i]`)).toEqual([[1], 1])
expect(await value(`let i = 0; const values = [10, 20]; values[i++] += i; return [values, i]`)).toEqual([
[11, 20],
1,
])
})

test("typeof reports constructors as functions and never throws", async () => {
expect(await value(`return typeof Map`)).toBe("function")
expect(await value(`return typeof ((x) => x)`)).toBe("function")
Expand Down
Loading