.iac files go through a multi-stage pipeline before Pulumi ever runs.
Pipeline Stages
Stage 1 — Lexer
Tokenizes the.iac source into keywords, identifiers, strings, numbers, and punctuation. Recognises ubx-specific tokens: ~ (pending sigil), block keywords (unit, component, deploy, sync, input, output, local, data, extend, import, moved, policy, remote, interface, workspace, test, provider).
Stage 2 — Parser
Builds a typed Abstract Syntax Tree (AST). Each block type has its own AST node:UnitBlock—unit "type" "name" { ... }OutputBlock—output "name" { value = expr }DataBlock—data "type" "name" { ... }PolicyBlock—policy "name" { rule { ... } }- etc.
StringLiteral, NumberLiteral, BoolLiteral, ObjectExpr, ListExpr, PendingRef, ResolvedRef, Interpolation, FunctionCall, or ConditionalExpr.
Stage 3 — Merger
Appliesextend block overrides for the target environment (--env flag). Merges override attributes into base blocks. Extend blocks for environments other than the target are discarded.
Stage 4a — Schema Validation
Checks allunit block attributes against the provider schema registry:
- Required attributes must be present
- Unknown attributes produce warnings
- Type mismatches produce errors
Stage 4b — Policy Evaluation
Evaluates allpolicy block rules against matching unit blocks. Violations produce errors (severity = "error") or warnings (severity = "warn").
Stage 4c — Type Checker
- Resolves all symbol references (
input.name,local.name,~unit.type.name.attr) - Classifies every expression as
Resolved<T>orPending<T> - Detects circular dependencies
- Validates
depends_onreferences - Validates
datablock references - Validates cross-stack
~@name.outputreferences
Stage 5 — Code Generator
Emits valid Pulumi TypeScript:unitblocks →new Provider.ResourceName("name", { ... })inputblocks →new pulumi.Config().get("name") ?? defaultoutputblocks →export const name = valuedatablocks →pulumi.output(provider.getResource({ ... }))Pending<T>refs →.apply()chains orpulumi.all([]).apply()secret()calls → helper functions +pulumi.secret()
