Skip to content

§ 6 — Recipes

6. Recipes

Normative. This chapter defines the shape of a recipe declaration: the header, the optional dependency list, the purely-declarative recipe body (CS-0134), and the at-most-one-gather rule. Step kinds are defined in § 8; chores are in § 7.

6.1. Recipe header

A recipe is the form recipe_header NEWLINE recipe_body (App. A.3). The recipe_header is the form "recipe" name (":" dependency_list)?, where name is either a BARE_IDENTIFIER or a double-quoted STRING (App. A.5, name). An optional : introduces a dependency list (§ 6.2).

The keyword recipe MUST be followed by a separator — whitespace, a tab, or a " character — before the name. An identifier whose first word merely begins with recipe (for example, recipes_cleanup) is a Content token, not a header (§ 3.10).

A recipe_header MUST NOT be introduced by a line whose trimmed content begins with recipe but is immediately followed by a non-separator character — for example, recipes_cleanup. Such a line is a Content token.

A recipe_body extends from the line following the header up to (but not including) the next terminating line of the surrounding Cookfile, or end-of-file (whichever comes first). See § 4.3 for the full terminator-set, which includes register and the top-level module_call shape as of CS-0072.

The reserved-segment rule of § A.2 applies: the final segment of a recipe’s name (the text following the last ., or the entire name if it contains no .) MUST NOT be one of the identifiers stem, name, ext, dir, in, out. A conforming implementation MUST reject a recipe whose final name segment matches any of these reserved identifiers (App. A.2). all is NOT among them (CS-0130): recipe all is well-formed.

The duplicate-name rule of § A.2 also applies: recipes and chores share a single callable namespace, so a recipe whose name matches that of an earlier recipe or chore declaration in the same Cookfile MUST be rejected at parse time. The diagnostic identifies both the offending declaration and the source line of the prior declaration. Cross-Cookfile composition that would otherwise collide is achieved via import aliases (§ 11.2), not by re-declaring under the same name.

The chore top-level form (§ 7) is a peer of recipe. A chore body has a more restrictive step-kind alternation than a recipe body — see § 7.2 for the differences.

Example 6.1.1

recipe build
    cook "main" { gcc -o main main.c }

recipe "bundle": backend.build frontend.build
    cook "bundle.out" { echo bundling > $<out> }

Two recipes, in two valid header shapes: explicit with a bare name (build), and explicit with a quoted name and dependency list mixing bare and quoted forms. Each body is a single declarative cook step — the recipe-body form CS-0134 requires; loose shell (gcc …, echo …) would be rejected at the top level of a recipe body (§ 8.1) and belongs inside a cook/test block body or a chore.

Example 6.1.2

recipe "build": "setup"
    cook "main" { gcc -o main main.c }

recipe build: setup
    cook "main" { gcc -o main main.c }

Both recipes above are semantically identical; the first uses a quoted name, the second a bare name.

Note 6.1.1

The token recipe is recognised as a top-level keyword only when followed by a separator (space, tab, or "). An identifier whose first word merely begins with recipe (for example, recipes_cleanup) is a Content token, not a header. See lexer test test_recipe_prefix_is_shell_command.

A recipe_header MUST NOT carry parameters. Parameters are a chore-only feature (§ 7.1.1). A conforming implementation MUST reject a recipe header containing any token between the name and the : (or end of header) that is not part of a dependency list. Recommended diagnostic form: recipe 'NAME': recipes don't take parameters; use a 'chore' (§7) or a config preset (§5).

6.2. Dependency list

A dependency list is a sequence of one or more names following the : in a recipe header (§ A.3, dependency_list). Each name in a dependency list is a name reference; its resolution rules are given in § 10.2 and its effect on execution ordering in § 10.6 and § 16.1.

The dependency_list is one or more names separated by whitespace; each element is a BARE_IDENTIFIER or a STRING, and the two forms MAY be freely mixed within a single list.

A conforming implementation MUST accept a mix of BARE_IDENTIFIER and STRING forms within a single dependency list. Dependencies MAY use the dotted form (e.g. backend.build) to refer to a recipe imported under an alias (§ 11); the dotted form is a single BARE_IDENTIFIER under the token rules of § 3.3, not two tokens separated by ..

The dependency list is purely syntactic at parse time. The Standard requires only that:

  1. Each referenced recipe exists in the resolved Cookfile (itself, its use modules, or its import aliases). A conforming implementation MUST reject a Cookfile that references a non-existent recipe — this diagnostic MAY be deferred to the first resolution pass (§ 13).
  2. The dependency graph is acyclic. A conforming implementation MUST reject a Cookfile whose dependency graph contains a cycle — this diagnostic MAY be deferred to DAG construction (§ 13).

Example 6.2.1

recipe tools: colorize wordcount reverse

recipe bundle: "backend.build" "frontend.build"

The first recipe lists three dependencies as bare identifiers. The second uses quoted strings for the same effect; it is semantically identical to recipe bundle: backend.build frontend.build.

6.3. Recipe body — purely declarative

As of CS-0134 a recipe_body is purely declarative: it admits only register-phase items — gather_step, cook_step, test_step, and bare module_call lines (auto-classified as register-phase, § 8.1) — together with structural comment and blank lines. A recipe body has no imperative region: standalone execute-phase steps (lua_line >, lua_block >{) and loose shell_command lines are NOT admitted at the top level of a recipe body; the @ interactive prefix and its interactive_command step kind, together with the register sigils >> / >>{, are removed from the language entirely and rejected everywhere (§ 3.10, § 3.7). A recipe’s execute-phase work is expressed exclusively inside the block body of a cook or test step (>{ … } / { … }, § 8.4, § 8.6).

Because every admitted recipe-body item is register-phase, its items MAY appear in any order. The two-region ordering rule of prior versions — which required declarative-region steps to precede imperative-region steps and forbade their interleaving — is REMOVED by CS-0134; a recipe body has no imperative region to order against, so the constraint no longer exists.

At-most-one-gather rule (normative). A recipe_body MUST contain at most one gather_step. A conforming implementation MUST reject a recipe body that contains two or more gather_step items; the diagnostic MUST identify the offending line and the line of the prior gather_step.

See App. C (§ C.6.10) for the rationale behind collapsing the register/execute split onto body position rather than intra-body ordering.