Documentation
¶
Overview ¶
Package template implements the §7 mini-language: lexer + recursive-descent parser for the bounded expression grammar from the Phase 1 design spec §B, plus reference extraction over the resulting AST and a `{{ … }}` slot scanner for substitution-bearing host strings.
Evaluation (value substitution + bounded boolean evaluation) lands later — it needs state. This slice is state-free.
The grammar:
expr := or
or := and ( "||" and )*
and := unary ( "&&" unary )*
unary := "!" unary | comparison
comparison := primary ( ("=="|"!="|"<"|"<="|">"|">=") primary )? ; non-associative
primary := ref | literal | "(" expr ")"
ref := IDENT ( "." IDENT | "." INT )*
literal := STRING | NUMBER | "true" | "false" | "null"
No arithmetic, no function calls, no loops. Comparisons are non-associative (a<b<c invalid). Identifiers are [A-Za-z_][A-Za-z0-9_\-]* (hyphens allowed because step/binding ids may contain them in AWF, e.g. `cve-pipeline`).
Index ¶
- Constants
- func EvalBool(e Expr, scope Scope) (bool, error)
- func EvalBoolString(src string, scope Scope) (bool, error)
- func EvalTemplateValue(raw json.RawMessage, scope Scope) (any, error)
- func ParseArtifactRef(raw string) (id, name string, ok bool)
- func ParseAssetRef(raw string) (id string, ok bool)
- func ParseWorkflowInputFileRef(raw string) (name string, ok bool)
- func Substitute(host string, scope Scope) (string, error)
- func UnwrapEnvelope(src string) string
- type AndExpr
- type BoolLit
- type CmpExpr
- type EvalError
- type Expr
- type NotExpr
- type NullLit
- type NumberLit
- type OrExpr
- type Ref
- type Scope
- type Segment
- type Slot
- type StringLit
- type SyntaxError
- type Token
- type TokenKind
Constants ¶
const ( EvalCodeOversize = "AWF4001" // ref resolved to a value > MaxInlineBytes EvalCodeRefUnresolved = "AWF4002" // ref doesn't bind in the scope EvalCodeTypeMismatch = "AWF4003" // operator's operand types don't match (no coercion per §7) EvalCodeInvalidScalar = "AWF4004" // a {{ }} slot resolved to a non-scalar (map/slice/nil) EvalCodeSyntax = "AWF4005" // {{ }} slot or ref-grammar syntax error in the host template (slot scan / ParseRef failure; the ref never reached the scope) EvalCodeRefAbsent = "AWF4006" // ref is a step under a non-taken if branch — legitimately absent, distinct from AWF4002 missing EvalCodeDeferred = "AWF4099" // ref shape valid but resolution lands in a later slice (slice 2.4 for stdout, etc.) )
Evaluation error codes — the AWF4xxx namespace reserved by the Phase 2 design spec for runtime evaluation errors. These mirror the AWF1xxx / AWF2xxx / AWF3xxx string-code shape ir/diagnostic.go uses for VALIDATION diagnostics, but they are NOT registered in ir.catalog because they arise at runtime — the interpreter (slice 2.5) catches them and projects them into node.failed events and OTel attrs, not into `awf validate` output.
const MaxInlineBytes = 64 * 1024
MaxInlineBytes is the upper bound on a resolved ref's rendered/string size. Values exceeding this cap reject with AWF4001 at resolution — spec §7 / §4 directs authors to pass large data via output_files (CAS-stored, referenced by name) instead of inlining it into expressions or run: commands.
Phase 2 default: 64 KiB — matches ir.maxExpressionBytes for symmetry. The constant is exported so test fixtures and (eventually) a config layer can reason about it without parsing the package docs.
Variables ¶
This section is empty.
Functions ¶
func EvalBool ¶
EvalBool evaluates e against scope and returns its boolean value. The expr's top-level value MUST be a bool (no truthiness coercion); a non-bool top-level is AWF4003 (type mismatch). Short-circuiting: && stops on left-false; || on left-true.
func EvalBoolString ¶
EvalBoolString is the string-input convenience over EvalBool: unwrap the outer `{{ }}` envelope (if present), parse the inner as an Expr, then evaluate as bool. Used by engine.runIf and engine.runLoop for if.cond / loop.until — both fields' surface form is the `{{ <expr> }}` envelope per spec §5.1/§5.2, but the parser wants the bare expression. Returning the raw error from ParseExpr or EvalBool lets the caller route to its permanent_failure path with the same diagnostic codes (AWF4001/2/3/4/5).
func EvalTemplateValue ¶
func EvalTemplateValue(raw json.RawMessage, scope Scope) (any, error)
EvalTemplateValue decodes raw JSON, then evaluates template slots inside JSON string leaves. A string that is exactly one {{ ref }} slot resolves to the typed reference value; mixed strings use normal scalar substitution.
func ParseArtifactRef ¶
ParseArtifactRef parses a static artifact reference "step.<id>.files.<name>" into (id, name). Trims surrounding whitespace (parity with the ref-validation path, validate_refs.go's TrimSpace on slot inners). ok=false for any other shape (wrong root, wrong arity, index segments, or {{ }} which the lexer rejects). Shared by the IR validator and the engine resolver so the grammar lives in one place.
func ParseAssetRef ¶
ParseAssetRef parses a static asset reference "asset.<id>" into id. Trims surrounding whitespace (parity with ParseArtifactRef). ok=false for any other shape.
func ParseWorkflowInputFileRef ¶
ParseWorkflowInputFileRef parses a static workflow input file reference "input.files.<name>" into name. ok=false for any other shape.
func Substitute ¶
Substitute renders host by replacing every {{ ref }} slot with the typed value Scope.Resolve returns for that ref. The slot scanner is slice 1.3's Slots; ParseRef parses each slot's inner content (substitution targets are refs only per AWF §7, never full expressions). Returned errors are always *EvalError.
Host-level escape (AWF §7): `\{{` renders to a literal `{{` with the backslash consumed, and the escaped region is NOT treated as a slot (its inner text never reaches ParseRef / the scope). `\` is special ONLY immediately before `{{` — a `\` anywhere else copies through verbatim. `\\{{` therefore emits a literal `\` followed by a literal `{{` (the second `\` escapes, the first is ordinary text).
func UnwrapEnvelope ¶
UnwrapEnvelope strips a single `{{ ... }}` envelope (with optional surrounding whitespace) from src, returning the inner expression. If src does not start with `{{` and end with `}}`, it is returned unchanged.
Used by callers that parse a single-expression field (if.cond / loop.until / gate.until — spec §5.1 / §5.2 / §5.5) where the YAML surface wraps the expression in `{{ }}` for human readability but the parser wants the bare expression. This is intentionally narrow: it does NOT scan for multiple slots (use Slots for that — the field's surface contract is "exactly one expression").
Mirrors ir/validate_refs.go's previously-private unwrapEnvelope; promoting it here gives the runtime (engine.runIf, engine.runLoop) the same logic without crossing the ir → template layer.
Types ¶
type AndExpr ¶
type AndExpr struct {
L, R Expr
}
AndExpr models `L && R` (left-associative chain via nested AndExpr).
type CmpExpr ¶
CmpExpr models a single non-associative comparison `L <op> R`. Op is one of "==", "!=", "<", "<=", ">", ">=".
type EvalError ¶
EvalError is the typed runtime error returned by Substitute / EvalBool. The Msg field includes any positional context as free text (e.g. "slot scan at offset N"); a structured Pos field is deferred until Phase 6's OTel projection has a consumer for it.
type Expr ¶
type Expr interface {
// contains filtered or unexported methods
}
Expr is the AST sum type for parsed condition expressions. Concrete kinds: *OrExpr, *AndExpr, *NotExpr, *CmpExpr, *Ref, *StringLit, *NumberLit, *BoolLit, *NullLit. Sealed by exprMarker — linters (go-check-sumtype / exhaustive) enforce exhaustive switches in consumers.
type NotExpr ¶
type NotExpr struct {
X Expr
}
NotExpr models `!X` (right-associative via nesting: `!!a` → NotExpr{NotExpr{Ref a}}).
type OrExpr ¶
type OrExpr struct {
L, R Expr
}
OrExpr models `L || R` (left-associative chain via nested OrExpr).
type Ref ¶
type Ref struct {
Segments []Segment
}
Ref is a reference path: IDENT ( "." IDENT | "." INT )*. First segment is always an ident. Use Segments[0].Pos for "start of ref" diagnostics; per-segment Pos enables pointing at the specific failing segment (e.g. "step `triage` not declared" → Segments[1].Pos).
func ParseRef ¶
ParseRef parses src as a single reference (substitution slot contents). The input must be exactly one ref — no operators, no parens, no literals, and no trailing tokens.
func References ¶
References walks e and returns every Ref encountered, in left-to-right source order. Duplicates are preserved (the validator can dedupe by ref path if it wants — slice 1.4 will). The empty slice is returned for an Expr that contains no refs (a literal-only condition).
Used by validation to compute the set of references the workflow makes, feeding the "output_schema iff referenced" check (AWF3001/AWF3002) and the structural rule that bans references to undeclared steps / unbound names.
type Scope ¶
Scope is the value source the evaluator and substituter consume. The single- method shape keeps the template package engine-free: engine.Scope (slice 2.3 in engine/scope.go) implements it from *RunState + *ir.Workflow + a runtime context path. Test code can implement it from a flat map (see mapScope in eval_test.go) — the evaluator's mechanics are exercised in isolation that way.
The returned value is the typed reference value — one of: string, bool, float64, int, json.Number, map[string]any, []any, nil. The size check (AWF4001) is the evaluator's responsibility, NOT the Scope's — the Scope just returns the value; the evaluator applies MaxInlineBytes after.
type Segment ¶
type Segment struct {
Ident string // populated when IsIndex is false
Index int // populated when IsIndex is true (non-negative integer)
IsIndex bool
Pos int
}
Segment is one ref segment. Exactly one of {Ident set, IsIndex true} holds; the other is zero. Pos is the byte offset of this segment's token in the source — preserved so validation can point at the specific failing segment (e.g. `step.triage.field` → "triage" unknown at pos 5).
type Slot ¶
Slot is a `{{ … }}` region inside a host Template string. Start/End are byte offsets in the host source ([Start, End) covers the literal `{{` and `}}` markers as well as the inner text). Inner is the text between `{{` and `}}` (no trimming — the caller decides what counts as surrounding whitespace).
Positions reported by errors from ParseRef(slot.Inner) are slot-local. To translate a slot- local offset N to the host string, callers compute hostPos = slot.Start + 2 + N (the +2 is the byte length of the `{{` opener). Slice 1.4's validator MUST perform this translation before emitting a Diagnostic, otherwise reported positions are off by `slot.Start + 2`.
func Slots ¶
Slots scans src and returns every `{{ … }}` region in left-to-right order. Returns a *SyntaxError on an unterminated `{{` or on a `{{` that appears before a matching `}}` (no nesting). A stray `}}` outside any open slot is treated as literal text.
A `{{` immediately preceded by a single `\` is an ESCAPED opener (host-level escape, AWF §7): it does NOT open a slot, and the `}}` that would close it is inert literal text. Authors write `\{{` to embed a literal `{{` (e.g. an SSTI payload or prose teaching templating). The backslash is consumed at substitution time (see Substitute); here the escaped region is simply skipped. `\` is special only in this position.
Used by validation: for every ir.Template field (e.g. CodeStep.Run, Container.Image, idempotency_key), call Slots, then ParseRef each slot's Inner. Per AWF §7, substitution targets are references — never full expressions — so ParseRef (not ParseExpr) is the right consumer. Position translation: see the Slot doc comment.
type StringLit ¶
type StringLit struct{ Value string }
StringLit, NumberLit, BoolLit, NullLit cover the literal production. NumberLit holds the raw token text as a json.Number so the evaluator (Phase 2) can choose int/float decoding without loss; in this slice nothing decodes the value, it just round-trips the source.
type SyntaxError ¶
SyntaxError is the typed error returned by Lex / ParseExpr / ParseRef on any deviation from the §B grammar. Pos is the byte offset into the source; Msg is a short, position-free message. Callers extract Pos programmatically via errors.As(err, &se) — avoids the substring-match-on- the-rendered-message fragility we hit elsewhere.
func (*SyntaxError) Error ¶
func (e *SyntaxError) Error() string
type Token ¶
type Token struct {
Kind TokenKind
Text string // for TIdent: the identifier; for TNumber: the raw number literal (may include leading '-' and decimal point); for TString: decoded content
Pos int
}
Token is one lexed token. Pos is the byte offset into the source.