( node: ASTNode, context: EvaluationContext, schemaCtx?: SchemaContext, )
| 40 | * Evaluate an AST node to a runtime value. |
| 41 | */ |
| 42 | export function evaluate( |
| 43 | node: ASTNode, |
| 44 | context: EvaluationContext, |
| 45 | schemaCtx?: SchemaContext, |
| 46 | ): unknown { |
| 47 | switch (node.k) { |
| 48 | // ── Literals ────────────────────────────────────────────────────────── |
| 49 | case "Str": |
| 50 | return node.v; |
| 51 | case "Num": |
| 52 | return node.v; |
| 53 | case "Bool": |
| 54 | return node.v; |
| 55 | case "Null": |
| 56 | return null; |
| 57 | case "Ph": |
| 58 | return null; |
| 59 | |
| 60 | // ── State references ────────────────────────────────────────────────── |
| 61 | case "StateRef": |
| 62 | return context.extraScope?.[node.n] ?? context.getState(node.n); |
| 63 | |
| 64 | // ── References ──────────────────────────────────────────────────────── |
| 65 | case "Ref": |
| 66 | case "RuntimeRef": |
| 67 | return context.resolveRef(node.n); |
| 68 | |
| 69 | // ── Collections ─────────────────────────────────────────────────────── |
| 70 | case "Arr": |
| 71 | return node.els.map((el) => evaluate(el, context)); |
| 72 | case "Obj": |
| 73 | return Object.fromEntries(node.entries.map(([k, v]) => [k, evaluate(v, context)])); |
| 74 | |
| 75 | // ── Component ───────────────────────────────────────────────────────── |
| 76 | case "Comp": { |
| 77 | // Lazy builtins — control their own evaluation |
| 78 | if (LAZY_BUILTINS.has(node.name)) { |
| 79 | return evaluateLazyBuiltin(node.name, node.args, context, schemaCtx); |
| 80 | } |
| 81 | // Check shared builtin registry first |
| 82 | const builtin = BUILTINS[node.name]; |
| 83 | if (builtin) { |
| 84 | const args = node.args.map((a) => evaluate(a, context)); |
| 85 | return builtin.fn(...args); |
| 86 | } |
| 87 | // Action calls → evaluate to ActionPlan/ActionStep |
| 88 | if (ACTION_NAMES.has(node.name)) { |
| 89 | return evaluateActionCall(node.name, node.args, context); |
| 90 | } |
| 91 | // If parser already mapped args→props (via materializeExpr), use named props. |
| 92 | // With schema context: emit ReactiveAssign for StateRef on reactive props. |
| 93 | // Without schema context: preserve StateRef as AST for evaluate-tree to handle. |
| 94 | if (node.mappedProps) { |
| 95 | const def = schemaCtx?.library.components[node.name]; |
| 96 | const props: Record<string, unknown> = {}; |
| 97 | for (const [key, val] of Object.entries(node.mappedProps)) { |
| 98 | const propSchema = def?.props?.shape?.[key]; |
| 99 | if (val.k === "StateRef" && propSchema && isReactiveSchema(propSchema)) { |
no test coverage detected