Skip to content

§ 10 — Cross-recipe references

10. Cross-recipe references

Normative. This chapter defines name references: the mechanism by which one recipe refers to another. It specifies in full the three syntactic forms — dep-list references (recipe A: B), bare placeholders ($<B>), and accessor placeholders ($<B.stem>) — giving their resolution, expansion, and effect on the dependency graph. A fourth surface, the register-phase cook.require_recipe call (§ 22.9), is a name reference too and carries the same dependency implications (§ 10.6), but it is a Lua API rather than surface syntax: it is enumerated in § 10.1 and specified at § 22.9, not here. The formal grammar of the placeholders is given in App. A; this chapter specifies their meaning.

10.1. Name references

A name reference is the sole mechanism by which a recipe refers to another recipe. A name reference MAY appear in one of four surfaces:

SurfaceLocationForm
Dep-listRecipe header : … (§ 6.2)NAME
Bare placeholderAny step template string$<NAME>
Accessor placeholdercook step output pattern (§ 8.4, § 8.5)$<NAME.ACCESSOR>
Register-phase callInside a recipe body, register phase (§ 22.9)cook.require_recipe("NAME")

All four surfaces share the dependency implications of § 10.6: each one names its referent, which is what makes it a name reference and what establishes the cross-recipe edge. § 16.1 enumerates the same four.

The first three are surface syntax and additionally share the resolution rules of § 10.2. Their expansion behaviour differs by surface: dep-list references have no template expansion; bare placeholders expand per § 10.5; accessor placeholders drive iteration per § 10.4.

The fourth is not surface syntax but a register-phase Lua API, so this chapter enumerates it without specifying it: it has no grammar in App. A and no template expansion, it resolves its argument against the bare declared names of the current registration pass rather than through § 10.2‘s placeholder cascade, and its resolution, ordering guarantee, and error contract are given in § 22.9. It is listed here because the category this section defines — name references, and nothing else — is what § 10.6 draws its edges from, and a list of surfaces that could not express this one would put the two sections in contradiction.

10.2. Resolution

A conforming implementation MUST resolve a placeholder $<TOKEN> (defined in § 9.1) by applying these steps in order; first match wins:

  1. Builtin. If TOKEN matches one of the builtin shapes from § 9.3, the placeholder substitutes per the builtin’s row. Builtin validity is also checked against the step’s iteration mode and declared output count; mismatches MUST produce a load-time diagnostic. The number N in out_N MUST be in the range 1..=K where K is the step’s declared output count; out_0 and out_N for N > K MUST produce a load-time diagnostic.

  2. Recipe in scope. If TOKEN is the name of a recipe reachable from the current Cookfile (own recipes, recipes imported via use, or recipes reachable through sigil-imported Cookfiles per §7), the placeholder substitutes to that recipe’s terminal output(s) per § 10.5. If TOKEN has the shape RECIPE.ACCESSOR where ACCESSOR ∈ {stem, name, ext, dir}, the accessor applies to the recipe’s single output via § 10.3. Multi-output recipes accessed via accessor MUST produce a load-time diagnostic. This exact in-scope lookup includes colon-qualified recipe names: $<dotnet:build> names dotnet:build before the colon fallback in step 3. The set of recipes in scope is the full register-phase recipe set: it includes recipes registered during the register phase by a top-level module_call (a target maker such as cook_cc.bin("x"), § 4.4), not only recipes declared with a native recipe block. Because target makers register their recipes during the register phase, a conforming implementation MAY determine recipe-in-scope membership for this step at register time — after register-phase recipe registration completes — on the same footing as the register-time allowance in step 4; an implementation MUST NOT restrict this step to the statically parsed recipe blocks, which would mis-resolve $<x> to step 4 (or step 5) for a module-registered recipe x.

  3. Probe key (CS-0240). If TOKEN, or the part of TOKEN preceding its access path (§ 22.5.7), names a probe key declared in the current Cookfile — a native probe declaration, or a files or tools declaration (§ 22.5.2.1) — the placeholder is a probe-value reference and substitutes per § 22.5.7. A TOKEN containing : that did not exactly match an in-scope recipe in step 2 is a probe reference and needs no probe-key lookup: no builtin or declared-variable name can contain a colon, so the fallback form is self-identifying wherever a probe keyset is not at hand. A probe reference resolves identically in every position, but a position whose lowering is evaluated at register time MUST refuse rather than substitute it, per § 10.2.4.

    This lookup set is deliberately narrower than step 2’s, and the difference is not an oversight. Step 2 forbids restricting the recipe set to statically parsed recipe blocks, because a target maker registers real recipes and $<x> has to reach them. Step 3 states the opposite for probe keys, over a different set, for a reason step 2 does not face: the keys admitted here are exactly the ones a Cookfile declares in its own text, fixed before any module runs. A register-phase cook.probe registration (§ 22.5.3) is therefore NOT admitted by name, because § 22.5.6 leaves cook.probe and cook.add_unit call order unconstrained — a name-based lookup over that set could resolve one way or the other depending on which module happened to load first, and the same $<x> would mean a probe in one build and a variable in the next. When no exact in-scope recipe matches, the colon-qualified form (§ 22.5.7) is how a module-registered key is referenced by sigil, and it needs no probe-key lookup, so nothing a module registers becomes unreachable; it is reached by the spelling every module already uses. An implementation MUST NOT resolve a colon-free probe reference against a set whose contents depend on module evaluation order.

  4. Declared variable. If TOKEN (or var.TOKEN per the explicit form defined in § 10.7) is exported as var.TOKEN = ... in a config block reachable from this Cookfile’s config-block scope (§ 5.3), the placeholder substitutes to that value’s string form (§ 5.3). The implementation MUST capture the consulted name in the unit’s consulted_env_keys for cache invalidation. The “declared” check MAY be implemented at register time (i.e., after config-block evaluation captures the keyset). A declared value is never placed in a step’s process environment (§ 5.3), so a step that consults it always keys on its resolved value.

  5. Hard error. A conforming implementation MUST emit a load-time diagnostic for any placeholder that fails to resolve under steps 1–4. The diagnostic MUST name the unresolved TOKEN and SHOULD enumerate the recipes, probe keys, and declared variables in scope. Lexically valid IDENT shapes that no resolution rule handles (e.g., $<foo.> with trailing dot, $<A.B.C> with multiple unrecognized dotted segments) fall under this step.

This is a closed function: every well-lexed placeholder either resolves to a known thing or fails loudly. There is no silent fallthrough — $<UNKNOWN> against an undeclared variable MUST NOT produce an empty-string substitution.

One name, one kind (normative; CS-0240). For steps 2 and 3 the order is not a precedence rule to be relied on, because a name reaches at most one of them. A Cookfile in which one name is declared both as a probe key and as a recipe or chore name, or both as a probe key and as an import alias, MUST be rejected at load time with a diagnostic naming both declaration sites (App. A.2). Without that rejection the cascade would silently pick a winner, and § 10.2.1 would need a third heuristic to decide what $<A.B> addresses; with it, the base name’s kind decides, and the decision is total.

A declared variable is the one member of the cascade this rule cannot reach: it is produced by evaluating a config block, so it has no declaration site the parse phase can name, and a probe-vs-variable collision is therefore governed by § 10.2.3 on the same terms as the recipe-vs-variable collision that has always been permitted there. Step 3 preceding step 4 is load-bearing in that case, and $<var.NAME> (§ 10.7) remains the explicit spelling that reaches the variable.

10.2.4. Position independence

Resolution is a function of the token and the enclosing step’s scope, iteration mode, and declared output count — not of which shell-text position the token occupies. A conforming implementation MUST resolve the same TOKEN, under the same scope and step context, to the same step of § 10.2 whether it appears in a cook output pattern, a cook body, a test body, or a chore body, and MUST report the same diagnostic where resolution fails. (CS-0184.)

Positions differ in which resolutions they admit, never in what a token means. A test body admits no $<out…> form (§ 9.4); that is a refusal of an otherwise-resolved token, stated where the position is specified, and it MUST name the position as the reason. A position MUST NOT decline to substitute a token it does not recognise: an output pattern carrying no iteration driver is not thereby literal text, and emitting it unsubstituted places an unresolved $<…> in a declared output path, where the shell reads it as syntax and the value it named never enters the unit’s key.

A position that cannot honour a resolution MUST refuse it. It MUST NOT substitute a value the phase cannot yet have: a probe-value reference (§ 22.5.7) reaching a position whose lowering is evaluated at register time MUST NOT be read from the per-run probe-value store there, because a probe that the register phase has not materialised answers with no value rather than an error.

A name reference appearing in a dep-list is resolved against the recipe names in scope with no accessor split: the whole string is the recipe name. Qualified cross-Cookfile dep-list references (alias.recipe) follow the resolution rule of § 11.

Independence binds analyses as well as positions (CS-0210). An implementation that examines a token for some purpose other than substituting it — deriving the dependency edges of § 10.6 is the case that matters, since those edges decide execution order — MUST reach the same answer as § 10.2 about what that token names. A narrower recognition is non-conforming even where the substitution is correct, because the observable defect is not a wrong string but an unordered build.

10.2.1. Dotted name references

Recipe and chore names declared in a Cookfile MUST NOT contain . (§ 3.3, App. A.2). A dotted name reference therefore never names a recipe defined in the current Cookfile; it is always one of:

  • a qualified cross-Cookfile reference of the form alias.recipe, resolved against the importing Cookfile’s import aliases per § 11.3; or
  • a path-accessor placeholder of the form RECIPE.ACCESSOR (where ACCESSOR ∈ {stem, name, ext, dir}), per step (2) of § 10.2; or
  • a probe access path of the form KEY.field (§ 22.5.7), per step (3) of § 10.2.

A conforming implementation resolves a dotted $<TOKEN> by first attempting the path-accessor split (the rightmost dot separates RECIPE from ACCESSOR); if ACCESSOR is one of the four path-accessor names and RECIPE resolves to a recipe in scope, the placeholder is an accessor reference. Otherwise the leftmost dot separates the base name from the rest, and the base name’s kind selects the reading: a probe key gives a probe access path, an import alias gives a qualified reference per § 11.3. A token that satisfies none of these shapes MUST produce a load-time diagnostic per step (5) of § 10.2.

Because no dotted name can resolve to a recipe defined in the current Cookfile, there is no whole-token-vs-split ambiguity to adjudicate. Nor is the third reading a third heuristic: § 10.2‘s one-name-one-kind rule rejects a name that is both a probe key and an import alias, so at most one of the two base-name readings is available for any given token (CS-0240).

10.2.2. Reserved final segment

A recipe’s final dot-separated segment MUST NOT equal a path accessor (§ 10.3) or in or out. A conforming implementation MUST reject a Cookfile that declares such a name, at parse time. The rule is enforced by the check_reserved_recipe_name production in App. A. all is NOT among the reserved segments (CS-0130).

10.2.3. Environment-variable shadowing

When step (2), (3) or (4) succeeds, step (5) is NOT consulted. If a variable named TOKEN is also declared, the earlier step wins: a recipe name wins under step (2), a probe key wins under step (3) (CS-0240). A conforming implementation MUST emit a warning naming both. The warning is the remedy here rather than the hard error of § 10.2‘s one-name-one-kind rule because a declared variable has no parse-time declaration site to name, and because $<var.NAME> (§ 10.7) is already the spelling that reaches the variable regardless of what shadows it.

10.3. Path accessors

A path accessor is one of the four names stem, name, ext, dir. For a path p, the accessors are defined by the path.* helpers of § 26.1:

AccessorEquivalent
stempath.stem(p) (§ 26.2)
namepath.name(p) (§ 26.3)
extpath.ext(p) (§ 26.4)
dirpath.dir(p) (§ 26.5)

The set of path accessors is closed: an accessor not in this list is not a path accessor for the purposes of § 10.2 or § 9.3. Extending the set is a change to this Standard, not a library change. See rationale B.5.P.

10.4. Dep-driven iteration

Rule. When a cook step’s output pattern list contains a $<NAME.ACCESSOR> accessor placeholder naming recipe lib, the step’s iteration driver is lib’s output list, not this recipe’s own input list.

  • Iteration shape. The step is a one-to-one step (§ 8.4). For each item x in lib’s output list (§ 10.4.1), one work unit is registered. Within that unit, the iteration item is x, and the dotted-accessor placeholders $<in.ACCESSOR> refer to x. A placeholder $<lib.ACCESSOR> in the same step refers to the same accessor of x.
  • One driver per step. A step’s output pattern list MAY contain accessor placeholders referring to AT MOST ONE distinct recipe. Multiple occurrences referring to the same lib (possibly with different accessors) are permitted and refer to the same driver. Accessor placeholders naming two or more distinct recipes in the same step MUST be rejected with a diagnostic.
  • Bare $<lib> in an output pattern. A conforming implementation MUST reject a cook step whose output pattern list contains a bare $<lib> (no accessor) naming a recipe. $<lib> has no iteration semantics.
  • Accessor placeholder in a cook-step body or test body. A $<lib.ACCESSOR> appearing in a cook shell_block body, a test body (shell or Lua), or a bare shell_command MUST be rejected with a diagnostic. Test steps have no output pattern and therefore can never declare a lib-driven driver; the rejection is universal in those bodies.
  • A gather $<lib> source is the other spelling (CS-0239). This section’s driver is declared by an accessor in an output pattern, which is why it is available to a cook step and to nothing else. § 8.2‘s Form 3 declares the same driver on the recipe, from a gather line, so it reaches a test step too — a step that has no output pattern and therefore could never name a driver here. The two iterate the same list, lib’s output list of § 10.4.1, and establish the same edge. They differ in where the iteration is declared and consequently in what may consume it.
  • String-substitution references coexist. A step MAY contain any number of {libX} bare references (§ 10.5) to other recipes, orthogonally to whichever recipe (if any) drives iteration. Those references do not participate in the “one driver” rule.

10.4.1. A recipe’s output

A recipe’s output is the output list of its last step, under the following rule applied per step kind:

Step kindOutput list
gatherPassthrough: the resolved input paths (§ 8.2)
cook (one-to-one)The per-input outputs recorded at register time
cook (many-to-one)The single output path
testPassthrough: the step’s input list
Bare shell (@ or @!)Passthrough: the step’s input list

“Passthrough” means the step’s output equals its input. A recipe with no steps has the empty list as its output.

A recipe assembled from register-phase API work units (a recipe body that registers units via cook.add_unit rather than native cook steps — the shape a module target maker such as cook_cc.bin produces) has, as its output, the output list of its last registered unit, by the same “last step” rule. A conforming implementation MUST record that terminal output for cross-recipe resolution (§ 10.2 step 2, § 10.5); failing to do so makes $<recipe> substitute the empty string for module-registered recipes.

10.4.2. Example

recipe protos
    gather "proto/*.proto"

recipe compile_protos
    cook "build/obj/$<protos.stem>.pb.cc" { protoc --cpp_out=build/obj $<in> }
    cook "build/lib/libprotos.a" { ar rcs $<out> $<in> }

The compile_protos recipe’s first step is dep-driven by protos; for each x in protos’s output list (the resolved globs), one work unit is registered with $<in> = x and $<out> = build/obj/STEM(x).pb.cc. The second step chains on the first (§ 8.4), not on protos.

10.5. String-substitution references

Rule. A $<NAME> bare reference in a cook shell block, test shell block, or bare shell_command MUST be substituted by the space-joined concatenation of the named recipe’s output list (§ 10.4.1). A $<NAME> reference inside an exec_lua_block is parsed as Lua syntax (a single-element table containing the local variable NAME); cross-recipe references in Lua bodies use cook.dep_output(NAME) and cook.dep_output_list(NAME) per § 11.

  • Empty output. If the named recipe’s output list is empty, the substitution is the empty string. A conforming implementation MUST emit a warning at register time, naming the referring recipe and the referent. The reference itself is not an error.
  • Lua lowering. A conforming implementation MAY lower a $<NAME> reference to cook.dep_output("NAME") and a $<NAME.ACCESSOR> iteration to a cook.dep_output_list("NAME")-driven form. The Standard specifies the observable behaviour; the Lua forms are one conforming lowering.

10.5.1. Example

recipe libmath
    gather "src/math/*.c"
    cook "build/obj/math/$<in.stem>.o" { gcc -c $<in> -o $<out> }
    cook "build/lib/libmath.a" { ar rcs $<out> $<in> }

recipe app
    cook "build/bin/app" { gcc -o $<out> main.c $<libmath> }

The $<libmath> reference in app’s shell body expands at register time to "build/lib/libmath.a" (the single-element output of libmath).

10.6. Dependency implications

  • Name reference ⇒ cross-recipe edge. A name reference to recipe B appearing in any step or dep-list of recipe or chore A establishes a cross-recipe dependency from A to B, observable through execution ordering (§ 13). In particular, B MUST join the build closure of any invocation that reaches A: a chore step referencing $<B> runs only after B has built, with no explicit : B dep-list entry required (CS-0121).
  • Granularity. A dep-list reference (recipe A: B) MUST be interpreted at whole-recipe granularity: every unit of A waits for every unit of B. A placeholder reference ($<B> or $<B.ACCESSOR>) in a specific step of A MAY be refined by the implementation to a per-unit edge from that step’s unit(s) to the unit(s) of B that produced the referenced output items. Coarse edges are always conformant; fine-grained refinement is an optimisation available to placeholder-sourced edges only.
  • Validation and cycles. An implementation MUST validate that the named recipe is defined in scope before the execute phase begins, and MUST reject the Cookfile with a cycle diagnostic if the transitive closure of all cross-recipe edges contains a cycle, before executing any work unit.
  • Every name reference creates its edge. The edge set is a function of § 10.2, not a separate recognition of the shapes that look like references (CS-0210). If a token in a step or dep-list of A resolves to recipe B under § 10.2 step 2, a conforming implementation MUST establish the A → B edge; it MUST NOT exclude the token on a test the resolution does not make. This binds the whole closed set, including the qualified forms of § 11.3: a reference is excluded from the edge set only where § 10.2 itself resolves it to something that is not a recipe — a builtin (step 1), a probe-value reference (step 3, § 22.5.7), a declared variable (step 4), or a hard error (step 5). This bullet and the next are complements: this one forbids losing an edge a reference implies, that one forbids inventing an edge no reference implies. Losing one is the more dangerous direction, because it leaves the build correct-looking and unordered rather than failing; see § C.16.1.
  • Only name references create edges. A string equal to another recipe’s output path does NOT establish a cross-recipe edge, wherever that literal appears: in a gather line, in a cook step’s output pattern, or in the inputs[] / outputs[] entries of cook.add_unit (§ 22.1). All are literal path surfaces and none of them are name references. An implementation MUST NOT infer an edge from path-string equality on any of them. Authors who want “run this after that” without a name reference MUST use an explicit : dep. Note that not inferring an edge is not the same as staying silent: where such a match would otherwise become a silent read-after-write race, and the implementation can detect it from the source alone, the implementation rejects the build with a diagnostic rather than synthesising the missing ordering — see § 16.1.2, which states that rule and enumerates the surfaces it covers. That enumeration is narrower than this prohibition: this bullet binds every literal surface listed above, whether or not a match on it is detectable. See § C.16.1.

10.7. The var. placeholder namespace

Normative.

A placeholder $<TOKEN> whose TOKEN begins with var. is reserved as the explicit declared-variable lookup form. For such a placeholder, the implementation MUST resolve TOKEN by stripping the var. prefix and looking up the remaining identifier as a declared variable per § 10.2 step 4, regardless of whether a recipe or probe key of the same name exists in scope.

The reserved namespace prevents resolution ambiguity. A Cookfile that declares both a recipe named foo and a variable var.foo will see bare $<foo> resolve to the recipe (per the rule order) and $<var.foo> resolve to the variable. The recipe-naming rules of § 3.4 forbid var as either the first or last dotted segment of a recipe name, so a recipe cannot be reached via $<var.X> form by construction.

The two prefixes sit at different points in the cascade (normative; CS-0240). var. is resolved BEFORE steps 1–3: nothing reachable through those steps can be named through it, and a probe keyed var would otherwise make $<var.X> read as member X of that probe — the one thing this namespace exists to prevent.

The reservation binds import aliases too (normative; CS-0240). The pre-CS-0240 rule reserved var only as a segment of a recipe name, which left the alias unconstrained: a Cookfile could write import var …, and its recipes were then addressed as $<var.NAME> — a spelling this section requires to name a variable. Because var. now resolves ahead of every lookup step, such a recipe is unreachable by construction. A conforming implementation MUST therefore reject an import declaration whose alias is var, at parse time, with a diagnostic naming the alias and this reservation. Refusing the declaration rather than each reference is what keeps the remedy a one-word rename instead of a puzzle at every use site.

The retired env. prefix is resolved AFTER steps 1–3, and its refusal is correspondingly narrower. env is reserved as a placeholder segment and NOT as an import alias, so a Cookfile MAY import under the alias env and reference $<env.build> as a qualified cross-Cookfile reference (§ 11.3); that reference resolves at step 2 and carries its dependency edge (§ 10.6), and only a token reaching no earlier step is refused as the retired prefix. An implementation that hoisted the env. refusal alongside var. would silently delete that edge, which § 10.6 names as the more dangerous direction: the build stays correct-looking and becomes unordered.

The pre-CS-0172 spelling of this namespace was env., which named the process environment the declared values then shared. With the namespaces separated (§ 5.3), $<env.NAME> is retired: a conforming implementation MUST reject it with a diagnostic naming both replacements — $<var.NAME> for a declared variable, and, for an ambient process variable, either an ordinary shell $NAME in the step body or an ordinary named shell probe sealed on the unit when it must be a determinant (§ 8.4.3).