(node: ASTNode, ctx: MaterializeCtx)
| 199 | * - null for placeholders |
| 200 | */ |
| 201 | export function materializeValue(node: ASTNode, ctx: MaterializeCtx): unknown { |
| 202 | switch (node.k) { |
| 203 | // ── Ref resolution ─────────────────────────────────────────────────── |
| 204 | case "Ref": |
| 205 | return resolveRef(node.n, ctx, "value"); |
| 206 | |
| 207 | // ── Literals → plain values ────────────────────────────────────────── |
| 208 | case "Str": |
| 209 | return node.v; |
| 210 | case "Num": |
| 211 | return node.v; |
| 212 | case "Bool": |
| 213 | return node.v; |
| 214 | case "Null": |
| 215 | return null; |
| 216 | case "Ph": |
| 217 | return null; |
| 218 | |
| 219 | // ── Collections ────────────────────────────────────────────────────── |
| 220 | case "Arr": { |
| 221 | const items: unknown[] = []; |
| 222 | for (const e of node.els) { |
| 223 | // Drop unresolved placeholders from arrays |
| 224 | if (e.k === "Ph") continue; |
| 225 | const value = materializeValue(e, ctx); |
| 226 | // Drop null entries from component/ref resolution (incomplete props, unresolved refs, unknown components) |
| 227 | if (value === null && (e.k === "Comp" || e.k === "Ref")) continue; |
| 228 | items.push(value); |
| 229 | } |
| 230 | return items; |
| 231 | } |
| 232 | case "Obj": { |
| 233 | const o: Record<string, unknown> = {}; |
| 234 | for (const [k, v] of node.entries) o[k] = materializeValue(v, ctx); |
| 235 | return o; |
| 236 | } |
| 237 | |
| 238 | // ── Component nodes ────────────────────────────────────────────────── |
| 239 | case "Comp": { |
| 240 | const { name, args } = node; |
| 241 | |
| 242 | // Builtins (Sum, Count, Filter, Action, etc.) → preserve as ASTNode for runtime |
| 243 | if (isBuiltin(name)) { |
| 244 | const lazy = materializeLazyBuiltin(node, ctx, new Set()); |
| 245 | if (lazy) return lazy; |
| 246 | return { ...node, args: args.map((a) => materializeExpr(a, ctx)) }; |
| 247 | } |
| 248 | |
| 249 | // Inline Query/Mutation (not from a statement-level declaration) → validation error |
| 250 | if (isReservedCall(name)) { |
| 251 | ctx.errors.push({ |
| 252 | code: "inline-reserved", |
| 253 | component: name, |
| 254 | path: "", |
| 255 | message: `${name}() must be declared as a top-level statement, not used inline as a value`, |
| 256 | statementId: ctx.currentStatementId, |
| 257 | }); |
| 258 | return null; |
no test coverage detected