Appendix A — Grammar
Appendix A. Grammar (normative)
Normative. This appendix contains the full formal grammar of the Cookfile language in the W3C-style EBNF variant defined in § 2.4. Where this appendix disagrees with the prose of any chapter of Part I, this appendix is authoritative (§ 2.5).
A.1. Top level
cookfile ::= toplevel_item*toplevel_item ::= recipe | chore | files_decl | tools_decl | probe_decl | config_block | use_declaration | import_declaration | register_block | top_level_module_call | comment | NEWLINE
register_block ::= "register" NEWLINE register_bodyregister_body ::= LUA_SOURCE (* see §{lexical.source-representation}, §{decl.register} *)top_level_module_call ::= module_call (* column-0; multi-line via brace-balance per §{lexical.brace-blocks.lua-spans} *)files_decl ::= "files" probe_name NEWLINE INDENT file_set_line+ DEDENTfile_set_line ::= glob_pattern+ NEWLINEtools_decl ::= "tools" probe_name NEWLINE INDENT tool_set_line+ DEDENTtool_set_line ::= TOOL_NAME+ NEWLINEA.2. Top-level declarations
use_declaration ::= "use" ( use_name | use_alias? use_path ) NEWLINEuse_name ::= LUA_IDENT | ( '"' LUA_IDENT '"' )use_alias ::= LUA_IDENT | ( '"' LUA_IDENT '"' )use_path ::= tree_path | ( '"' tree_path '"' )import_declaration ::= "import" BARE_IDENTIFIER import_path NEWLINEconfig_block ::= "config" name? NEWLINE config_bodyconfig_body ::= LUA_SOURCEWhich alternative a use argument selects (normative; CS-0206). A use_declaration carries either one argument or two. The last argument is a use_path if and only if it ends in .lua; otherwise it is a use_name. A conforming implementation MUST reject a two-argument use_declaration whose last argument is not a use_path, and MUST reject a use_name written in the two-argument form.
The discriminator is the .lua suffix rather than “does the argument fail to lex as a LUA_IDENT”, because the two productions are already disjoint under it — a use_name admits no . at all — and because the alternative would silently reclassify every name CS-0035 rejects. use my-mod continues to fail as a malformed name, rather than as a path that omitted its suffix.
use_name constraint (normative; CS-0035; amended CS-0206). The use_name MUST match the LUA_IDENT production (§ A.5), regardless of whether it is written bare or inside double quotes. A conforming implementation MUST reject any use_declaration whose name contains characters outside [A-Za-z0-9_], does not start with [A-Za-z_], or is empty — including names containing whitespace, hyphens (-), or dots (.). The diagnostic MUST identify the offending name and source line. The restriction follows from the use binding (§ 12.1): the name is bound at load time as a Lua local under the same spelling, and any non-identifier spelling would yield malformed generated Lua. Hyphens are rejected outright (rather than rewritten to underscores) to eliminate silent collisions between names like foo-bar and foo_bar. The same constraint and the same diagnostic apply to a use_alias, which is bound identically.
use_path constraint (normative; CS-0206). A use_path MUST be a tree_path: relative, forward-only, containing no .. segment, and not beginning with /. The sigil_path alternative that import_path admits is NOT admitted here, and a conforming implementation MUST reject a use_path beginning with //. A use_path MUST end in .lua, and MUST contain no whitespace — path_segment admits none, and the quoted form does not widen it. The production ends at NEWLINE: a conforming implementation MUST reject trailing text after a use_declaration’s arguments, including a # comment. Each rejection’s diagnostic MUST identify the offending text and source line.
use is therefore strictly more confined than import, which is the opposite of the two declarations’ usual relationship and is deliberate. § 12.2 states the rule and § C.11.0 states why: the boundary is not a stylistic preference but the boundary a portable cache key can reconstruct (§ 17.1.1.3).
Ordering (normative). use_declaration, import_declaration, and config_block MUST appear before the first recipe or chore. A conforming implementation MUST reject a Cookfile that places any of these after a recipe or chore.
Body termination (normative). A config_body’s extent is determined by the implicit-termination rule of § 4.3: it ranges from the line after the header to the next column-0 line classified as recipe, chore, config, use, import, or register, or to a column-0 line matching the top-level module_call shape, or to end-of-file. A bare end line is a Content token (§ 3.10) and does NOT terminate the body. A conforming implementation MUST NOT require an explicit end line.
Duplicate config_block rule. At most one unnamed config_block is permitted per Cookfile. Named config_block names MUST be unique. A conforming implementation MUST reject a Cookfile that violates either constraint.
Duplicate import_declaration rule. The BARE_IDENTIFIER (local alias) of each import_declaration MUST be unique within a Cookfile. A conforming implementation MUST reject duplicate import names.
Recipe / chore declaration names — no dots. The name of a recipe_header or chore_header MUST NOT contain . (dot). A conforming implementation MUST reject a top-level declaration recipe NAME or chore NAME whose NAME (whether bare or quoted) contains a dot, at parse time. Diagnostic: recipe name 'NAME': dotted recipe names are not permitted at the declaration site; use 'import alias path' for cross-Cookfile namespacing. Namespacing across Cookfiles is achieved exclusively via import_declaration (§ 11.2); a dotted reference appearing in a dependency_list or in a $<...> placeholder resolves through an import alias per § 11.3.
Duplicate declaration name rule. Recipes, chores and probe keys share a single declaration namespace within a Cookfile. The name of every recipe_header and chore_header, and the probe_name of every probe, files_decl and tools_decl, MUST be unique across the union of those declarations. A conforming implementation MUST reject, at parse time, a Cookfile that contains:
- two
recipe_headerdeclarations with the samename(recipe-vs-recipe collision); or - two
chore_headerdeclarations with the samename(chore-vs-chore collision); or - one
recipe_headerand onechore_headerwith the samename(recipe-vs-chore collision); or - a probe key equal to a
recipe_headerorchore_headername, in either declaration order (CS-0240); or - a probe key equal to an
import_declarationalias, in either declaration order (CS-0240).
Rules 4 and 5 exist because § 10.2 step 3 resolves a probe key by name: a name that is two kinds gives $<NAME> two readings and $<NAME.field> three, and rejecting it is what keeps § 10.2.1 at two heuristics rather than three. They do not extend to recipe-vs-alias, which stays legal. A colon-qualified key is outside both rules by construction, since a colon cannot appear in a bare declaration name; the QUOTED form is not, because names compare verbatim and probe "status" denotes the same key as probe status — recipe status beside probe "status" is a collision and MUST be rejected. Names compare verbatim — bare and quoted forms of the same name (e.g. recipe build and recipe "build") refer to the same callable and therefore collide. The diagnostic MUST identify the offending kind, the colliding name, and the source line of the prior declaration. The recommended diagnostic form is KIND 'NAME': duplicate declaration (already declared as PRIOR_KIND at line N), where KIND is the kind of the second declaration and PRIOR_KIND is the kind of the first. Cross-Cookfile namespacing for callables that would otherwise collide is achieved via import_declaration aliases per § 11.2.
Reserved recipe segment identifiers. The seven identifiers stem, name, ext, dir, in, out, env MUST NOT appear as the final dot-separated segment of a dependency_list entry or a $<...> placeholder, and env MUST NOT appear as the first dot-separated segment of either. This keeps the env. prefix reserved after CS-0172 replaced its env-var-lookup form with var.NAME (§ 10.7), so the retired spelling is rejected rather than resolved as a reference. The reservation does NOT constrain a recipe or chore declaration name or a chore parameter name (CS-0132): a declaration name is undotted (a dotted declaration name is rejected outright by the no-dots rule above), so recipe out, chore name, and recipe env are all well-formed. all is NOT among these reserved identifiers (CS-0130): recipe all and a dependency segment all are both well-formed.
A.3. Recipes
recipe ::= recipe_header NEWLINE recipe_bodyrecipe_header ::= "recipe" name (":" dependency_list)?dependency_list ::= name+recipe_body ::= recipe_item*recipe_item ::= gather_step | seal_step | cook_step | test_step | module_call | comment | NEWLINEAs of CS-0134 a recipe body is purely declarative: it is a flat sequence of recipe_items, all of which are register-phase (§ 8.1, § 8.3). The pre-CS-0134 two-region structure — a declarative_region followed by an optional imperative_region of lua_line / lua_block / shell_command steps — is removed, along with the register-phase inline_lua_line (>>) and inline_lua_block (>>{) items (those prefixes no longer exist anywhere in the language, § 3.7), and along with the @ interactive prefix and its interactive_command step kind, which CS-0134 removed from the language outright (§ 3.7). Register-phase Lua inside a recipe body is now written as a bare module_call (§ A.4, “Step-dispatch priority”, the module-call rule). The lua_line / lua_block (> / >{) forms survive only in body position — as the opener of a cook / test / probe step’s body — and never as a standalone recipe-body item; a shell_command is rejected as a recipe-body item and survives only inside a chore body (§ A.3.1, § 8.1).
At-most-one-gather rule. A recipe_body MUST contain at most one gather_step. A conforming implementation MUST reject a recipe with two or more gather_step items.
ingredients migration (CS-0229). The removed ingredients spelling has two replacements. When the command consumes each file, use gather "tests/*.txt" in the recipe body. When the command does not name the files, declare and seal a named determinant:
files core:src
"src/**/*.rs" "Cargo.toml"
recipe build
seal core:src
Body termination (normative). A recipe_body’s extent is determined by the implicit-termination rule of § 4.3: it ranges from the line after the header to the next column-0 line classified as recipe, chore, config, use, import, or register, or to a column-0 line matching the top-level module_call shape, or to end-of-file. A bare end line is a Content token (§ 3.10) and does NOT terminate the body. A conforming implementation MUST NOT require an explicit end line.
A.3.1. Chore
chore ::= chore_header NEWLINE chore_bodychore_header ::= "chore" name param_list (":" dependency_list)?param_list ::= positional_params variadic_param?positional_params ::= required_param* defaulted_param*required_param ::= BARE_IDENTIFIERdefaulted_param ::= BARE_IDENTIFIER "=" default_valuedefault_value ::= STRING | "(" LUA_EXPR ")"variadic_param ::= ("+" | "*") BARE_IDENTIFIERchore_body ::= chore_item*chore_item ::= lua_line | lua_block | shell_command | comment | NEWLINEAs of CS-0134 a chore_body is a flat sequence of execute-phase (imperative) step kinds — lua_line (>), lua_block (>{), and shell_command. The pre-CS-0134 two-region structure is collapsed: chores lose the register-phase inline_lua_line (>>) and inline_lua_block (>>{) items (those prefixes are removed from the language outright, § 3.7), and the @ interactive prefix / interactive_command step kind is likewise removed everywhere (§ 3.7) — a chore shell_command is interactive by default (§ 7.3), so no marker is needed. This leaves no declarative region and therefore nothing to order against. Chores keep the loose (implicitly-interactive) shell surface a recipe body no longer admits (§ 7.2).
Chore parameter ordering rule (normative). Within a param_list, every required_param MUST precede every defaulted_param, and the optional variadic_param MUST be the final parameter. A conforming implementation MUST reject a header that violates this order.
Chore parameter name rule (normative). Parameter names MUST NOT contain .. Duplicate parameter names within one header MUST be rejected at parse time. The reserved-segment rule above does NOT constrain parameter names (CS-0132): chore deploy name in out is well-formed. all is likewise unconstrained (CS-0130).
Variadic rules (normative). A variadic_param with sigil + (one-or-more) MUST be the final parameter; at invocation time, all remaining argv collect into a Lua sequence. If zero remain, a diagnostic MUST be raised (§7.1.3). A variadic_param with sigil * (zero-or-more) behaves identically except that zero remaining argv is valid and binds {}. At most one variadic_param is permitted per param_list; a variadic_param MUST NOT carry an =default.
Chore step-kind ban (normative). A chore_body MUST NOT contain gather_step, cook_step, or test_step. A conforming implementation MUST reject a chore body that contains any of these. The diagnostic MUST identify the offending step’s keyword. The ban is on the step kind, not on the keyword: a shell_command MAY begin with gather, cook or test, and § 7.2.1 gives the normative rule for classifying such a line (CS-0233).
Chore default-interactive (normative). Every shell_command inside a chore_body is interactive: it MUST be run with the controlling terminal inherited (§ 7.3). There is no @ prefix or interactive_command step kind — CS-0134 removed both from the language (§ 3.7).
Chore no-caching (normative). A unit registered from inside a chore body via cook.add_unit or the Cook Lua API’s unit-recording helpers MUST be registered with cache = false. A conforming implementation MUST reject a register-phase call that would set cache = true (or equivalent) from within a chore body.
A.3.2. Probe
probe_decl ::= "probe" probe_name (":" probe_dep_list)? NEWLINE INDENT probe_body DEDENTprobe_name ::= BARE_PROBE_KEY | STRINGBARE_PROBE_KEY ::= PROBE_SEG (":" PROBE_SEG)* /* CS-0201: no segment cap */probe_dep_list ::= probe_ref (WS probe_ref)*probe_ref ::= BARE_PROBE_KEY | STRINGIDENT ::= /[A-Za-z_][A-Za-z0-9_]*/PROBE_SEG ::= /[A-Za-z_][A-Za-z0-9_\-]*/ /* CS-0201: no `.` — see below */probe_body ::= seal_step? producer NEWLINEproducer ::= ("json" | "lines")? shell_block | exec_lua_blockTOOL_NAME ::= /[A-Za-z_][A-Za-z0-9_.\-]*/ /* CS-0201: its own production; keeps `.` */glob_pattern ::= "!"? STRINGseal_step is defined in § A.4; shell_block and exec_lua_block are also defined there (the body-dispatch tests >{ before {).
Probe body (normative). A probe_body is an optional seal_step followed by exactly one producer; the semantics, region rule, and rejection diagnostics are specified in § 22.5.2. The producer KIND leads — there is no produce introducer keyword. The json and lines kinds are valid ONLY when the producer is a shell_block; applied to an exec_lua_block (>{ … }) they MUST be rejected. The former files { … } and tools { … } producers are removed by CS-0222; envs { … } is removed by CS-0226. Each retired spelling MUST receive a diagnostic naming its replacement.
Named determinant sets (normative; CS-0222). files_decl and tools_decl are keyword-led, column-0 top-level declarations. Their indented bodies admit one or more physical lines and one or more entries per line; files_decl also admits the no-whitespace !"glob" exclusion marker. Each declaration desugars to a probe of the same name with, respectively, files-manifest or tool-identity semantics, so its name is admitted anywhere a probe_ref is admitted, including seal. A column-0 module call whose module segment is files or tools is shadowed by the declaration keyword and MUST NOT be recovered through backtracking.
Source-list kinds (normative). The tools declaration takes bare tool names
and the files declaration takes quoted glob patterns with optional ! exclude
prefixes (gather pattern syntax, § 8.2). A conforming
implementation MUST reject a declaration with no entries and a files
declaration with no include pattern, a tool entry that is not a TOOL_NAME, a glob entry that
is not a quoted STRING, and a body (in particular a >{ … }
exec_lua_block) supplied where a name or glob list is required
(§ 22.5.2). Both list forms admit one or more entries across one or
more indented physical lines.
Module-prefix colon disambiguation (normative). The EBNF above admits an apparent ambiguity: the : in BARE_PROBE_KEY and the : that introduces probe_dep_list are spelled identically. The rule is purely positional:
- A
:that is immediately preceded by anIDENTsegment and immediately followed (with no intervening whitespace) by anIDENT-start character is part ofBARE_PROBE_KEY— the module-prefix colon. Example:cc:zlibis a single probe name. - A
:that is preceded by a completeprobe_nameand followed by whitespace or end-of-line introducesprobe_dep_list. Example:probe cards: dep1 dep2has namecardsand deps[dep1, dep2]. - Each further contiguous
:IDENTsegment extendsBARE_PROBE_KEYunder the same rule:cc:find:raylibis a single three-segment key. There is no cap on segment count (CS-0201, superseding the two-segment cap and its rejection diagnostic). - The quoted
STRINGform (e.g.probe "cc:zlib") is always unambiguous and bypasses the positional rule entirely.
Probe-key segment charset (normative; CS-0201, superseding CS-0131). Each PROBE_SEG of a BARE_PROBE_KEY is [A-Za-z_][A-Za-z0-9_-]*: an alpha/underscore head, then alphanumerics, underscores and internal hyphens. The : is the sole segment separator, - never terminates a segment, and there is no cap on segment count — cc:find:raylib is a well-formed key. The STRING form is an exact-equivalent escape hatch for any non-empty spelling, including one the bare form cannot express: "odd+key" and probe "odd+key" denote the same key. A conforming implementation MUST accept both spellings at every site that names a probe key: the declaration, a probe_dep_list entry, seal, and a gather probe source.
. is NOT admitted. It is member access in a probe reference, so a dot inside a segment makes $<demo:cc-version.ver> ambiguous between “field ver of demo:cc-version” and “the key demo:cc-version.ver”, with nothing in the text to separate the readings. CS-0131 admitted . before probe references had member access; the two features are incompatible and member access is worth more. A conforming implementation MUST reject a dotted bare key with a diagnostic naming member access as the cause and offering the quoted form — “malformed key” alone reads as a typo rather than a rule, over a spelling an earlier version accepted.
Tool-name charset (normative; CS-0201, superseding CS-0181). TOOL_NAME is its own production and keeps the dot: an executable name can carry one (python3.11) and is never member-accessed, so the ambiguity above does not arise. CS-0181 defined TOOL_NAME ::= PROBE_SEG, which conflated “a segment of a key” with “the name of an executable”; only the first has the ambiguity and only the second needs the dot.
Missing probe name (normative). If the probe keyword is followed by content that does not parse as a valid probe_name (e.g. probe :deps), the implementation MUST reject the input with a diagnostic of the form: line N: expected a probe name after \probe“.
Unexpected token after probe name (normative). If the probe keyword is followed by a valid probe_name and then by content that is neither : (introducing probe_dep_list) nor end-of-line (e.g. probe foo bar), the implementation MUST reject the input with a diagnostic of the form: line N: probe 'NAME': unexpected token after the probe name; a dependency list is introduced with ':' (e.g. \probe NAME: dep1 dep2`)`. The diagnostic MUST NOT identify the error as a recipe error.
probe_dep_list element shape (normative). Each element of probe_dep_list is a probe_ref, which follows the same BARE_PROBE_KEY | STRING shape as probe_name. Module-prefixed dep references (e.g. cc:compiler) are therefore syntactically valid in the dependency list. A conforming implementation MUST accept the STRING form in dependency position (probe x: "cc:zlib"), not only bare BARE_PROBE_KEY — the quoted arm is the escape hatch for keys a bare segment cannot spell (CS-0131).
Column-zero constraint (normative). The probe keyword is recognised as a header only when it appears at column zero (the line does not begin with whitespace). An indented probe line is classified as Content per § 3.10.
A.4. Steps
gather_step ::= "gather" ( input+ | probe_ref STRING* | recipe_ref ) NEWLINE /* probe_ref spells a bare gather source: either a named files declaration, whose resolved manifest keys are path members, or an ordinary probe, whose value must be an array and whose elements are members. A non-array ordinary probe is rejected; a tools declaration is not iterable. Trailing STRING globs (CS-0197) declare per-member file inputs; see §22.5.10. recipe_ref spells a recipe gather source (CS-0239): the members are that recipe's declared output paths. It takes no trailing globs; see §8.2, Form 3. */recipe_ref ::= "$<" IDENT ( "." IDENT )* ">" /* the placeholder shape of §2.11 restricted to its bare-name form: no accessor, no bracket index, no probe colon. Resolution is §10.2's, so the name must denote a recipe; §10.2.2's reserved final segments cannot. */input ::= STRING /* include */ | "!" STRING /* exclude */
seal_step ::= "seal" seal_operand+ NEWLINEseal_operand ::= BARE_PROBE_KEY | STRING | "!" STRING /* determinant input stream; see §8.4.3. Also the optional first item of a probe_body (§A.3). Operands after the first MAY sit on continuation lines under the CS-0078 rule below (CS-0238). */cook_step ::= "cook" cook_output body cook_mods NEWLINEcook_output ::= STRING+ | "(" LUA_EXPR ")"exec_lua_block ::= ">{" LUA_BLOCK_CONTENT "}"shell_block ::= "{" SHELL_BLOCK_CONTENT "}"cook_mods ::= share_mod?share_mod ::= "local" | "pinned" | "nondet" /* bare names reference declarations; quoted strings are inline file globs (CS-0227) — see §8.4.3 */
test_step ::= "test" body NEWLINEbody ::= exec_lua_block | shell_block
lua_line ::= ">" LUA_LINE_CONTENT? NEWLINElua_block ::= ">{" LUA_BLOCK_CONTENT "}" NEWLINE /* CS-0134: the `>` / `>{` forms are admitted ONLY in body position — as the opener of a cook / test / probe step's body — or as a standalone step inside a chore body (§{grammar-appendix.chore}). They MUST NOT appear as a standalone recipe-body item (§{steps.dispatch}, §{steps.lua}). The doubled- `>` register-phase forms `inline_lua_line` (`>>`) and `inline_lua_block` (`>>{`) are removed from the language outright (§{lexical.line-prefixes}). */
/* CS-0134 removed the `interactive_command` production (`@` SHELL_LINE_CONTENT) and the `@` interactive prefix from the language entirely (§{lexical.line-prefixes}). A chore-body `shell_command` is interactive by default (§{grammar-appendix.chore}); no `@` marker exists. */
module_call ::= MODULE_CALL_TEXT NEWLINE? /* Used both as a top_level_module_call (§A.1) and, as of CS-0134, as a recipe-body item (register- phase; §{steps.dispatch}, rule 6) — reinstating the recipe-body dispatch CS-0072 had removed. MODULE_CALL_TEXT is any Content line whose first token matches /[A-Za-z_][A-Za-z0-9_]*/ (alphanumeric and underscore only; hyphens and dots are NOT permitted in the first segment), followed by a literal ".", followed by at least one character matching /[A-Za-z_]/ (ident-start). The remainder of the second segment is not validated at dispatch time; the full Lua-expression-hood of the line is the Lua runtime's concern. Braces may span subsequent lines (see § 2.9). */
shell_command ::= SHELL_LINE_CONTENT NEWLINE /* Any Content line not matching a preceding alternative. See § 3.8. */Multi-line patterns (CS-0078, extended to seal_step by CS-0238). In gather_step, cook_step, and seal_step, whitespace between successive STRING (or input / seal_operand) tokens MAY include line breaks. A conforming implementation MUST accept continuation lines that begin with " (or !" in the case of gather_step and seal_step) as part of the same declaration. Pattern collection terminates at the first non-quote token on the same physical line as a quoted pattern, which then resolves per the surrounding production (the body opener { / >{, which is mandatory for cook_step — CS-0133; end-of-declaration for gather_step and seal_step); a non-quote token on the same line as a quoted pattern that is neither a legal terminator nor a valid cook_mods token MUST produce a diagnostic naming the offending text. A continuation line whose first token after leading whitespace is not " (or !" for gather_step and seal_step) terminates the declaration silently — the declaration is treated as complete, and the line is dispatched per §A.4’s step-dispatch-priority order. The latter case is genuinely ambiguous between an intended-but-misspelled continuation and the start of a new step; the Standard chooses non-diagnosis to preserve the analogous “gather without trailing patterns” idiom. For cook_step, a continuation line that is not a quoted pattern still terminates pattern collection silently, after which the body-mandatory rule (CS-0133) applies: a cook_step left without a body opener on its final pattern line is rejected.
A single token MUST NOT span a physical line. The line break is a boundary between tokens, never a character within one: an implementation MUST resume collection only from a position outside a quoted STRING, and MUST reject a STRING left open at end of line. A " opening a continuation line therefore always opens a new pattern and never closes one carried over from the line above. This holds for all three step kinds, so no STRING value ever contains a line break — a guarantee anything that folds these tokens into a record or a cache key is entitled to rest on.
One notion of “inside a STRING” (normative). The test that decides whether a continuation line resumes the declaration and the tokenizer that later splits the collected text into items MUST agree on where a STRING ends. Where they disagree, the resume test can treat a quote as closed that the splitter treats as open, and the next line’s leading " then closes an item begun on the line above — reintroducing exactly the line-spanning token this rule forbids. An implementation therefore MUST NOT decide continuation with an independent scan; it MUST use the same string-boundary rule its item splitter uses, including that rule’s treatment of a backslash before a ". STRING is /"[^"]*"/ (§ A.5), under which a backslash is an ordinary character and the first " closes the item.
The continuation trigger is the leading quote, not the operand kind, so a continuation line MUST NOT begin with a seal_operand in BARE_PROBE_KEY form: a bare key in that position is indistinguishable from a new step or a module call, so it terminates the seal under the rule above and dispatches per §A.4. The restriction binds the first token of a continuation line and nothing else — once a line has been admitted by a leading " or !", the rest of it is read by the rules that apply on the keyword’s own line, and a bare key following a quoted operand there joins the same step. Because a bare key cannot open a line, stacked seal steps, which union (§ 8.4.3.1 rule 2), remain the spelling for a long list of bare refs. The rule applies at both seal_step positions: the recipe body and the optional leading seal of a probe_body.
Example A.4.1
Both forms are equivalent:
cook "a.o" "b.o" "c.o" { ... }cook "a.o" "b.o" "c.o" { ... }Similarly for gather:
gather "src/*.c" !"src/legacy.c"gather "src/*.c" !"src/legacy.c"Multi-output cook-step rule. When cook_step has two or more STRING output patterns before the body, the body resolves per the unrestricted production above — the bare-STRING arm having been removed in CS-0022, a trailing STRING simply joins the output-pattern list rather than being read as a body; the resulting body-less multi-output cook_step is then rejected because a body is mandatory (CS-0133).
Iteration coherence (CS-0022). All STRING output patterns of a single cook_step MUST share an iteration source: every output is literal (no accessor placeholder), or every output bears $<in.ACCESSOR> (own-input driver), or every output bears $<lib.ACCESSOR> for the same recipe lib (dep-driven driver). A conforming implementation MUST reject a cook_step whose output patterns mix sources, with a diagnostic naming the offending outputs. See § 8.4.1.
Test mode coherence (CS-0024, revised by CS-0130). A test_step Lua-block body MUST NOT reference both an iteration-item binding (input) and a batched-source binding (inputs) (free-identifier scan, ignoring strings/comments/long-strings per § 3.9). A conforming implementation MUST reject a Lua-block body that violates this rule with a diagnostic naming both tokens. A shell-block body has no batched-source placeholder — CS-0130 removed the shell-text all-inputs sigil — so a shell body either references the per-item accessor ($<in> / $<in.ACCESSOR>, one-to-one) or references no iteration item (a single unit); it cannot textually address the collected set (a Lua-block body MAY bind inputs to do so). A single-unit test is keyed on its source set (engine ManyToOne), on a non-empty recipe seal set when source-less (CS-0223), or runs uncached (engine OneShot) only when it has neither source nor seal. See § 8.6.1.
Test source presence (CS-0024). A one-to-one test_step (its body references the iteration item) MUST have a non-empty iteration source: the recipe MUST contain a preceding cook_step whose output list is non-empty, or a gather_step whose resolved path list is non-empty. A conforming implementation MUST reject a one-to-one test_step whose source resolves empty (a fan-out over nothing). A single-unit test_step whose declared source resolves empty, or which declares no source at all, is not rejected: a non-empty recipe seal set gives it the seal-only key of § 17.1.1.4; without a seal it runs uncached (the one-shot disposition).
>>{ is rejected as a body. The only valid body openers are { (shell block) and >{ (Lua block). As of CS-0134 the >>{ prefix no longer exists anywhere in the language — it is rejected unconditionally at the lexical layer (§ 3.7) — so cook "out" >>{ … } is rejected because >>{ is not a valid body opener. A conforming implementation MUST reject cook STR >>{ … }.
Body dispatch order (normative). After the output pattern(s), a conforming implementation checks in this order: >{ (Lua block), { (shell block); the first matching alternative wins. An implementation MAY additionally test for the now-removed >>{ prefix (CS-0134, rejected as above) purely to emit a sharper migration diagnostic, but >>{ is not an admitted body form. The bare-STRING form is no longer admitted (CS-0022).
using is rejected (CS-0099). The former body introducer using (cook STR { … } / cook STR >{ … }, removed in CS-0099) MUST be rejected at the body position with a migration diagnostic that names CS-0099 and states that the keyword was removed. The body opener follows the output pattern(s) directly. Conformance fixture cs0099-using-keyword-rejected pins the diagnostic.
Cook body is mandatory (CS-0133). A cook_step MUST have a body ({ … } or >{ … }). The former declaration-only form (a cook with output pattern(s) and no body) was removed: it registered no work unit and its documented “declaration now, build later via a following shell_command or preceding Lua” idiom never executed. A conforming implementation MUST reject a body-less cook_step with a diagnostic naming CS-0133. A tail that is neither a body opener nor a valid cook_mods token still produces the sharper cook_mods diagnostic (see “Multi-line patterns”). Conformance fixture cs0133-declaration-only-cook-rejected pins the rejection.
Cook-step disposition (normative). A recipe-level seal step requires at least one probe_ref, is additive and order-independent, and its set applies to every cacheable unit. cook_mods is the optional single share_mod slot (local, pinned, or nondet). Trailing seal and every unseal position are removed by CS-0225 and MUST receive migration diagnostics. The removed record keyword remains rejected with its nondet migration diagnostic.
Step-dispatch priority (normative). A single Content line inside a recipe body is dispatched in this order, with the first match winning. This cascade MUST agree with the prose cascade of § 8.1 (§8.1). As of CS-0134 the recipe body is purely declarative: every admitted line is a register-phase step, and a line matching none of the admitted forms is REJECTED — the pre-CS-0134 shell_command fall-through is gone (a shell_command survives only inside a chore body, § A.3.1), and the @ interactive prefix with its interactive_command step kind is removed from the language entirely (§ 3.7):
- Prefix
gather+ separator (space, tab, or end of token) →gather_step. - Prefix
seal+ separator → recipe-levelseal_step(its operands contribute to the recipe seal set; § 8.4.3). - Prefix
unseal+ separator → rejected as removed by CS-0225 (§ 8.4.3). - Prefix
cook+ separator →cook_step(its trailingcook_modsare parsed after the body — a cook always has one, CS-0133). - Prefix
test+ separator →test_step. BARE_IDENTIFIER "." BARE_IDENTIFIERimmediately followed by(— a module call — →module_call, register-phase Lua (§ 4.4).- Otherwise → REJECTED (§ 8.1). In particular a loose/anonymous
shell_command, a line beginning with@(the@prefix and its formerinteractive_commandstep kind are removed from the language, § 3.7), and a standalone>/>{line (outside body position) are all rejected here; the standalone>>/>>{forms are already rejected at the lexical layer because CS-0134 removes those prefixes from the language (§ 3.7). A line whose first token islocal,pinned, ornondetis likewise rejected — those keywords are recognised only in the trailingcook_modsof acook_step, not at a recipe-body line position.
Note: CS-0134 reinstates rule 6 (the module-call classification), reversing the CS-0072 removal of the recipe-body BARE_IDENTIFIER "." BARE_IDENTIFIER(…) dispatch. CS-0072 had removed that rule and forced authors to reach for the >> (inline_lua_line) / >>{ (inline_lua_block) prefix to get register-phase Lua inside a recipe body; CS-0134 both reinstates the bare module-call dispatch and removes >> / >>{ from the language entirely (§ 3.7). Top-level module_call dispatch (§ 4.4) is unaffected by this reinstatement.
A.5. Primitives
name ::= BARE_IDENTIFIER | STRINGBARE_IDENTIFIER ::= /[A-Za-z_][A-Za-z0-9_.\\-]*/LUA_IDENT ::= /[A-Za-z_][A-Za-z0-9_]*/STRING ::= /"[^"]*"/NUMBER ::= /[0-9]+/import_path ::= sigil_path | tree_pathsigil_path ::= "//" path_segmentstree_path ::= ( "./" | path_segment ) ( "/" path_segment )*path_segments ::= path_segment ( "/" path_segment )*path_segment ::= /[^\s\n\/]+/path ::= /[^\s\n]+/ /* legacy form; import_path is the canonical path */
**No '..' segment (normative).** A `path_segment` MUST NOT consist solely of two dot characters (`..`). The grammar's regex permits `..` syntactically; this rule eliminates it normatively. See §{comp.import}.
comment ::= /#[^\n]*/ NEWLINENEWLINE ::= /\n/LUA_LINE_CONTENT ::= /[^\n]*/ /* zero or more characters; may be empty */LUA_BLOCK_CONTENT ::= BRACE_BALANCED /* see § 2.9 */SHELL_LINE_CONTENT ::= /[^\n]+/SHELL_BLOCK_CONTENT ::= BRACE_BALANCED /* see § 2.9 */LUA_SOURCE ::= /* any sequence of lines up to the next column-0 top-level keyword or end-of-file; see §{toplevel.termination} */BRACE_BALANCED ::= /* text in which `{` and `}` counted outside of interior-language opaque spans balance to zero. For `LUA_BLOCK_CONTENT`: ignore braces inside Lua line comments (--), double-quoted strings, single- quoted strings, long strings ([==[...]==] of any =-level), and block comments (--[==[...]==] of any =-level). For `SHELL_BLOCK_CONTENT`: ignore braces inside single- and double-quoted strings, after a backslash-escape (\{, \}), and inside POSIX here- docs (<<TAG, <<-TAG, <<'TAG', <<"TAG"). Long- string, block-comment, and heredoc spans MAY span multiple source lines; the brace counter MUST carry their state across lines (CS-0035). See § 2.9. */MODULE_CALL_TEXT ::= /* see § 4.11 */BARE_IDENTIFIER and TOOL_NAME are one character class (informative). The two productions are spelled with identical regexes and are separate only because they are reached from different places — TOOL_NAME got its own name at CS-0201, when PROBE_SEG dropped the . and it did not. Nothing in this Standard permits them to diverge, and an implementation that lets them is one where a name a Cookfile declares is a name some other surface refuses.
The reference implementation reads the class from one place, cook_contracts::naming, at every site that needs it: the lexer that parses recipe NAME / chore NAME / config NAME, the top-level tools NAME body validator that enforces TOOL_NAME, and the CLI’s @PRESET argv recognition. It was written out at five sites until COOK-421, and the pair that mattered was the lexer’s and the CLI’s — a preset is declared in a Cookfile and selected on the command line, so a spelling one end admits and the other refuses is a preset the user can name and cannot use, in either direction.
Placeholder shape. Placeholders embedded in cook_step output patterns and SHELL_BLOCK_CONTENT bodies are not separately tokenised by the Cookfile grammar — they are raw character sequences inside a STRING or SHELL_BLOCK_CONTENT production. Their well-formedness is validated at codegen time per § 9.3. The single recognised form is the strict sigil $<IDENT>, normatively defined in § 9.1 (CS-0033). Its production is reproduced here for reference in the context of surrounding grammar productions:
sigil_placeholder ::= "$<" placeholder_ident ">"placeholder_ident ::= bare_ident | probe_sigil_ref | recipe_member | out_indexed | out_indexed_accbare_ident ::= ALPHA (ALPHA | DIGIT | "_" | ".")*probe_sigil_ref ::= BARE_PROBE_KEY probe_access_pathprobe_access_path ::= ( "." IDENT | "[" DIGIT+ "]" )*recipe_member ::= bare_ident "[" member_index "]"member_index ::= "in"out_indexed ::= "out_" DIGIT+out_indexed_acc ::= "out_" DIGIT+ "." ACCESSORALPHA ::= "a"…"z" | "A"…"Z" | "_"DIGIT ::= "0"…"9"The ACCESSOR production is defined above. The probe_sigil_ref alternative (CS-0240) is the probe-value reference of § 22.5.7: BARE_PROBE_KEY is the production of § A.3.2, so the sigil’s IDENT-continue set admits : and internal - beyond bare_ident’s. It is not distinguished from bare_ident by shape — a colon-free key is spelled exactly like a recipe name — so the alternatives are disambiguated by § 10.2, not by the lexer, and the one-name-one-kind rule of A.2 is what makes that disambiguation total. The recipe_member alternative (CS-0098, respelled [in] by CS-0137) is resolved per § 8.9; only the literal index in is valid in v1.0 — the bracketed-index position is reserved for 1.x keyed joins. The sigil_placeholder production can appear inside output_pattern, SHELL_BLOCK_CONTENT, SHELL_LINE_CONTENT, and a test_step’s shell_block body text. Resolution is defined by § 10.2 and § 9.3; an implementation MAY record byte ranges of placeholder positions in its parse tree to support migration tooling such as cook --migrate-sigil.
BARE_IDENTIFIER vs name. Recipe and dependency names can be either a BARE_IDENTIFIER (allowing dots and hyphens as internal characters) or a double-quoted STRING. The import declaration’s local alias is always a BARE_IDENTIFIER (no quoting).
LUA_IDENT vs BARE_IDENTIFIER (CS-0035). A LUA_IDENT is the strict Lua identifier shape /[A-Za-z_][A-Za-z0-9_]*/ — narrower than BARE_IDENTIFIER, which additionally admits internal dots and hyphens. The LUA_IDENT production is used wherever the matched text is bound as a Lua local at load time and therefore needs to round-trip through the Lua identifier grammar. Its sites are the use_declaration’s use_name and its use_alias (§ A.2) — the two spellings of the same thing, a name the generated Lua declares as a local.
lua_line MAY be empty. The > prefix alone (with no following content) is a valid lua_line producing an empty Lua expression.
Brace-balance algorithm. Both LUA_BLOCK_CONTENT and SHELL_BLOCK_CONTENT use a stateful brace-balance algorithm whose interior-language opaque-span rules differ. For Lua-block content, braces are ignored inside Lua line comments (-- to end of line), double-quoted strings, single-quoted strings, multi-line long strings ([==[...]==] of any =-level), and multi-line block comments (--[==[...]==] of any =-level). For shell-block content, braces are ignored inside single- and double-quoted strings (which span newlines, per CS-0154), after a backslash-escape (\{, \}), and inside POSIX heredocs (<<TAG, <<-TAG, <<'TAG', <<"TAG"). The brace counter carries multi-line opaque-span state (long strings, block comments, heredocs, shell quotes) across lines per CS-0035 / CS-0154. The block body is the character span between the opening brace and the matching close (CS-0154): the opening-line remainder and the closing line’s pre-brace prefix are body text, and non-whitespace text after the closing brace is a parse error. See § 3.9.
Shell block line normalisation. Each body segment of SHELL_BLOCK_CONTENT (the opening-line remainder, each full interior line, and the closing-line prefix — CS-0154) is trimmed of leading and trailing whitespace; segments blank after trimming are discarded. The result is a sequence of command strings, not a single raw string.