(node: ASTNode)
| 58 | // ─── AST node serializer ───────────────────────────────────────────────────── |
| 59 | |
| 60 | function serializeASTNode(node: ASTNode): string { |
| 61 | switch (node.k) { |
| 62 | case "Str": |
| 63 | return JSON.stringify(node.v); |
| 64 | case "Num": |
| 65 | return String(node.v); |
| 66 | case "Bool": |
| 67 | return node.v ? "true" : "false"; |
| 68 | case "Null": |
| 69 | return "null"; |
| 70 | case "Ph": |
| 71 | return node.n; |
| 72 | case "StateRef": |
| 73 | return node.n; // already includes $ prefix |
| 74 | case "RuntimeRef": |
| 75 | return node.n; |
| 76 | case "Arr": |
| 77 | return "[" + node.els.map(serializeASTNode).join(", ") + "]"; |
| 78 | case "Obj": |
| 79 | return "{" + node.entries.map(([k, v]) => `${k}: ${serializeASTNode(v)}`).join(", ") + "}"; |
| 80 | case "BinOp": { |
| 81 | const left = serializeBinOpChild(node.left, node.op, "left"); |
| 82 | const right = serializeBinOpChild(node.right, node.op, "right"); |
| 83 | return `${left} ${node.op} ${right}`; |
| 84 | } |
| 85 | case "UnaryOp": |
| 86 | return `${node.op}${serializeASTNode(node.operand)}`; |
| 87 | case "Ternary": |
| 88 | return `${serializeASTNode(node.cond)} ? ${serializeASTNode(node.then)} : ${serializeASTNode(node.else)}`; |
| 89 | case "Member": |
| 90 | return `${serializeASTNode(node.obj)}.${node.field}`; |
| 91 | case "Index": |
| 92 | return `${serializeASTNode(node.obj)}[${serializeASTNode(node.index)}]`; |
| 93 | case "Assign": |
| 94 | return `${node.target} = ${serializeASTNode(node.value)}`; |
| 95 | case "Comp": { |
| 96 | const args = node.args.map(serializeASTNode).join(", "); |
| 97 | // Builtin (not Action) → @-prefix |
| 98 | if (isBuiltin(node.name) && node.name !== "Action") { |
| 99 | return `@${node.name}(${args})`; |
| 100 | } |
| 101 | // Action, reserved calls (Query/Mutation), catalog components → no prefix |
| 102 | return `${node.name}(${args})`; |
| 103 | } |
| 104 | // Ref nodes should not appear after materialization, but handle defensively |
| 105 | case "Ref": |
| 106 | return node.n; |
| 107 | default: |
| 108 | return "null"; |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | /** Wrap a BinOp child in parens if its precedence is lower than the parent's. */ |
| 113 | function serializeBinOpChild(child: ASTNode, parentOp: string, _side: "left" | "right"): string { |
no test coverage detected