From fd19fa89c4023b8eaeb599e451810dc3b6b68fec Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Wed, 15 Jul 2026 17:24:36 -0500 Subject: [PATCH] fix(codemode): linearize reference walks (#37179) --- .../codemode/src/interpreter/references.ts | 86 ++++++++++++------- 1 file changed, 57 insertions(+), 29 deletions(-) diff --git a/packages/codemode/src/interpreter/references.ts b/packages/codemode/src/interpreter/references.ts index f32b726149..b2ac2d22a2 100644 --- a/packages/codemode/src/interpreter/references.ts +++ b/packages/codemode/src/interpreter/references.ts @@ -34,30 +34,51 @@ export const isRuntimeReference = (value: unknown): boolean => value instanceof ErrorConstructorReference || isCodeModeValue(value) -export const containsRuntimeReference = (value: unknown, seen = new Set()): boolean => { - if (isRuntimeReference(value)) return true - if (value === null || typeof value !== "object") return false - if (seen.has(value)) return false - seen.add(value) - const contains = Array.isArray(value) - ? value.some((item) => containsRuntimeReference(item, seen)) - : Object.values(value).some((item) => containsRuntimeReference(item, seen)) - seen.delete(value) - return contains +function* childValues(value: object): Generator { + if (Array.isArray(value)) { + const length = value.length + for (let index = 0; index < length; index++) yield value[index] + return + } + yield* Object.values(value) +} + +export const containsRuntimeReference = (value: unknown): boolean => { + const pending: Array> = [[value].values()] + const seen = new Set() + while (pending.length > 0) { + const next = pending.at(-1)!.next() + if (next.done) { + pending.pop() + continue + } + const current = next.value + if (isRuntimeReference(current)) return true + if (current === null || typeof current !== "object" || seen.has(current)) continue + seen.add(current) + pending.push(childValues(current)) + } + return false } // CodeMode values are data here, not opaque interpreter references. -export const containsOpaqueReference = (value: unknown, seen = new Set()): boolean => { - if (isCodeModeValue(value)) return false - if (isRuntimeReference(value)) return true - if (value === null || typeof value !== "object") return false - if (seen.has(value)) return false - seen.add(value) - const contains = Array.isArray(value) - ? value.some((item) => containsOpaqueReference(item, seen)) - : Object.values(value).some((item) => containsOpaqueReference(item, seen)) - seen.delete(value) - return contains +export const containsOpaqueReference = (value: unknown): boolean => { + const pending: Array> = [[value].values()] + const seen = new Set() + while (pending.length > 0) { + const next = pending.at(-1)!.next() + if (next.done) { + pending.pop() + continue + } + const current = next.value + if (isCodeModeValue(current)) continue + if (isRuntimeReference(current)) return true + if (current === null || typeof current !== "object" || seen.has(current)) continue + seen.add(current) + pending.push(childValues(current)) + } + return false } // Reject cycles before mutation so later boundary walks remain safe. @@ -66,15 +87,22 @@ export const rejectCircularInsertion = ( value: unknown, label: string, node: AstNode, - seen = new Set(), ): void => { - if (value === container) - throw new InterpreterRuntimeError(`${label} contains a circular value.`, node, "InvalidDataValue") - if (value === null || typeof value !== "object" || isRuntimeReference(value) || seen.has(value)) return - seen.add(value) - const items = Array.isArray(value) ? value : Object.values(value) - for (const item of items) rejectCircularInsertion(container, item, label, node, seen) - seen.delete(value) + const pending: Array> = [[value].values()] + const seen = new Set() + while (pending.length > 0) { + const next = pending.at(-1)!.next() + if (next.done) { + pending.pop() + continue + } + const current = next.value + if (current === container) + 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)) + } } export const typeofValue = (value: unknown): string => {