fix(codemode): close symbol cycle gaps

This commit is contained in:
Kit Langton 2026-08-28 23:20:20 -04:00
parent a7deeaf296
commit 2a849c0fef
3 changed files with 27 additions and 1 deletions

View file

@ -109,7 +109,7 @@ export const rejectCircularInsertion = (container: object, value: unknown, label
throw new InterpreterRuntimeError(`${label} contains a circular value.`, node, "InvalidDataValue")
if (current === null || typeof current !== "object" || isRuntimeReference(current) || seen.has(current)) continue
seen.add(current)
pending.push(Array.isArray(current) ? current[Symbol.iterator]() : childValues(current))
pending.push(childValues(current))
}
}

View file

@ -70,6 +70,7 @@ export const invokeObjectMethod = (name: string, args: Array<unknown>, node: Ast
continue
}
if (key !== AsyncIteratorSymbol && key !== IteratorSymbol) continue
if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue
const item = Reflect.get(source, key)
rejectCircularInsertion(out, item, "Object.assign result", node)
Reflect.set(out, key, item)

View file

@ -17,6 +17,8 @@
import { describe, expect, test } from "bun:test"
import { Effect, Schema } from "effect"
import { CodeMode, Tool } from "../src/index.js"
import { IteratorSymbol } from "../src/interpreter/model.js"
import { invokeObjectMethod } from "../src/stdlib/object.js"
// Standard-library value types: Date, RegExp, Map, Set. Programs use them as ordinary JS;
// intra-CodeMode checkpoints (Object.* helpers, spread, coercion inputs) preserve the live
@ -824,6 +826,29 @@ describe("stdlib integration", () => {
expect(await value(`try { Object.assign(null, { a: 1 }); return false } catch { return true }`)).toBe(true)
})
test("Object.assign ignores non-enumerable supported symbols without reading them", () => {
const target = {}
const reads: Array<boolean> = []
const source = Object.defineProperty({}, IteratorSymbol, {
get() {
reads.push(true)
return target
},
})
expect(invokeObjectMethod("assign", [target, source], { type: "CallExpression" })).toBe(target)
expect(reads).toEqual([])
expect(Object.hasOwn(target, IteratorSymbol)).toBe(false)
})
test("Object.assign rejects cycles through supported symbols on nested arrays", () => {
const target = {}
const nested = Object.defineProperty([], IteratorSymbol, { enumerable: true, value: target })
expect(() => invokeObjectMethod("assign", [target, { nested }], { type: "CallExpression" })).toThrow(
"Object.assign result contains a circular value.",
)
expect(Object.hasOwn(target, "nested")).toBe(false)
})
test("Object.assign rejects direct and nested cycles", async () => {
expect(
await value(`