template

package
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Jul 7, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

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

View Source
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.

View Source
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

func EvalBool(e Expr, scope Scope) (bool, error)

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

func EvalBoolString(src string, scope Scope) (bool, error)

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

func ParseArtifactRef(raw string) (id, name string, ok bool)

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

func ParseAssetRef(raw string) (id string, ok bool)

ParseAssetRef parses a static asset reference "asset.<id>" into id. Trims surrounding whitespace (parity with ParseArtifactRef). ok=false for any other shape.

func ParseWorkflowInputFileRef

func ParseWorkflowInputFileRef(raw string) (name string, ok bool)

ParseWorkflowInputFileRef parses a static workflow input file reference "input.files.<name>" into name. ok=false for any other shape.

func Substitute

func Substitute(host string, scope Scope) (string, error)

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

func UnwrapEnvelope(src string) string

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 BoolLit

type BoolLit struct{ Value bool }

type CmpExpr

type CmpExpr struct {
	Op   string
	L, R Expr
}

CmpExpr models a single non-associative comparison `L <op> R`. Op is one of "==", "!=", "<", "<=", ">", ">=".

type EvalError

type EvalError struct {
	Code string
	Msg  string
}

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.

func EvalErrf

func EvalErrf(code, format string, args ...any) *EvalError

EvalErrf is a shorthand for &EvalError{Code: code, Msg: fmt.Sprintf(format, args...)}. Use it at construction sites where the format-args form is clearer than inlining fmt.Sprintf.

func (*EvalError) Error

func (e *EvalError) Error() string

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.

func ParseExpr

func ParseExpr(src string) (Expr, error)

ParseExpr parses src as a full §B expression. src is the inner content (callers strip any `{{ … }}` envelope themselves — see Slots). Returns a *SyntaxError on any deviation from the grammar; the input must be fully consumed.

type NotExpr

type NotExpr struct {
	X Expr
}

NotExpr models `!X` (right-associative via nesting: `!!a` → NotExpr{NotExpr{Ref a}}).

type NullLit

type NullLit struct{}

type NumberLit

type NumberLit struct{ Value json.Number }

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

func ParseRef(src string) (*Ref, error)

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

func References(e Expr) []Ref

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

type Scope interface {
	Resolve(ref *Ref) (any, error)
}

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

type Slot struct {
	Start, End int
	Inner      string
}

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

func Slots(src string) ([]Slot, error)

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

type SyntaxError struct {
	Pos int
	Msg string
}

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.

func Lex

func Lex(src string) ([]Token, error)

Lex tokenizes src per the §B grammar. Whitespace is skipped between tokens.

type TokenKind

type TokenKind int

TokenKind enumerates the token kinds produced by Lex. The exhaustive switch in the parser is enforced by go-check-sumtype / exhaustive linters (configured at the module level).

const (
	TEOF TokenKind = iota
	TIdent
	TNumber
	TString
	TDot
	TLParen
	TRParen
	TOr  // ||
	TAnd // &&
	TNot // !
	TEq  // ==
	TNeq // !=
	TLt  // <
	TLe  // <=
	TGt  // >
	TGe  // >=
)

func (TokenKind) String

func (k TokenKind) String() string

Jump to

Keyboard shortcuts

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