§ 11 — Cross-Cookfile composition
11. Cross-Cookfile composition
Normative. This chapter defines cross-Cookfile composition: how a Cookfile loads a sibling Cookfile via
import, how qualified name references (alias.recipe) resolve across the boundary, and how a workspace of Cookfiles is composed. Theusedeclaration, which brings a Lua module into scope, is defined in § 12.1 — not in this chapter. The grammar of theimportdeclaration is given in App. A.2.
11.1. Overview
This chapter specifies cross-Cookfile composition. The import declaration (§ 11.2) binds a sibling Cookfile to a local alias. A qualified name reference (alias.recipe) resolves under the rules of § 10.2, with the lookup set replaced by the recipe names of the imported Cookfile instead of the current one. All other name-reference rules of § 10 — accessors (§ 10.3), iteration (§ 10.4), string-substitution (§ 10.5), dependency implications (§ 10.6) — apply unchanged.
11.2. import <name> <path> declarations
An import declaration has the form import <name> <path>, where <name> is a BARE_IDENTIFIER and <path> is a non-whitespace path token (App. A.5).
A conforming implementation MUST validate the <path> token at parse time according to one of two shapes:
- Tree-relative.
<path>begins with./or with a bare segment, contains no..segments, and is not absolute. The path resolves relative to the directory of the enclosing Cookfile. - Sigil-anchored.
<path>begins with//(the workspace-root sigil). The remainder of<path>after the sigil is forward-only (no.., no leading/). The path resolves relative to the workspace root (§ 20).
A <path> matching neither shape MUST be rejected at parse time. Specifically:
<path>containing a..segment (in the tree-relative form) MUST be rejected. Diagnostic:import path '<path>': '..' segments are not permitted; use the workspace-root sigil '//path' for cross-cutting imports.<path>that is absolute and does not begin with//MUST be rejected. Diagnostic:import path '<path>': absolute paths are not permitted; tree-relative or '//' sigil.<path>beginning with//and containing a..segment after the sigil MUST be rejected. Diagnostic:import path '<path>': '..' segments are not permitted after '//'.- A sigil-anchored path that resolves to a directory outside the workspace root after symlink canonicalisation MUST be rejected at workspace load (§ 20). Diagnostic:
import '<name>': sigil path resolves outside workspace root '<root>'.
A conforming implementation MUST resolve <path> per the shape rules above (tree-relative against the enclosing Cookfile’s directory; sigil-anchored against the workspace root, § 20) and MUST load the Cookfile at the resolved-path’s Cookfile as a sibling Cookfile bound to the local alias <name>.
A conforming implementation MUST reject an import declaration for which:
- the resolved directory does not exist;
- the resolved directory’s
Cookfiledoes not exist; - the resolved directory’s
Cookfilefails to parse.
A conforming implementation MUST treat tree-relative and sigil-anchored imports uniformly for the purposes of qualified name references (§ 11.3), dependency implications (§ 10.6), and duplicate-alias detection (§ 11.5). The two shapes differ only in their resolution rule.
An import declaration does NOT bring a Lua value into scope under <name>. The alias <name> is reserved for cross-Cookfile recipe references of the form <name>.<recipe> that appear in dependency lists (§ 8.2), not for Lua expressions. A Cookfile loaded by import is parsed independently; its own use and import declarations are resolved relative to its own directory, not the importer’s directory.
A conforming implementation MUST reject a Cookfile in which two import declarations share the same <name> (App. A.2, duplicate-import clause; see also § 11.5).
Example 11.2.1
import backend ./services/backend # tree-relative
import frontend ./apps/frontend # tree-relative
import core //core/lib # sigil-anchored to workspace root
recipe "bundle": "backend.build" "frontend.build" "core.core_lib"
Here backend and frontend are tree-relative aliases for sibling Cookfiles below the importer’s directory; core is anchored at the workspace root via the // sigil and resolves to a directory at or below the workspace root, regardless of where the importing Cookfile sits in the tree. The dependencies "backend.build", "frontend.build", and "core.core_lib" refer to recipes in those Cookfiles uniformly; the first segment before the dot selects the import alias, and the remainder is the recipe name in the imported Cookfile.
Note 11.2.1
Because an import alias does not bind a Lua value, there is no interaction between import backend … and a use backend for a module of the same name: they occupy different namespaces. The duplicate rule of § 11.5 only prohibits collisions within the import alias namespace.
11.3. Qualified name references across Cookfiles
A qualified name reference has the form alias.recipe, where alias is a local alias introduced by an import declaration (§ 11.2) in the referring Cookfile and recipe is a recipe name defined in the imported Cookfile. Qualified name references are governed by § 10 (Cross-recipe references) with the following refinement of resolution:
- In § 10.2 step (2), the lookup set is the union of the recipe names in the current Cookfile and the set of
alias.recipenames, wherealiasranges over the importing Cookfile’simportaliases andreciperanges over the recipe names in the Cookfile bound toalias. A whole-token match againstalias.recipewins over an accessor split on the same token, both being part of step (2). - Within § 10.2 step (2), the accessor-split rule applies as written: when the
PREFIXis itself a qualified namealias.recipe, the reference is{alias.recipe.ACCESSOR}and PREFIX is matched whole against the qualified lookup set before the SUFFIX is checked against the path-accessor set (§ 10.3).
All other rules of § 10 apply unchanged: dependency implications (§ 10.6), dep-driven iteration (§ 10.4), string-substitution (§ 10.5), reserved final segment (§ 10.2.2). In particular, a qualified name reference establishes a cross-recipe edge with the same granularity rules as an unqualified one.
11.4. use scope pointer
use scope is defined in § 12.2. Cross-Cookfile composition is at the recipe level via qualified name references (§ 11.3), not at the module level. The cook runtime treats each Cookfile’s module tree as an independent search root, and a Cookfile shares module code with another only within its own subtree, by the path form of § 12.2.1. [amended CS-0206]
11.5. Duplicate and cycle detection
A conforming implementation MUST reject a Cookfile in which two import declarations share the same local alias. This rule is restated normatively in App. A.2 (duplicate-import clause); this section specifies the timing and the diagnostic surface.
Duplicate imports. The check is a parse-time check performed on the Cookfile in which the duplicates appear. A conforming implementation MUST detect the duplicate before loading the imported Cookfiles (§ 11.2) and MUST report both offending lines in the diagnostic. The rule is per-Cookfile: two different Cookfiles in the same workspace MAY each define an import foo ... of their own, because each alias namespace is scoped to its declaring Cookfile.
Import cycles. A conforming implementation MUST detect cycles in the import graph when loading the workspace and MUST reject a workspace whose import edges form a cycle. A cycle is defined as a sequence of canonicalised directory paths D_1, D_2, ..., D_k, D_1 such that each D_i/Cookfile contains an import declaration whose path resolves to D_{i+1} (and the last edge resolves back to D_1). Detection MUST use canonicalised paths so that two syntactically different <path> strings that refer to the same directory are recognised as the same node.
A conforming implementation MUST NOT reject the diamond case for sigil-anchored imports: if two distinct sigil-anchored imports both resolve to the same canonicalised directory, the implementation MUST load that directory once and reuse it. Tree-relative imports cannot form a diamond under §7.2 — two distinct tree-relative paths from sibling subtrees cannot canonicalise to the same directory without .. traversal, which the §7.2 grammar forbids. Diamond deduplication for sigil imports is not a cycle.
Canonical workspace-qualified prefix. Diamond deduplication leaves one loaded directory reachable through more than one chain of aliases, while implementation surfaces that present workspace-wide recipe names — recipe listings, workspace-scoped invocation targets, and diagnostics — need exactly one dotted prefix per directory. A conforming implementation MUST derive that canonical prefix from the shortest alias chain from the workspace root to the directory, breaking equal-length ties by declaration order (a breadth-first walk over the import graph from the root, following each Cookfile’s import declarations in source order). Consequently, a directory the root Cookfile imports directly is always named by the root Cookfile’s own alias — the addressability § 20.2.2 requires of the entry Cookfile’s imports — and a directory’s canonical prefix cannot change because an import elsewhere in the workspace was added, removed, or reordered, unless the change alters the shortest chain itself. The canonical prefix is a naming rule only: it MUST NOT affect qualified-name resolution within any Cookfile (§ 11.3), which remains scoped to the declaring Cookfile’s own aliases, and it MUST NOT contribute to cache identity (§ 17.1.1).
A workspace-qualified probe identity is the ordered pair of its declaring Cookfile’s canonical prefix and its Cookfile-local key. An implementation MUST retain that prefix as part of the identity and MUST NOT recover it by splitting a rendered qualified-key spelling: a quoted local key may contain ., so the spelling alone does not identify the prefix. Two imported Cookfiles may therefore declare the same quoted dotted local key without either probe’s declaring directory or identity collapsing into the other. [amended CS-0249]
The duplicate check and the cycle check operate on different graphs and are independent. A Cookfile MAY violate either without violating the other.
Example 11.5.1
import backend ./services/backend
import backend ./services/backend_v2
Rejected at parse time: both lines declare the alias backend (App. A.2). The first line’s target is irrelevant to the diagnostic; the collision is on the alias, not the path.
Example 11.5.2
Given three Cookfiles:
/ws/a/Cookfile: import b ../b/ws/b/Cookfile: import a ../a/ws/Cookfile: import a ./aThe workspace load that begins at /ws/Cookfile MUST be rejected: canonicalisation yields the cycle /ws/a -> /ws/b -> /ws/a.
Note 11.5.1
Duplicate-import detection fires at parse time because the governing grammar production (App. A.2) constrains a single Cookfile’s token stream, which is the unit the parser consumes. Cycle detection, by contrast, is a property of the import graph and necessarily crosses Cookfile boundaries; it fires at workspace-load time as the graph is walked (§ C.11).
11.6. Config-block composition across Cookfiles
Normative. This section specifies how
configblocks (§ 5.3) behave when a workspace loads more than one Cookfile. It settles two questions § 5.3 leaves to the single-Cookfile case: the scope of a config block across animportboundary, and how a@PRESETselection resolves against the several Cookfiles a workspace loads.
11.6.1. Config scope is the declaring Cookfile
A config block’s scope is exactly its own Cookfile. Its var.* outputs and its host.* reads (§ 5.3, § 5.3.2) parameterise only that Cookfile’s own $<NAME> placeholders. Neither a var.* value nor a host.* read crosses an import boundary: an imported Cookfile does not observe an importer’s config values, and an importer does not observe an imported Cookfile’s config values. There is no merged config namespace across a workspace, and no merge pass runs across Cookfiles.
A conforming implementation MUST resolve each Cookfile’s $<NAME> placeholders against that Cookfile’s own config outputs alone. Consequently, two Cookfiles in one workspace MAY each declare a config value under the same NAME without collision — each resolves to its own value, and the two never meet.
11.6.2. @PRESET selection is validated against the union of all loaded Cookfiles
An overlay selection (a @PRESET name, § 5.3.1) is a single per-run global. A conforming implementation MUST validate the selected name against the union of the named config_block names of every Cookfile loaded in the run — the root Cookfile and every transitively imported Cookfile — not against the entry Cookfile alone.
- A selected name that matches a named
config_blockin any loaded Cookfile is valid. - A selected name that matches a named
config_blockin no loaded Cookfile MUST be rejected with a load-time error. The diagnostic MUST identify the requested name and the deduplicated union of available names across all loaded Cookfiles. - A loaded Cookfile that does not declare an overlay of the selected name runs its base config for that run — a silent no-op, not an error. Only a name that exists in no Cookfile is an error.
Selection remains at most one overlay per Cookfile (§ 5.3.1): the per-run global names one overlay, and each Cookfile either matches it (running base + that one overlay) or no-ops to its base. A run in which some Cookfiles match the selected name and others no-op is still one overlay per Cookfile, not several overlays on one file, so the § 5.3.1 single-overlay restriction is untouched.
Because the selection is a per-run global validated against the whole loaded set, cook <alias>.<recipe> @PRESET (invoked from an importer) and cd <alias-dir> && cook <recipe> @PRESET (invoked from the imported directory) MUST select the same overlay for the imported Cookfile. A selection reachable from one invocation directory but not another would let the invocation directory influence which bytes a step produces, which §20.2.3 forbids for cache identity.
Example 11.6.1
/ws/Cookfile: import cli ./cli/ws/cli/Cookfile: config profiling var.CARGO_PROFILE_ARGS = "--profile profiling"cook cli.build @profiling, invoked from /ws, is valid: profiling is declared in the loaded cli/Cookfile, so validation passes even though the root Cookfile declares no profiling overlay. The root Cookfile, lacking a profiling overlay, runs its base config; cli/Cookfile runs its base plus the profiling overlay. cook build @nope from /ws is a load-time error naming nope and listing profiling (the union of available names).
11.6.3. Reserved: cross-Cookfile preset routing
Non-normative (reserved). This subsection reserves a direction; it specifies no requirement, and no conforming implementation is obliged to implement it.
A future revision MAY let an importer steer which overlay each of its imports registers under — a config.<alias> routing directive inside a config block — together with a resolution rule for a Cookfile reached by more than one import path (a diamond): agreeing routings select the agreed overlay, and disagreeing routings abort the run. This direction is deliberately left unspecified here because the § 11.6.2 silent-no-op already lets every in-tree import compose under a single per-run global, and no consumer has demonstrated the need for per-import routing. Reserving it — rather than specifying it — keeps the composition surface minimal while recording the design for when a concrete need appears. Until then, an implementation that does not provide routing is fully conforming.