planner

package
v0.0.0-...-1255d63 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 24, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func EvaluateExpr

func EvaluateExpr(expr *ExprIR, getValue ValueLookup) (any, error)

EvaluateExpr evaluates an expression using a lookup function for values. Used during the resolution phase to evaluate conditions like `@var.ENV == "prod"`.

The getValue function looks up values by name:

  • For ExprVarRef, looks up by variable name (e.g., "ENV")
  • For ExprDecoratorRef, looks up by decorator key (e.g., "env.HOME")

For ExprBinaryOp, recursively evaluates operands and applies the operator.

func IsTruthy

func IsTruthy(v any) bool

IsTruthy determines if a value is truthy. Follows common scripting language conventions: - nil is falsy - false is falsy - 0 (any numeric type) is falsy - "" (empty string) is falsy - Everything else is truthy

func Plan

func Plan(events []parser.Event, tokens []lexer.Token, config Config) (*planfmt.Plan, error)

Plan is the canonical planner entrypoint.

func RenderCommand

func RenderCommand(cmd *CommandExpr, displayIDs map[string]string) string

RenderCommand renders a command expression to a string with DisplayID placeholders. Concatenates all parts, rendering each according to its type. Trims leading and trailing whitespace from the result.

func RenderExpr

func RenderExpr(expr *ExprIR, displayIDs map[string]string) string

RenderExpr renders an expression to a string with DisplayID placeholders. Used during the emit phase to build plan commands.

For literals, returns the string representation. For var refs and decorator refs, looks up the DisplayID in the map. Binary ops are not supported (they're for conditions, not interpolation).

Types

type ArgIR

type ArgIR struct {
	Name  string // Parameter name
	Value *ExprIR
}

ArgIR represents a decorator argument.

type BlockerIR

type BlockerIR struct {
	Kind       BlockerKind
	Depth      int            // Nesting level (for wave-based resolution)
	Condition  *ExprIR        // Condition expression (for if/when)
	ThenBranch []*StatementIR // Statements if condition is true
	ElseBranch []*StatementIR // Statements if condition is false (optional)
	Taken      *bool          // Set by Resolver: true=then, false=else, nil=unresolved

	// For-loop specific
	LoopVar    string  // Loop variable name (for "for x in ...")
	Collection *ExprIR // Collection expression (for "for x in collection")

	// Set by Resolver for for-loops: resolved iterations with deep-copied body
	Iterations []LoopIteration

	// When-specific (pattern matching)
	Arms []*WhenArmIR // Pattern arms (for "when expr { pattern -> ... }")

	// Set by Resolver for when: index of the matched arm (-1 if none)
	MatchedArm int
}

BlockerIR represents control flow that blocks execution until resolved. The Resolver evaluates conditions and sets the Taken flag.

After resolution:

  • if/when: Taken is set, untaken branch is nil (pruned)
  • for: Iterations contains resolved loop iterations with deep-copied body

type BlockerKind

type BlockerKind int

BlockerKind identifies the type of control flow blocker.

const (
	BlockerIf   BlockerKind = iota // if condition { ... } else { ... }
	BlockerWhen                    // when expr { pattern -> ... }
	BlockerFor                     // for item in collection { ... }
)

type CallTraceStmtIR

type CallTraceStmtIR struct {
	Label string         // e.g. deploy(token=opal:abc123) in rendered output
	Block []*StatementIR // Fully resolved expanded statements
}

CallTraceStmtIR wraps expanded statements with function-call provenance. This is display-only metadata and does not affect execution semantics.

type CommandExpr

type CommandExpr struct {
	Parts []*ExprIR // Sequence of literals and expression references
}

CommandExpr represents a command with interpolated expressions. A command like `echo "Hello @var.NAME"` becomes:

Parts: [Literal("echo \"Hello "), VarRef("NAME"), Literal("\"")]

Named CommandExpr to distinguish expression-level command parts from statement-level command IR types.

type CommandStmtIR

type CommandStmtIR struct {
	Decorator      string         // "@shell", "@retry", etc.
	Command        *CommandExpr   // The command with interpolated expressions
	Args           []ArgIR        // Decorator arguments
	Block          []*StatementIR // Nested statements (for decorator blocks)
	Operator       string         // "&&", "||", "|", ";" - chain to next command
	RedirectMode   string         // ">", ">>" - redirect mode (empty if no redirect)
	RedirectTarget *CommandExpr   // For redirect operators, the target path (nil otherwise)
}

CommandStmtIR represents a command statement.

type Config

type Config struct {
	Target    string           // Command name (e.g. "hello") or "" for script mode.
	Args      []FunctionArg    // Optional target function arguments (positional + named).
	Context   context.Context  // Optional planning context for cancellation/deadlines.
	IDFactory secret.IDFactory // Optional deterministic placeholder factory.
	Vault     *vault.Vault     // Optional shared vault for value storage/scrubbing.
	PlanSalt  []byte           // Optional deterministic salt (32 bytes) for contract verification.
	Telemetry TelemetryLevel   // Telemetry level (production-safe).
	Debug     DebugLevel       // Debug level (development only).
}

Config configures planner behavior.

type DebugEvent

type DebugEvent struct {
	Timestamp time.Time
	Event     string
	EventPos  int
	Context   string
}

DebugEvent captures planner trace/debug events.

type DebugLevel

type DebugLevel int

DebugLevel controls debug tracing.

const (
	DebugOff DebugLevel = iota
	DebugPaths
	DebugDetailed
)

type DecoratorRef

type DecoratorRef struct {
	Name     string    // Decorator name: "env", "aws", "var", "shell", etc.
	Selector []string  // Property path: ["HOME"], ["secret", "api_key"]
	Args     []*ExprIR // For parameterized decorators: @retry(3, "1s")
	ArgNames []string  // Argument keys aligned with Args (canonical names; empty means positional source)
}

DecoratorRef is a structured decorator reference. Represents @decorator.selector.path with optional arguments.

type DecoratorResolutionMetrics

type DecoratorResolutionMetrics struct {
	TotalCalls   int
	BatchCalls   int
	BatchSizes   []int
	TotalTime    time.Duration
	SkippedCalls int
}

DecoratorResolutionMetrics tracks per-decorator resolution metrics.

type Emitter

type Emitter struct {
	// contains filtered or unexported fields
}

Emitter transforms a resolved IR tree into a planfmt.Plan. It traverses the pruned tree from the Resolver and builds: - planfmt.Step for each command - SecretUses for contract verification - DisplayID placeholders for secrets

func NewEmitter

func NewEmitter(result *ResolveResult, v *vault.Vault, scopes *ScopeStack, target string) *Emitter

NewEmitter creates a new Emitter.

func (*Emitter) Emit

func (e *Emitter) Emit() (*planfmt.Plan, error)

Emit transforms the resolved IR into a Plan.

type EnumMemberIR

type EnumMemberIR struct {
	Name  string
	Value *ExprIR
}

EnumMemberIR represents a single enum member declaration.

type EnumTypeIR

type EnumTypeIR struct {
	Name     string
	BaseType string
	Members  []EnumMemberIR
	Span     SourceSpan
}

EnumTypeIR represents a top-level user-defined enum declaration.

type EvalError

type EvalError struct {
	Message string
	VarName string     // Variable or decorator name (if applicable)
	Span    SourceSpan // Source location (if available)
}

EvalError represents an error during expression evaluation.

func (*EvalError) Error

func (e *EvalError) Error() string

type ExecutionGraph

type ExecutionGraph struct {
	Statements []*StatementIR         // Top-level statements (script mode)
	Functions  map[string]*FunctionIR // Function definitions (command mode)
	Types      map[string]*StructTypeIR
	Enums      map[string]*EnumTypeIR
	Scopes     *ScopeStack // Variable scopes (name → exprID)
}

ExecutionGraph is the complete execution graph built from parser events. Contains all possible execution paths - the Resolver determines which are taken.

func BuildIR

func BuildIR(events []parser.Event, tokens []lexer.Token) (*ExecutionGraph, error)

BuildIR constructs an ExecutionGraph from parser events and tokens. This is a pure structural pass - no resolution or condition evaluation.

type ExprIR

type ExprIR struct {
	Kind ExprKind
	Span SourceSpan

	// For ExprLiteral - the actual value (string, int, bool, etc.)
	Value any

	// For ExprVarRef - the variable name (without @var. prefix)
	VarName string

	// For ExprDecoratorRef - structured decorator reference
	Decorator *DecoratorRef

	// For ExprEnumMemberRef - enum type and member
	EnumName   string
	EnumMember string

	// For ExprBinaryOp - operator and operands
	Op    string  // "==", "!=", "&&", "||", "<", ">", "<=", ">="
	Left  *ExprIR // Left operand
	Right *ExprIR // Right operand

	// For ExprTypeCast - target type and optionality
	TypeName string // Target type name (String, Int, Object, etc.)
	Optional bool   // True for Type? casts
}

ExprIR is the unified expression representation. Used for both conditions (if @var.X == "prod") and command interpolation (echo @var.X). This unification prevents divergence between two separate expression systems.

type ExprKind

type ExprKind int

ExprKind identifies the type of expression.

const (
	ExprLiteral       ExprKind = iota // Literal value (string, int, bool)
	ExprVarRef                        // Variable reference (@var.X)
	ExprDecoratorRef                  // Decorator reference (@env.HOME, @aws.secret.key)
	ExprEnumMemberRef                 // Enum member reference (Type.Member)
	ExprBinaryOp                      // Binary operation (==, !=, &&, ||)
	ExprTypeCast                      // Type cast (expr as Type, expr as Type?)
)

type FunctionArg

type FunctionArg struct {
	Name  string
	Value any
}

FunctionArg represents one target function argument. Empty Name means positional argument.

type FunctionCallStmtIR

type FunctionCallStmtIR struct {
	Name string  // Function name
	Args []ArgIR // Call arguments (positional and named)
}

FunctionCallStmtIR represents a function call statement.

type FunctionIR

type FunctionIR struct {
	Name   string
	Params []ParamIR // Function parameters
	Body   []*StatementIR
	Span   SourceSpan
	Scopes *ScopeStack // Scope snapshot for command mode prelude
}

FunctionIR represents a function definition.

type LoopIteration

type LoopIteration struct {
	Value any            // Loop variable value for this iteration
	Body  []*StatementIR // Deep-copied statements for this iteration
}

LoopIteration represents one iteration of a resolved for-loop. Each iteration has its own deep-copied body to allow independent resolution of nested blockers (e.g., if statements inside the loop).

type ParamIR

type ParamIR struct {
	Name    string
	Type    string  // Type annotation (optional)
	Default *ExprIR // Default value (optional)
}

ParamIR represents a function parameter.

type PlanError

type PlanError struct {
	Message     string
	Context     string
	EventPos    int
	TotalEvents int
	Suggestion  string
	Example     string
}

PlanError represents a planning error with contextual hints.

func (*PlanError) Error

func (e *PlanError) Error() string

type PlanResult

type PlanResult struct {
	Plan        *planfmt.Plan
	PlanTime    time.Duration
	Telemetry   *PlanTelemetry
	DebugEvents []DebugEvent
}

PlanResult holds a generated plan and optional observability data.

func PlanWithObservability

func PlanWithObservability(events []parser.Event, tokens []lexer.Token, config Config) (*PlanResult, error)

PlanWithObservability is the canonical observability entrypoint.

type PlanTelemetry

type PlanTelemetry struct {
	EventCount int
	StepCount  int

	DecoratorResolutions map[string]*DecoratorResolutionMetrics
}

PlanTelemetry holds planner metrics.

type ResolveConfig

type ResolveConfig struct {
	TargetFunction string          // Empty = script mode, non-empty = command mode
	FunctionArgs   []FunctionArg   // Optional command-mode function arguments
	Context        context.Context // Execution context
	PlanHash       []byte          // Deterministic plan hash/salt for value resolution context
	StepPath       string          // Step path prefix for value resolution provenance
	Telemetry      *PlanTelemetry  // Optional telemetry sink
	TelemetryLevel TelemetryLevel  // Telemetry level
}

ResolveConfig configures the resolution process.

type ResolveResult

type ResolveResult struct {
	Statements       []*StatementIR    // Pruned tree (only taken branches, nested blockers resolved)
	DecoratorExprIDs map[string]string // Decorator key → exprID for display ID lookup
	EnumMemberValues map[string]string // Enum member key (Type.Member) -> resolved string value
}

ResolveResult contains the resolved execution tree. The tree preserves structure for rich dry-run output:

  • Blockers remain as nodes with Taken set and untaken branches pruned
  • For-loops have Iterations populated with resolved values and deep-copied bodies
  • Try/catch blocks are preserved as-is (runtime constructs)

func Resolve

func Resolve(graph *ExecutionGraph, v *vault.Vault, session decorator.Session, config ResolveConfig) (*ResolveResult, error)

Resolve processes the execution graph and resolves all expressions. Returns a ResolveResult containing the pruned execution tree, or an error if resolution fails (undefined variables, decorator failures, etc.).

The returned tree preserves structure for rich dry-run output:

  • Blockers remain as nodes with Taken set and untaken branches pruned (set to nil)
  • For-loops have Iterations populated with resolved values and deep-copied bodies
  • Try/catch blocks are preserved as-is (runtime constructs)

type Resolver

type Resolver struct {
	// contains filtered or unexported fields
}

Resolver processes an ExecutionGraph, resolving all expressions and determining which branches are taken. Uses a wave-based resolution model.

Scope Semantics: Metaprogramming vs Execution Blocks

Opal has two kinds of blocks with different scoping rules:

**Language control blocks (if, for, when, fun)** - These are METAPROGRAMMING constructs that are evaluated at plan-time and "disappear" in the final plan. Variables declared inside these blocks LEAK to the outer scope because the block structure is flattened away.

var OUTER = "before"
if @var.COND {
    var INNER = "inside"   // Leaks to outer scope!
}
echo @var.INNER            // Works if COND was true

After planning (if COND is true), this becomes:

var OUTER = "before"
var INNER = "inside"       // Flattened - no if block anymore
echo @var.INNER

**Execution blocks (try/catch, @retry, @timeout, etc.)** - These are RUNTIME constructs that remain in the plan. Variables declared inside these blocks are ISOLATED and do NOT leak to the outer scope.

var counter = 0
try {
    var counter = 5        // Local copy, doesn't affect outer
}
echo @var.counter          // Still 0

This distinction exists because:

  • Metaprogramming blocks resolve deterministically at plan-time
  • Execution blocks have runtime behavior (retries, exceptions) that's non-deterministic

Blockers

A "blocker" is a control flow construct (if/when/for) that cannot be evaluated until its condition or collection expression is resolved. We can't know which branch to take (if/when) or how many iterations to unroll (for) until the controlling expression has a concrete value.

Example blockers:

  • `if @var.ENV == "prod"` - blocked until ENV is resolved
  • `for item in @var.ITEMS` - blocked until ITEMS collection is resolved
  • `when @env.REGION` - blocked until REGION is resolved

Sequential Processing with Flattening

Because metaprogramming blocks leak variables, statements AFTER a blocker must wait until the blocker is evaluated and its taken branch is processed. This ensures variables declared in taken branches are visible to subsequent statements.

Processing order for: `var A; if COND { var B }; echo @var.B`

  1. Process `var A` - add to scope
  2. Hit blocker `if COND` - STOP, resolve COND
  3. Evaluate blocker - condition is true
  4. Queue: taken branch statements + remaining statements after blocker
  5. Process `var B` from taken branch - add to scope
  6. Process `echo @var.B` - B is now visible!

Wave-Based Resolution

Resolution proceeds in waves, processing statements sequentially:

  1. Collect expressions until we hit a blocker
  2. Batch resolve all collected expressions (grouped by decorator type)
  3. Evaluate the blocker, determine taken branch
  4. Queue: taken branch + remaining statements after blocker
  5. Repeat until no more statements

For-Loop Unrolling

For-loops are blockers because we can't unroll until the collection is resolved. Once resolved, unrolling produces flat statements with VarDecl injections:

for item in ["a", "b"] { echo @var.item }

Unrolls to:

VarDecl: item = "a"
echo @var.item
VarDecl: item = "b"
echo @var.item

Each VarDecl rebinds the loop variable before its iteration's body statements. This ensures nested blockers (like `if @var.item == "b"`) see the correct value.

Branch Pruning

Untaken branches are never traversed or resolved. This provides:

  • Security: Secrets in untaken branches are never resolved or exposed
  • Efficiency: No wasted API calls for unused decorator values

Key Principles

  • Sequential processing: Statements after blockers wait for taken branch
  • Flattening semantics: Metaprogramming blocks disappear, variables leak
  • Scope isolation: Execution blocks (try/catch, decorators) don't leak
  • Batch-first: Collect expressions up to blocker, THEN batch resolve
  • Branch pruning: Untaken branches are never resolved

type ScopeStack

type ScopeStack struct {
	// contains filtered or unexported fields
}

ScopeStack tracks variable scopes during IR building. Each scope maps variable names to their expression IDs.

func NewScopeStack

func NewScopeStack() *ScopeStack

NewScopeStack creates a new scope stack with a root scope.

func (*ScopeStack) Clone

func (s *ScopeStack) Clone() *ScopeStack

Clone returns a deep copy of the scope stack.

func (*ScopeStack) Define

func (s *ScopeStack) Define(name, exprID string)

Define adds a variable to the current scope.

func (*ScopeStack) Depth

func (s *ScopeStack) Depth() int

Depth returns the current scope depth.

func (*ScopeStack) Lookup

func (s *ScopeStack) Lookup(name string) (string, bool)

Lookup finds a variable in the scope stack (innermost first). Returns the exprID and true if found, empty string and false otherwise.

func (*ScopeStack) Pop

func (s *ScopeStack) Pop()

Pop removes the current scope.

func (*ScopeStack) Push

func (s *ScopeStack) Push()

Push creates a new scope.

type SourceSpan

type SourceSpan struct {
	File  string // Source file name (empty for inline/REPL)
	Start int    // Start position (byte offset)
	End   int    // End position (byte offset)
}

SourceSpan identifies a range in source code for error messages.

type StatementIR

type StatementIR struct {
	Kind         StatementKind
	Span         SourceSpan
	CreatesScope bool // True for decorator blocks, try/catch

	// Exactly one of these is set based on Kind
	Command      *CommandStmtIR      // For StmtCommand
	VarDecl      *VarDeclIR          // For StmtVarDecl
	Blocker      *BlockerIR          // For StmtBlocker
	Try          *TryIR              // For StmtTry
	FunctionCall *FunctionCallStmtIR // For StmtFunctionCall
	CallTrace    *CallTraceStmtIR    // For StmtCallTrace
}

StatementIR represents a statement in the execution graph.

func DeepCopyStatement

func DeepCopyStatement(stmt *StatementIR) *StatementIR

DeepCopyStatement creates a deep copy of a single statement.

func DeepCopyStatements

func DeepCopyStatements(stmts []*StatementIR) []*StatementIR

DeepCopyStatements creates a deep copy of a statement slice. Used by the Resolver to create independent copies for each loop iteration, allowing nested blockers to be resolved independently.

type StatementKind

type StatementKind int

StatementKind identifies the type of statement.

const (
	StmtCommand      StatementKind = iota // Shell command or decorator invocation
	StmtVarDecl                           // Variable declaration
	StmtBlocker                           // Control flow (if/when/for)
	StmtTry                               // Try/catch/finally error handling
	StmtFunctionCall                      // Function call statement
	StmtCallTrace                         // Call provenance wrapper (display-only)
)

type StructFieldIR

type StructFieldIR struct {
	Name    string
	Type    string
	Default *ExprIR
}

StructFieldIR represents a single struct field.

type StructTypeIR

type StructTypeIR struct {
	Name   string
	Fields []StructFieldIR
	Span   SourceSpan
}

StructTypeIR represents a top-level user-defined struct declaration.

type TelemetryLevel

type TelemetryLevel int

TelemetryLevel controls telemetry collection.

const (
	TelemetryOff TelemetryLevel = iota
	TelemetryBasic
	TelemetryTiming
)

type TryIR

type TryIR struct {
	TryBlock     []*StatementIR // Statements in try block
	CatchBlock   []*StatementIR // Statements in catch block (optional)
	FinallyBlock []*StatementIR // Statements in finally block (optional)
}

TryIR represents try/catch/finally error handling.

type ValueLookup

type ValueLookup func(name string) (any, bool)

ValueLookup is a function that looks up a value by name. Returns (value, true) if found, (nil, false) if not found. Used by EvaluateExpr to look up variable and decorator values.

type VarDeclIR

type VarDeclIR struct {
	Name   string  // Variable name (without @var. prefix)
	Value  *ExprIR // Value expression
	ExprID string  // Unique expression ID (set during IR building)
}

VarDeclIR represents a variable declaration.

type WhenArmIR

type WhenArmIR struct {
	Pattern *ExprIR        // Pattern to match (literal, regex, range, else)
	Body    []*StatementIR // Statements to execute if pattern matches
}

WhenArmIR represents a single arm in a when statement.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL