{"record":{"id":"e464a66e33659e63","repo":"pydantic/monty","slug":"component-value-arena-contains-a-cycle","errorCode":null,"errorMessage":"component value arena contains a cycle","messagePattern":"component value arena contains a cycle","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"crates/monty-js/ts/worker/value.ts","lineNumber":270,"sourceCode":"  return {\n    tag: 'file-handle',\n    val: { path: object.path, mode: canonicalFileMode(object.mode), position: BigInt(position) },\n  }\n}\n\n/** Appends key/value pairs while preserving their insertion order. */\nfunction pushPairs(pairs: [unknown, unknown][], nodes: ValueNode[]): NodePair[] {\n  return pairs.map(([key, value]) => ({ key: pushValue(key, nodes), value: pushValue(value, nodes) }))\n}\n\n/** Fetches a raw arena node by index with the same bounds/cycle checks as\n *  `readValue`, for callers that must inspect the node's tag. The index stays\n *  marked as visiting, so a parent cycle in class-type nodes throws instead\n *  of recursing forever. */\nfunction readValueNode(index: number, nodes: ValueNode[], visiting: Set<number>): ValueNode {\n  const node = nodes[index]\n  if (node === undefined) throw new Error(`component value node index ${index} is out of bounds`)\n  if (visiting.has(index)) throw new Error('component value arena contains a cycle')\n  visiting.add(index)\n  return node\n}\n\n/** Reads one arena node recursively, rejecting malformed indexes and cycles. */\nfunction readValue(index: number, nodes: ValueNode[], visiting: Set<number>): unknown {\n  const node = nodes[index]\n  if (node === undefined) throw new Error(`component value node index ${index} is out of bounds`)\n  if (visiting.has(index)) throw new Error('component value arena contains a cycle')\n  visiting.add(index)\n  let value: unknown\n  switch (node.tag) {\n    case 'ellipsis':\n      value = { [TYPE_MARKER]: 'Ellipsis' }\n      break\n    case 'not-implemented':\n      value = { [TYPE_MARKER]: 'NotImplemented' }\n      break","sourceCodeStart":252,"sourceCodeEnd":288,"githubUrl":"https://github.com/pydantic/monty/blob/adc986b362e3961f407868cb118a99fe831b9e61/crates/monty-js/ts/worker/value.ts#L252-L288","documentation":"While reading a class-type's component value nodes, the decoder keeps a `visiting` set of in-flight indexes; re-entering an index that is already being read means the arena graph contains a cycle (e.g. a class type whose attribute value loops back to the class type itself). This Error aborts decoding instead of recursing forever. Note readValueNode leaves the index marked as visiting, so parent-level cycles are caught too.","triggerScenarios":"Decoding an arena where a class-type node's attr value index chain returns to an ancestor node; hand-built arenas that model cyclic structures (which the flat format does not allow); a producer bug that emits self-referencing nodes.","commonSituations":"Custom encoders attempting to serialize recursive JS objects (e.g. `obj.self = obj`) into the arena without cycle detection on the encode side; a mismatch between the Rust encoder and the TS decoder versions.","solutions":["Break the cycle on the encode side: encode one direction of the reference only, or represent back-references as ids rather than nested nodes.","Validate the arena is a DAG before decoding (DFS with the same visiting-set logic).","If producing arenas with the library's own encoder, report/upgrade — self-produced cyclic arenas should not be encodable.","Pin matching versions of the monty binary and the JS package so both sides agree the format is acyclic."],"exampleFix":"// before\n// classNode.attrs = [['parent', <index of classNode itself>]]\n// after\n// classNode.attrs = [['name', <index of a leaf value node>]]","handlingStrategy":"validation","validationCode":"function assertAcyclic(nodes) {\n  const visiting = new Set();\n  function dfs(i) {\n    if (visiting.has(i)) throw new Error(`cycle at node ${i}`);\n    visiting.add(i);\n    for (const ref of referencedIndexes(nodes[i])) dfs(ref);\n    visiting.delete(i);\n  }\n  dfs(0);\n}","typeGuard":null,"tryCatchPattern":"try {\n  const value = decodeValue(nodes);\n} catch (e) {\n  if (e instanceof Error && e.message === 'component value arena contains a cycle') {\n    throw new Error('encoder produced a cyclic arena; back-references must use identity ids');\n  } else throw e;\n}","preventionTips":["Never encode recursive JS objects directly — break cycles before building the arena","Represent back-references with uuid identity nodes, not repeated indexes","Run an acyclicity check (DFS with visiting set) on any hand-built arena","Keep encoder and decoder versions aligned so both agree the format is acyclic"],"tags":["typescript","wasm","arena","decoding","cycle"],"backgroundTag":"internal-invariant-violation","analyzedSha":"adc986b362e3961f407868cb118a99fe831b9e61","analyzedAt":"2026-09-13T19:19:18.698Z","contentChangedAt":"2026-09-13T19:19:18.698Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}