Grammar
The surface grammar of the .forage language. The Syntax reference explains each construct in prose; this is the formal version. The parser is recursive-descent, so precedence is the rule order shown, no separate precedence table.
Notation: := defines a production, | alternates, ? optional, * zero-or-more, + one-or-more, ( … ) groups. Quoted 'foo' and uppercase STRING/INT are terminals; lowercase names are non-terminals.
Lexical
STRING := "..." // may contain {…} interpolations
INT := -?[0-9]+
FLOAT := -?[0-9]+\.[0-9]+
BOOL := 'true' | 'false'
NULL := 'null'
DATE := YYYY-MM-DD
REGEX := /pattern/flags // flags ⊆ {i,m,s,u}
Ident := lowercase-starting identifier
TypeName := uppercase-starting identifier
Keyword := reserved word
DollarVar := '$' Ident // $input and $secret are reserved roots[*] (wildcard), ?. (optional chain), ← (bind), and → (case arm) are single tokens. // and /* … */ comments are stripped by the lexer.
Top-level
A file is a flat sequence of forms; order is not load-bearing.
forage_file := top_level_form*
top_level_form := recipe_header | type_decl | enum_decl | input_decl
| emits_decl | secret_decl | fn_decl | auth_block
| expect_block | statement | compose_block
recipe_header := 'recipe' STRING
emits_decl := 'emits' ( TypeName ( '|' TypeName )* )?Types, enums, inputs, secrets
type_decl := 'share'? 'type' TypeName ( 'aligns' alignment_uri )*
'{' field_list '}'
field_list := ( field ( ';' | ',' )? )*
field := field_name ':' field_type '?'? ( 'aligns' alignment_uri )?
field_type := 'String' | 'Int' | 'Double' | 'Bool'
| '[' field_type ']' | 'Ref' '<' TypeName '>' | TypeName
enum_decl := 'share'? 'enum' TypeName '{' ( variant ( ',' | ';' )? )* '}'
variant := Ident | TypeName
input_decl := 'input' field_name ':' field_type '?'?
secret_decl := 'secret' Ident
alignment_uri := alignment_path? '/' alignment_path? | alignment_path
alignment_path := alignment_segment ( '.' alignment_segment )*Type extension (extends @author/Name@v1) and the details of aligns are covered in Compose and Align.
Functions
fn_decl := 'share'? 'fn' Ident '(' param_list? ')' '{' fn_body '}'
param_list := DollarVar ( ',' DollarVar )*
fn_body := let_binding* extraction
let_binding := 'let' DollarVar '=' extraction ';'?A function body is zero or more let bindings followed by one trailing expression, its return value.
Statements
statement := step | visit | emit | for_loop
emit := 'emit' TypeName '{' ( emit_binding ( ';' | ',' )? )* '}' ( 'as' DollarVar )?
emit_binding := field_name '←' extraction
for_loop := 'for' DollarVar 'in' extraction '{' statement* '}'
step := 'step' field_name '{' step_field* '}'
step_field := 'method' STRING
| 'url' STRING
| 'headers' '{' ( STRING ':' STRING ( ',' | ';' )? )* '}'
| 'body' '.' ( 'json' | 'form' | 'raw' ) '{' body_contents '}'
| 'paginate' pagination_block
| 'extract' '.' 'regex' '{' regex_extract_body '}'
visit := 'visit' field_name '{' visit_field* '}'
visit_field := 'url' STRING
| paginate_action 'until' 'noProgressFor' '(' INT ')'
( 'maxIterations' INT )? ( 'iterationDelay' NUMBER )?
paginate_action := 'scroll' | 'click' STRINGurl, header values, raw bodies, and string-typed bindings are templates: any {…} segment is re-lexed as an extraction. step / paginate (author requests) and visit (drive a browser) are detailed in Engines & pagination and the Syntax reference.
Composition
A recipe body is either statements or a composition, never both.
compose_block := 'compose' recipe_ref ( '|' recipe_ref )+
recipe_ref := STRINGExpressions
One grammar for emit bindings, step values, template interpolations, fn bodies, and case arms. Listed low-to-high precedence.
extraction := pipe
pipe := additive ( '|' transform_call )*
additive := multiplicative ( ( '+' | '-' ) multiplicative )*
multiplicative := unary ( ( '*' | '/' | '%' ) unary )*
unary := '-' unary | postfix
postfix := primary ( '[' extraction ']' )* // on calls / struct literals
primary := 'case' path 'of' '{' case_arms '}'
| struct_literal | regex_literal
| '(' extraction ')' | call | path | literal
struct_literal := '{' ( field_name ':' extraction ( ',' | ';' )? )* '}'
call := Ident '(' arg_list? ')'
transform_call := ( Ident | Keyword ) ( '(' arg_list? ')' )?
arg_list := extraction ( ',' extraction )*
case_arms := ( case_label '→' extraction ( ',' | ';' )? )*
case_label := Ident | TypeName | Keyword | BOOL | NULL | INT | STRING
literal := STRING | INT | FLOAT | BOOL | NULL
path := path_head path_step*
path_head := '$' | '$input' | '$secret' '.' Ident | DollarVar
path_step := '.' path_field | '?.' path_field | '[' INT ']' | '[*]'
path_field := Ident | TypeName | Keyword| is lowest precedence, so $x * 28 | toString parses as ($x * 28) | toString.