lang

package
v0.1.99 Latest Latest
Warning

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

Go to latest
Published: Aug 30, 2026 License: Apache-2.0 Imports: 6 Imported by: 0

Documentation

Overview

Package lang implements the lexer, parser, and typed AST for the .agent surface syntax fixed by ADR 002 (docs/adr/002-language-frontend-and-ir-expressiveness.md).

Scope is parsing (this package), resource lowering (#197, internal/lang/lower), type and effect checking (#198, internal/lang/check), and — added in #199 — conditionals, loops, and dynamic fan-out with the boolean expression language they require, lowered to the execution IR (internal/execir). Every AST node carries a spec.Pos so positions are compatible with the IR positions threaded by #187, and the parser recovers from errors to report multiple diagnostics per file rather than stopping at the first.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Parse

func Parse(file, src string) (*File, Diagnostics)

Parse lexes and parses src into a typed AST. It always returns a non-nil *File (possibly with a partial or empty Decls slice) plus every diagnostic found — lexical and syntactic — sorted by position. The parser recovers after each error (see the sync* helpers) so malformed input yields multiple positioned diagnostics rather than stopping at the first (issue #196).

Parsing does not resolve references, check types, or verify the effects clause; those are #198, and lowering to the resource model is #197.

func Print

func Print(f *File) string

Print reconstructs canonical .agent source from an AST. Output is normalized — 4-space indentation, single spaces around operators, comma-joined effects — so it does not depend on the incidental formatting of the input. Print is the engine behind `terfyn fmt`; parse -> Print -> parse -> Print is stable (idempotent) for any file that parses without error.

Types

type AgentDecl

type AgentDecl struct {
	Pos    Pos
	Name   *Ident
	Model  *ModelRef // model <provider>/<name>
	Policy *Ident    // policy <name> (reference to a Policy resource)
	Grants []*Grant  // grants { tool.<name>.<operation> ... }
	Input  *TypeRef  // input <Type>
	Output *TypeRef  // output <Type>
}

AgentDecl is `agent <Name> { ... }`. Each field appears at most once; a field the author omitted is a nil pointer (requiredness is a checking concern, #198) and a repeated field keeps the first occurrence and yields a duplicate-field diagnostic (the grammar admits each field once). Fields do not preserve source order across kinds — the surface fixes their meaning by keyword, not position.

func (*AgentDecl) Position

func (d *AgentDecl) Position() Pos

type Arg

type Arg struct {
	Pos   Pos
	Name  *Ident // nil => positional
	Value Expr
}

Arg is one call argument. Name is nil for a positional argument and set for a named one (repo: input.repo). A single call may mix positional and named arguments at the parse layer; validity is a checking concern (#198).

func (*Arg) Position

func (a *Arg) Position() Pos

type AssignStmt

type AssignStmt struct {
	Pos    Pos
	Target *Ident
	Value  Expr
}

AssignStmt is `<Target> = <Value>` binding a name to an expression result.

func (*AssignStmt) Position

func (s *AssignStmt) Position() Pos

type BinaryExpr

type BinaryExpr struct {
	Pos Pos
	Op  Kind
	X   Expr
	Y   Expr
}

BinaryExpr is `<X> <Op> <Y>`: a comparison (== != < <= > >=) or a logical connective (&& ||). Comparisons do not chain (a < b < c is a syntax error); logical connectives are left-associative with && binding tighter than ||.

func (*BinaryExpr) Position

func (e *BinaryExpr) Position() Pos

type CallExpr

type CallExpr struct {
	Pos    Pos
	Callee *RefExpr
	Args   []*Arg
}

CallExpr is `<Callee>(<args>)`. Callee is the dotted reference being invoked (a workflow-level tool call like github.get_pr, or an agent/subworkflow invocation like SecurityReviewer). Args may nest arbitrarily.

func (*CallExpr) Position

func (e *CallExpr) Position() Pos

type Decl

type Decl interface {
	Node
	// contains filtered or unexported methods
}

Decl is a top-level declaration: an AgentDecl or a WorkflowDecl.

type Diagnostic

type Diagnostic struct {
	Pos      Pos
	Msg      string
	Severity Severity
}

Diagnostic is one positioned parse, lowering, or checking problem. The parser recovers after each error (see parser.synchronize) so a single file yields every diagnostic it can find in one pass rather than stopping at the first.

func (Diagnostic) Error

func (d Diagnostic) Error() string

Error formats the diagnostic as "file:line:col: message" using the shared spec.Pos formatting; the location prefix is omitted when unknown. A warning is prefixed so it reads distinctly from an error in combined output.

type Diagnostics

type Diagnostics []Diagnostic

Diagnostics is an ordered collection of parse diagnostics.

func Format

func Format(file, src string) (string, Diagnostics)

Format parses src and returns canonical .agent source plus any diagnostics. When parsing reports errors the returned source is best-effort (formatted from the partial AST) and callers should surface the diagnostics rather than write the output.

func (Diagnostics) AsError

func (ds Diagnostics) AsError() error

AsError returns ds as an error, or nil when ds has no SeverityError entry — including when ds is a non-empty slice holding only warnings.

This is the ONLY safe way to convert a Diagnostics value to a plain error for a pass/fail check. Diagnostics is a slice type, so a bare interface conversion (`var err error = ds`), `len(ds) != 0`, or `fmt.Errorf("%w", ds)` all produce a non-nil error whenever ds is non-empty, warnings included — none of those consult Severity. Error() does not fix this either: it still renders every diagnostic (warnings included) for a caller that wants a full human-readable dump, and a warning-only Error() string is merely prefixed "warning:", not absent — the slice itself is still non-empty and would still test as a "failure" under any of the patterns above. Use AsError (or HasErrors directly) wherever "did this fail" matters.

func (Diagnostics) Error

func (ds Diagnostics) Error() string

Error joins every diagnostic on its own line. This renders ds for display — including warnings — and is NOT a pass/fail signal; see AsError.

func (Diagnostics) HasErrors

func (ds Diagnostics) HasErrors() bool

HasErrors reports whether ds contains at least one SeverityError diagnostic. A caller should treat a Diagnostics value with only warnings as non-fatal.

func (Diagnostics) Sorted

func (ds Diagnostics) Sorted() Diagnostics

Sorted returns the diagnostics ordered by position (file, then line, then column) so caller output is deterministic regardless of recovery order.

type EffectRef

type EffectRef struct {
	Pos  Pos
	Name string // dotted, e.g. "github.read"
}

EffectRef is one bare dotted effect identifier in the effects clause, such as github.read or external.visible. Unlike a Grant it carries no tool. prefix; the two namespaces must never be interchangeable (ADR 002).

func (*EffectRef) Position

func (e *EffectRef) Position() Pos

type Expr

type Expr interface {
	Node
	// contains filtered or unexported methods
}

Expr is a workflow expression: a CallExpr, a RefExpr, or — in a condition or call argument (#199) — a LitExpr, UnaryExpr, or BinaryExpr.

type ExprStmt

type ExprStmt struct {
	Pos Pos
	X   Expr
}

ExprStmt is a bare expression used for its effect, e.g. a deterministic tool call whose result is unbound: github.post_comment(...).

func (*ExprStmt) Position

func (s *ExprStmt) Position() Pos

type File

type File struct {
	Pos   Pos
	Decls []Decl
}

File is the root: an ordered list of top-level declarations.

func (*File) Position

func (f *File) Position() Pos

type ForStmt

type ForStmt struct {
	Pos      Pos
	Var      *Ident
	In       Expr
	Body     []Stmt
	Parallel bool
}

ForStmt is `for <Var> in <In> { <Body> }` (#199): iteration over a runtime collection. Parallel marks the dynamic fan-out form `parallel for <Var> in <In> { }` — ADR 002 §1 classifies dynamic fan-out over a runtime collection as "a loop wearing a graph costume," so it is language work, not a graph field. Both forms lower to the execution IR's Loop; only Parallel runs its iterations with bounded concurrency. Var binds inside Body only.

func (*ForStmt) Position

func (s *ForStmt) Position() Pos

type Grant

type Grant struct {
	Pos  Pos
	Name *Ident // the <Name> segment (tool.<Name>.<operation>); a single identifier
	// Operation is the operation path (the segments after the tool name), at
	// least one identifier and possibly dotted (pull_request.post_comment).
	Operation []*Ident
	// Segments is the full dotted path as written (including the leading
	// "tool"), preserved for diagnostics and round-tripping.
	Segments []*Ident
}

Grant is one autonomous capability bound inside a `grants { }` block. Per the ADR 002 amendment (#188) a grant names a concrete operation as tool.<Name>.<Operation> — the exact reference vocabulary of approvals.requiredFor / uses: — and lives in a namespace distinct from effects. It is split the same way as tools.ParseUses: the leading "tool" segment is the namespace marker, Name is the single tool-name segment, and Operation is everything after it. Operation is therefore a dotted path, not a single identifier: shipped strings such as tool.github.pull_request.get carry a multi-segment operation (pull_request.get), and a lowering pass (#197) reconstructs the uses string as tool.<Name>.<Operation joined by ".">. A grant that omits the tool. prefix is a diagnostic, not an EffectRef.

func (*Grant) OperationName

func (g *Grant) OperationName() string

OperationName returns the dotted operation path (e.g. "pull_request.get"), or "" if the grant is malformed. This is the <operation> half that ParseUses yields and that a uses string reconstructs after the tool name.

func (*Grant) Position

func (g *Grant) Position() Pos

func (*Grant) ToolName

func (g *Grant) ToolName() string

ToolName returns the granted tool's name, or "" if the grant is malformed.

type Ident

type Ident struct {
	Pos  Pos
	Name string
}

Ident is a bare identifier occurrence with its position.

func (*Ident) Position

func (i *Ident) Position() Pos

type IfStmt

type IfStmt struct {
	Pos  Pos
	Cond Expr
	Then []Stmt
	Else []Stmt
}

IfStmt is `if <Cond> { <Then> } (else ({ <Else> } | <IfStmt>))?` (#199). Cond is a boolean expression; Then and Else are statement lists. An `else if` chain parses as an Else holding a single nested IfStmt. Control flow never becomes a field on the resource-model WorkflowStep (ADR 002 §4): it lowers to the execution IR's Branch, and its two arms both flatten into the resource projection so the effect bound is the union over branches (ADR 002 §5).

func (*IfStmt) Position

func (s *IfStmt) Position() Pos

type Kind

type Kind int

Kind enumerates the lexical token classes of the .agent language.

const (
	// KindError is a lexer-level malformed token (e.g. a stray rune). The
	// offending text is carried in Token.Lit and a diagnostic is emitted.
	KindError Kind = iota
	// KindEOF marks the end of input. The lexer always terminates the stream
	// with exactly one KindEOF token.
	KindEOF

	// KindIdent is a bare identifier: [A-Za-z_][A-Za-z0-9_-]*. Hyphens are
	// permitted after the first rune so DNS-style resource references
	// (guarded-writes) and model name segments (gpt-5) are single tokens; the
	// language has no arithmetic, so '-' is never an operator.
	KindIdent

	// Structural keywords. These always begin a construct and never serve as a
	// name in the ADR 002 surface, so they are reserved. The field words
	// (model, policy, grants, input, output, effects) are NOT reserved because
	// they double as parameter names; the parser treats them contextually. The
	// loop keyword `in` (as in `for x in coll`) is likewise contextual — it is
	// lexed as an ordinary identifier and matched by the parser only in loop
	// position, so a parameter may still be named `in`. `true`/`false` are
	// contextual boolean literals recognized by the expression parser (#199),
	// not reserved words.
	KindAgent    // agent
	KindWorkflow // workflow
	KindParallel // parallel
	KindReturn   // return
	KindIf       // if
	KindElse     // else
	KindFor      // for

	// Punctuation.
	KindLBrace // {
	KindRBrace // }
	KindLParen // (
	KindRParen // )
	KindDot    // .
	KindSlash  // /
	KindComma  // ,
	KindColon  // :
	KindEquals // =
	KindArrow  // ->

	// Comparison, logical, and grouping operators for the expression language
	// that conditionals and loops require (#199). The surface has no arithmetic;
	// these appear only in `if` conditions and other boolean positions.
	KindEqEq   // ==
	KindBangEq // !=
	KindLt     // <
	KindLte    // <=
	KindGt     // >
	KindGte    // >=
	KindAndAnd // &&
	KindOrOr   // ||
	KindBang   // !

	// Literals (#199): string and number literals usable in conditions and as
	// call arguments. For KindString, Token.Lit holds the DECODED string value
	// (escapes already applied); for KindNumber, Token.Lit holds the raw source
	// text, which the parser converts to an int64 or float64.
	KindString // "..."
	KindNumber // 123, 1.5
)

func (Kind) String

func (k Kind) String() string

String renders a Kind for diagnostics and tests.

type Lexer

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

Lexer scans .agent source into a token stream. It is newline-insensitive: statement and field boundaries are recovered by the grammar (each construct has a deterministic shape), so newlines are ordinary whitespace and are not emitted as tokens. Line comments (// to end of line) are skipped.

func NewLexer

func NewLexer(file, src string) *Lexer

NewLexer returns a lexer over src. file is recorded in every token's Pos.

func (*Lexer) Diagnostics

func (l *Lexer) Diagnostics() Diagnostics

Diagnostics returns any lexical errors accumulated so far (stray runes).

func (*Lexer) Next

func (l *Lexer) Next() Token

Next returns the next token. After end of input it returns KindEOF repeatedly.

type LitExpr

type LitExpr struct {
	Pos   Pos
	Kind  Kind
	Value any
}

LitExpr is a literal operand: a string, a number, or a boolean (#199). Kind is one of KindString, KindNumber, or a boolean (recorded as KindIdent with a bool Value). Value holds the decoded Go value: string, int64, float64, or bool. Literals appear in conditions and as call arguments; the surface has no arithmetic, so numbers are only ever compared or passed, never combined.

func (*LitExpr) Position

func (e *LitExpr) Position() Pos

type ModelRef

type ModelRef struct {
	Pos      Pos
	Provider string
	Name     string
	Raw      string
}

ModelRef is a `<provider>/<name>` model reference such as openai/gpt-5. Provider and Name preserve hyphens (gpt-5); Raw is the reassembled text.

func (*ModelRef) Position

func (m *ModelRef) Position() Pos

type Node

type Node interface {
	Position() Pos
}

Node is any AST node. Position returns the node's start position.

type ParallelStmt

type ParallelStmt struct {
	Pos  Pos
	Body []*AssignStmt
}

ParallelStmt is `parallel { <AssignStmt>... }` — static fan-out into named branches with fan-in (ADR 002 graph structure; #192). Each branch binds a name, so the body admits only assignments.

func (*ParallelStmt) Position

func (s *ParallelStmt) Position() Pos

type Param

type Param struct {
	Pos  Pos
	Name *Ident
	Type *TypeRef
}

Param is one `<name>: <Type>` workflow parameter.

func (*Param) Position

func (p *Param) Position() Pos

type Pos

type Pos = spec.Pos

Pos is the source position carried by every token and AST node. It is a type alias for spec.Pos so .agent positions are the *same* type as the IR positions threaded through the resource model by #187 — a lowering pass (#197) can copy an AST node's Pos onto an IR node with no conversion.

type RefExpr

type RefExpr struct {
	Pos   Pos
	Parts []*Ident
}

RefExpr is a dotted reference path: a bare name (pr), a member access (input.repo, result.summary), or a callee path (github.get_pr). Parts holds each dotted segment in order and is always non-empty.

func (*RefExpr) Position

func (e *RefExpr) Position() Pos

type ReturnStmt

type ReturnStmt struct {
	Pos   Pos
	Value Expr
}

ReturnStmt is `return <Value>`.

func (*ReturnStmt) Position

func (s *ReturnStmt) Position() Pos

type Severity

type Severity int

Severity distinguishes a fatal diagnostic from an advisory one. The zero value is SeverityError so every pre-existing call site (lexer, parser, lowering) that constructs a Diagnostic{Pos, Msg} without setting Severity is unaffected — every diagnostic before #198 was an error.

const (
	SeverityError Severity = iota
	SeverityWarning
)

func (Severity) String

func (s Severity) String() string

type Stmt

type Stmt interface {
	Node
	// contains filtered or unexported methods
}

Stmt is a workflow body statement.

type Token

type Token struct {
	Kind Kind
	// Lit is the source text of the token. For punctuation it is the literal
	// symbol; for KindIdent and keywords it is the matched identifier; for
	// KindEOF it is empty.
	Lit string
	// Pos is the 1-based start position of the token (spec.Pos, #187).
	Pos Pos
}

Token is one lexeme with its class, source text, and position.

func (Token) String

func (t Token) String() string

String renders a token for test output and diagnostics.

type TypeRef

type TypeRef struct {
	Pos  Pos
	Name string
}

TypeRef names a schema/type by identifier (e.g. PullRequest, Review). It is an unresolved reference at the parse layer.

func (*TypeRef) Position

func (t *TypeRef) Position() Pos

type UnaryExpr

type UnaryExpr struct {
	Pos Pos
	Op  Kind // KindBang
	X   Expr
}

UnaryExpr is `<Op> <X>` — only `!` (logical negation) exists in the surface.

func (*UnaryExpr) Position

func (e *UnaryExpr) Position() Pos

type WorkflowDecl

type WorkflowDecl struct {
	Pos     Pos
	Name    *Ident
	Params  []*Param
	Result  *TypeRef     // return type after ->; nil if omitted
	Effects []*EffectRef // effects { github.read, ... }; nil if no clause
	Body    []Stmt
}

WorkflowDecl is `workflow <Name>(<params>) -> <Result> effects { ... } { body }`. Result and the effects clause are optional in the grammar; the body is a statement list that may include conditionals and loops (IfStmt, ForStmt; #199) in addition to assignments, calls, parallel blocks, and a return.

func (*WorkflowDecl) Position

func (d *WorkflowDecl) Position() Pos

Directories

Path Synopsis
Package check implements the ADR 002 §5 "checked program": the pass that sits between the #196 typed AST and the two sibling projections (the #197 resource projection and the #199 execution lowering, internal/execir).
Package check implements the ADR 002 §5 "checked program": the pass that sits between the #196 typed AST and the two sibling projections (the #197 resource projection and the #199 execution lowering, internal/execir).
Package lower turns the #196 typed .agent AST into the existing resource model — the resource projection of ADR 002 §5.
Package lower turns the #196 typed .agent AST into the existing resource model — the resource projection of ADR 002 §5.

Jump to

Keyboard shortcuts

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