compiler

package
v0.0.0-...-fc9437c Latest Latest
Warning

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

Go to latest
Published: May 13, 2026 License: Apache-2.0 Imports: 25 Imported by: 0

Documentation

Overview

cel.go contains CEL runtime integration: expression compilation/evaluation and error classification for distinguishing retryable "data pending" errors from expression bugs.

Custom CEL extension functions (plural, .ready(), simpleSchema.toOpenAPI(), .updated(), .dependencies()) are defined in celfuncs.go.

Performance model: CEL environments and programs are compiled eagerly when a Graph spec is first seen (or when it changes). The reconcile loop only evaluates pre-compiled programs — no compilation happens during the resource walk.

Per-instance mutable state (scope, forEach state) is tracked separately in instanceState, keyed by namespace/revision-name.

celfuncs.go defines the custom CEL extension functions registered into the compilation environment: plural(), .ready(), simpleSchema.toOpenAPI(), .updated(), and .dependencies(). These are pure function factories — they produce cel.EnvOption values consumed by CompileGraphSpec and compileDeferredExpressions.

deferred.go validates deferred ($${...}) expressions at compile time. When a node's template produces a child Graph CR, the compiler extracts the child's scope (node IDs + forEach variables) and validates deferred expressions against it. Parse errors and undeclared references are caught at the parent's compile time instead of deferring to the child controller.

Per 004-compilation.md § Recursive Compilation: "The compiler handles arbitrary depth by recursing. $${expr} is depth 1... The mechanism is general; the current patterns are not."

fieldcompat.go validates that CEL expression return types match schema field types.

fieldpath.go extracts referenced field paths from compiled CEL expressions.

The design (005-reconciliation.md) commits to field-path-level hashing: "At graph compilation, the controller walks each compiled expression's AST to extract reference chains — sequences of select operations rooted at a scope variable." This file implements that extraction.

The FieldPath type itself lives in graph/ (it's a pure data type). This file contains the CEL AST walking that produces FieldPaths — compiler logic that depends on cel-go/common/ast.

infertype.go infers DeclTypes from template structure without schema information.

readyrewrite.go implements CEL AST rewrites that substitute member-function calls on scope variables with map lookups on reserved scope variables.

Two rewrites use the same mechanism:

  • `<wk_id>.ready()` → `__kroNodeReady["<wk_id>"]` for Watch node IDs. This lets `.ready()` on a Watch reflect the node's own readyWhen verdict — including when the collection is empty. Per 001-graph.md § readyWhen.

  • `<ident>.dependencies()` → `__kroDeps["<ident>"]` for all node IDs. Per 001-graph.md § propagateWhen.

Both use rewriteMemberCallToMapLookup — a generic AST walker parameterized by the function name to match, the ID set to match against, and the reserved variable to redirect to.

timesolver.go implements compile-time detection of solvable time comparison expressions and runtime threshold evaluation for precise requeue scheduling.

When a CEL expression like `time.now() - timestamp(X) >= duration('2h')` evaluates to false, the solver computes the exact moment it will become true (X + 2h) and returns the duration until that moment as a requeue hint.

Detection happens at compile time via AST pattern matching. Evaluation of the threshold sub-expression happens at runtime against the current scope.

typecache.go tracks the global schema epoch for compilation staleness. Per 004-compilation.md § Type Cache: "The cache tracks a single global generation counter." Any schema change (CRD installed, updated, removed) advances it. Staleness is one integer comparison: current generation exceeds the artifact's recorded generation.

typerefine.go narrows definition and iterator types using compiled expression return types.

typesource.go resolves OpenAPI schemas and builds CEL type declarations.

Index

Constants

View Source
const ReservedDepsMapVar = "__kroDeps"

ReservedDepsMapVar is the CEL scope variable carrying per-node dependency lists for the .dependencies() member function. Maps node ID → list of dependency scope values. Populated before propagateWhen evaluation. Per 001-graph.md § propagateWhen.

View Source
const ReservedNodeReadyVar = "__kroNodeReady"

ReservedNodeReadyVar is the CEL scope variable carrying per-Watch readiness verdicts. Declared as map(string, bool) in the env; populated before each prg.Eval from instanceState. The leading underscore distinguishes it from user-visible identifiers.

Variables

View Source
var ErrDependencyError = dagpkg.ErrCircularDependency

ErrDependencyError is an alias for dag.ErrCircularDependency. Kept for backward compatibility with status.go's error classification.

View Source
var ErrInvalidExpression = errors.New("invalid expression")

ErrInvalidExpression indicates that one or more CEL expressions in the Graph spec failed to compile. This is a permanent error until the spec is fixed.

Functions

func ExtractLiteralGVK

func ExtractLiteralGVK(tmpl map[string]any) *runtimeschema.GroupVersionKind

ExtractLiteralGVK extracts a GVK from a template if apiVersion and kind are literal strings (not CEL expressions). Returns nil if either is missing or contains an expression.

func InferFieldType

func InferFieldType(path string, value any) *apiservercel.DeclType

inferFieldType determines the CEL type of a template value.

func InferObjectType

func InferObjectType(typeName string, tmpl map[string]any) *apiservercel.DeclType

inferObjectType builds a DeclType from a template map. Each key becomes a typed field. Nested maps produce nested ObjectTypes with path-based naming.

func InferStringType

func InferStringType(s string) *apiservercel.DeclType

inferStringType classifies a string value for type inference:

  • Pure literal (no ${...}): string
  • Standalone expression (${expr} is the entire string): dyn
  • Embedded expression (text around ${expr}): string (interpolation always produces string)

func StripDeferralLevel

func StripDeferralLevel(v any) any

stripDeferralLevel walks a value tree and strips one $ from every $${...} pattern in string values. Used to produce the child Graph's spec from the parent's template: $${expr} → ${expr}, $$${expr} → $${expr}, ${expr} → ${expr} (unchanged).

Types

type ChildScope

type ChildScope struct {
	NodeIDs     []string // child node IDs (from spec.nodes[].id)
	ForEachVars []string // child forEach variable names
}

ChildScope holds the identifiers visible in a child Graph's CEL environment.

func ExtractChildScopeFromBody

func ExtractChildScopeFromBody(body map[string]any) ChildScope

extractChildScopeFromBody extracts the child Graph's scope (node IDs + forEach variables) from a template body that produces a Graph CR. Returns an empty scope if the body is not a Graph CR or if the child's node list can't be determined statically.

type CompiledGraph

type CompiledGraph struct {
	Programs map[string]cel.Program // expression string → compiled program

	Topology       *dagpkg.Topology          // shared DAG structure (immutable after BuildDAG)
	UnresolvedGVKs []schema.GroupVersionKind // GVKs that fell back to dyn (triggers recompilation on CRD install)
	// collectionIDs captures the set of Watch node IDs in this spec.
	// Used by the dynamic-compile fallback to apply the same
	// `<wk_id>.ready()` AST rewrite that the eager-compile path does —
	// expressions that reach the dynamic path (cross-revision
	// finalization, forEach-finalizer synthesis, ad-hoc test evals)
	// must honor the same rewrite, otherwise empty-Watch `.ready()`
	// reverts to vacuously-true on that path.
	CollectionIDs map[string]bool

	// resourceSchemas maps node ID → resolved OpenAPI schema. Used at Eval
	// time to wrap scope entries via UnstructuredToVal so schema-typed
	// fields (e.g. Secret data values declared format:"byte") arrive in
	// CEL as their declared runtime types (types.Bytes, not types.String).
	// Mirrors upstream pkg/runtime/node_context.go buildContext.
	ResourceSchemas map[string]*spec.Schema

	// TimeComparisons maps expression strings to their time-solving metadata.
	// Only populated for expressions matching solvable patterns (e.g.,
	// `time.now() - timestamp(X) >= duration('2h')`). Used at eval time to
	// compute precise requeue durations when such expressions return false.
	TimeComparisons map[string]*TimeComparison
	// contains filtered or unexported fields
}

CompiledGraph holds the immutable compilation artifacts for a Graph spec. All fields are derived from the spec. The DAG topology, CEL programs, and CEL environment are all immutable after construction — cel.Program is thread-safe by the CEL spec, and BuildDAG produces a read-only topology. Per-instance concrete node bodies are assembled separately via assembleDAG.

func CompileGraphSpec

func CompileGraphSpec(spec *graph.GraphSpec, typeInfo *TypeSource) (*CompiledGraph, error)

CompileGraphSpec builds a typed CEL environment, eagerly compiles every expression, and builds the dependency graph. Returns a CompiledGraph ready for sharing across multiple instances.

The typeInfo parameter carries resolved types from ResolveNodeTypes. When nil, all nodes fall back to dyn.

func (*CompiledGraph) Env

func (c *CompiledGraph) Env() *cel.Env

Env returns the CEL environment for use in tests and downstream compilation.

func (*CompiledGraph) Eval

func (c *CompiledGraph) Eval(expr string, scope map[string]any) (any, error)

Eval evaluates a CEL expression against the given scope.

func (*CompiledGraph) SolveTimeComparison

func (c *CompiledGraph) SolveTimeComparison(expr string, scope map[string]any) time.Duration

SolveTimeComparison evaluates a time comparison's threshold against the current scope and returns how long until the comparison becomes true. Returns 0 if the threshold is already past or evaluation fails.

func (*CompiledGraph) WrapScope

func (c *CompiledGraph) WrapScope(scope map[string]any) map[string]any

wrapScope returns an activation map where scope entries for nodes with resolved OpenAPI schemas are wrapped via UnstructuredToVal. Without this, a Secret's data.key (declared format:"byte" in the OpenAPI spec) enters CEL as a raw base64 string, so string(secret.data.key) is an identity op rather than a decode. After wrapping, the runtime value conveys its declared type — schema-aware type conversion happens at field-access time.

Wrapping is shallow and per-call: the original scope map is never mutated, so hash inputs and serialization paths still see plain map[string]any values.

Entries without a schema (definitions, unresolved CRDs, forEach iterators) pass through unchanged — CEL's default type adapter handles them as before.

Mirrors upstream's pkg/runtime/node_context.go buildContext behavior.

type SchemaGeneration

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

SchemaGeneration tracks the global schema epoch for compilation staleness. Per 004-compilation.md § Type Cache: "The cache tracks a single global generation counter." Any schema change (CRD installed, updated, removed) advances it. Staleness is one integer comparison: current generation exceeds the artifact's recorded generation.

Thread-safe: the generation counter is atomic. No locking required for the hot-path staleness check (one Load per reconcile per graph).

func NewSchemaGeneration

func NewSchemaGeneration() *SchemaGeneration

NewSchemaGeneration creates a schema generation tracker.

func (*SchemaGeneration) AdvanceGeneration

func (sg *SchemaGeneration) AdvanceGeneration()

AdvanceGeneration increments the generation counter. Called when a schema change is detected (CRD install, update, or removal). All artifacts with a recorded generation less than the new value are stale.

func (*SchemaGeneration) Generation

func (sg *SchemaGeneration) Generation() int64

Generation returns the current schema generation. Artifacts compare their recorded generation against this to determine staleness.

type TimeComparison

type TimeComparison struct {
	// ThresholdProgram evaluates to the timestamp at which the comparison
	// becomes true. For `time.now() - E >= D`, this computes `E + D`.
	ThresholdProgram cel.Program
}

TimeComparison holds compile-time metadata for a solvable time expression. The threshold program evaluates to the timestamp at which the comparison flips.

type TypeSource

type TypeSource struct {
	// ResourceSchemas maps node ID → OpenAPI schema for nodes with resolved GVKs.
	// Does NOT include forEach nodes (those stay dyn in outer scope).
	ResourceSchemas map[string]*spec.Schema

	// DefinitionTypes maps node ID → inferred DeclType for definition nodes.
	DefinitionTypes map[string]*apiservercel.DeclType
	// ForEachDefinitions tracks definition nodes that have forEach (scope is list, not object).
	ForEachDefinitions map[string]bool
	// UntypedIDs are node/variable identifiers declared as dyn.
	UntypedIDs []string

	// unresolvedGVKs are GVKs that had literal apiVersion/kind but whose schema
	// could not be resolved (CRD not yet installed). Used by the CRD watch to
	// detect when recompilation is needed.
	UnresolvedGVKs []runtimeschema.GroupVersionKind

	// DynamicGVKNodes lists node IDs whose apiVersion or kind contains a CEL
	// expression. Per 004-compilation.md § Deferred Types: the type is
	// unknowable until runtime.
	DynamicGVKNodes []string
	// contains filtered or unexported fields
}

TypeSource holds all resolved type information for building the CEL environment. Populated during compilation phases 1 (schema resolution) and 2 (definition inference).

func ResolveNodeTypes

func ResolveNodeTypes(nodes []graph.Node, schemaResolver resolver.SchemaResolver) *TypeSource

ResolveNodeTypes resolves types for all nodes in the spec. It resolves OpenAPI schemas for resource nodes and infers types for definition nodes. The resolver may be nil — all resource nodes fall back to dyn.

func (*TypeSource) PrePopulateSchema

func (ts *TypeSource) PrePopulateSchema(nodeID string, gvk runtimeschema.GroupVersionKind, schemaResolver resolver.SchemaResolver)

PrePopulateSchema injects a resolved schema for a dynamic GVK node. Called by the compilation caller (not the compiler) when the node's GVK was resolved on a previous reconcile. The compiler sees the pre-populated schema and types the node like any static resource node.

Per 004-compilation.md § Deferred Types: "The caller resolves the schema for the recorded GVK and pre-populates the type source before calling the compiler."

Jump to

Keyboard shortcuts

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