( node: ASTNode, ctx: MaterializeCtx, scopedRefs: ReadonlySet<string>, )
| 97 | } |
| 98 | |
| 99 | function materializeExprInternal( |
| 100 | node: ASTNode, |
| 101 | ctx: MaterializeCtx, |
| 102 | scopedRefs: ReadonlySet<string>, |
| 103 | ): ASTNode { |
| 104 | switch (node.k) { |
| 105 | case "Ref": |
| 106 | return scopedRefs.has(node.n) ? node : (resolveRef(node.n, ctx, "expr") as ASTNode); |
| 107 | |
| 108 | case "Ph": |
| 109 | return node; |
| 110 | |
| 111 | case "Comp": { |
| 112 | const lazy = materializeLazyBuiltin(node, ctx, scopedRefs); |
| 113 | if (lazy) return lazy; |
| 114 | const recursedArgs = node.args.map((a) => materializeExprInternal(a, ctx, scopedRefs)); |
| 115 | // Builtins, reserved calls, and action calls: recurse args, keep as-is |
| 116 | if (isBuiltin(node.name) || isReservedCall(node.name)) { |
| 117 | return { ...node, args: recursedArgs }; |
| 118 | } |
| 119 | // Catalog component: add mappedProps for the evaluator |
| 120 | const def = ctx.cat?.get(node.name); |
| 121 | if (def) { |
| 122 | const mappedProps: Record<string, ASTNode> = {}; |
| 123 | for (let i = 0; i < def.params.length && i < recursedArgs.length; i++) { |
| 124 | mappedProps[def.params[i].name] = recursedArgs[i]; |
| 125 | } |
| 126 | return { ...node, args: recursedArgs, mappedProps }; |
| 127 | } |
| 128 | // Unknown component in expression: push error (same as value path) |
| 129 | ctx.errors.push({ |
| 130 | code: "unknown-component", |
| 131 | component: node.name, |
| 132 | path: "", |
| 133 | message: `Unknown component "${node.name}" — not found in catalog or builtins`, |
| 134 | statementId: ctx.currentStatementId, |
| 135 | }); |
| 136 | return { ...node, args: recursedArgs }; |
| 137 | } |
| 138 | |
| 139 | case "Arr": |
| 140 | return { ...node, els: node.els.map((e) => materializeExprInternal(e, ctx, scopedRefs)) }; |
| 141 | case "Obj": |
| 142 | return { |
| 143 | ...node, |
| 144 | entries: node.entries.map( |
| 145 | ([k, v]) => [k, materializeExprInternal(v, ctx, scopedRefs)] as [string, ASTNode], |
| 146 | ), |
| 147 | }; |
| 148 | case "BinOp": |
| 149 | return { |
| 150 | ...node, |
| 151 | left: materializeExprInternal(node.left, ctx, scopedRefs), |
| 152 | right: materializeExprInternal(node.right, ctx, scopedRefs), |
| 153 | }; |
| 154 | case "UnaryOp": |
| 155 | return { ...node, operand: materializeExprInternal(node.operand, ctx, scopedRefs) }; |
| 156 | case "Ternary": |
no test coverage detected