§ 13 — Two-phase model
13. Two-phase model
Normative. This chapter defines the register/execute split that governs every Cookfile evaluation. The phase classification of every Lua-bearing surface form is consolidated in the table at § 13.2.
13.1. Two-phase execution
A conforming implementation MUST evaluate each selected recipe in two phases: a register phase and an execute phase. The register phase discovers the work a recipe wishes to perform; the execute phase performs it. The phases MUST NOT be interleaved within a single recipe: every unit of work produced by the register phase of a recipe MUST be known before the execute phase of that recipe begins.
During the register phase, the implementation evaluates the recipe body as Lua code (§ 21) in capture mode. Cook Lua API calls that declare work — cook.add_unit, cook.step_group, and the step kinds in § 6 that desugar to these calls — record the work into a list of work units rather than performing it. The register phase MUST run on a single thread with a single Lua VM per Cookfile registration pass; every recipe registered by that Cookfile shares the same VM for the duration of the pass.
During the execute phase, the implementation arranges the recorded work units into a dependency graph (§ 15.1, § 16.1) and executes the ready units. The execute phase MAY run work units in parallel where the dependency rules below permit it. The execute phase MUST respect every ordering relation established during the register phase.
A conforming implementation MAY register and execute different recipes concurrently only when doing so does not observably reorder work units relative to the rules in § 15.1–§ 16.2; in particular, a recipe that depends on another recipe (§ 16.1) MUST NOT begin execution of any of its work units until every work unit of the prerequisite has completed successfully.
Example 13.1.1
recipe build
gather "src/*.c"
cook "build/$<in.stem>.o" { gcc -c $<in> -o $<out> }
Register phase: the recipe body runs once. The cook step expands to a step group containing one work unit per matched source file. No gcc process is spawned.
Execute phase: the recorded units run, each in its own subprocess. Members of the step group MAY run in parallel (§ 15.1).
Note 13.1.1
The two phases use different Cook Lua API bindings for the same call. In the register phase cook.exec records a shell work unit and returns the empty string without spawning a process; in the execute phase a shell work unit is dispatched to a subprocess. See § 21 for the capture-mode contract of each API.
13.2. Phase classification of every Lua-bearing surface form
Every Lua expression in a Cookfile is evaluated in exactly one of three timings. The following table is normative; rows that link to other sections defer the surface-level definition to that section, and merely state the phase here.
| Surface form | Source section | Phase | Produces a unit? |
|---|---|---|---|
config block body | § 5.3, § 5.3.1 | load (before any register phase begins) | no — runs against Cook Lua API state directly |
Cook module top level (return mod) and init() | § 11.1 | load (when the importing Cookfile’s use resolves) | no |
| Chore parameter default (Lua expression) | § 7.1.1 | load (Cookfile-scope VM, evaluated lazily when argv is absent; read-only cook.* — register-only API calls from inside the expression raise an error) | no |
register_block body | § 5.4 | register | no — spliced verbatim into the compiled register-phase Lua program |
top-level module_call (alias.fn(…)) | § 4.4 | register | no — spliced verbatim at source position |
recipe-body module_call (ns.fn(…)) | § 8.1 | register | no — bare module call in a recipe body; CS-0134 reinstates this dispatch (§ 4.4), reversing CS-0072, and it replaces the removed >> / >>{ prefixes |
lua_line (> body) | § 8.7 | execute | yes — admitted only in body position (a cook/test/probe step’s body opener) or as a standalone step inside a chore body; never a standalone recipe-body item (CS-0134). Recipe body-bundling (§ 15.2) is vacuous. |
lua_block (>{ … }) | § 8.7 | execute | yes — same admitted positions as lua_line (CS-0134) |
cook STR >{ … } (cook-body Lua) | § 8.4, § 8.5 | execute | yes — recorded with lua_code payload, with § 23.1 bindings |
cook.add_unit({lua_code = "…"}) payload | § 22.1 | execute | yes — runs on the worker VM that picks up the unit |
test { … } (shell block) | § 8.6 | execute | yes — recorded with shell payload via cook.add_unit (step_kind = "test", CS-0185), with § 9.4 substitutions |
test >{ … } (Lua block) | § 8.6 | execute | yes — recorded with lua_code payload via cook.add_unit (step_kind = "test", CS-0185), with § 23.2 bindings |
Three corollaries:
- The single-
>family is execute-phase; register-phase Lua inside a recipe body is a baremodule_call. As of CS-0134 the doubled->family (>>/>>{) is removed from the language (§ 3.7); the surface-level rule a Cookfile author needs to remember is that>/>{are execute-phase and appear only in body position (or inside a chore body), while register-phase Lua in a recipe body is written asns.fn(…). Everything else in the table follows from that plus the splicing rules in § 5.4 and § 4.4. register_blockbodies, top-level module_calls, and recipe-body module_calls are register-phase. They splice verbatim into the compiled register-phase Lua program at their source positions (§ 5.4.1); the module’s recordedcook.add_unitcalls fire during registration and the work units run at execute. Note: CS-0134 reinstates recipe-bodymodule_calldispatch (§ 8.1), reversing the CS-0072 removal; see § 4.4 for the shared module-call form.- Inside an execute-phase Lua chunk, register-only Cook Lua API helpers are unavailable.
cook.add_unit,cook.exec,cook.interactive,cook.step_group, andcook.recipeMUST raise a Lua runtime error when invoked from execute-phase Lua.cook.shis available in both phases (§ 24.1). See § C.6.12.
13.3. Unified work-unit DAG
The register phase produces a single work-unit DAG covering every recipe reachable from any dispatched target within a Cookfile registration pass. Cross-recipe dependencies (§ 16.1) are edges of this unified DAG, not synchronisation boundaries between separate per-recipe partitions.
A conforming implementation MAY use any topological ordering of recipe-body invocation at register time — for example, to resolve cook.dep_output references (§ 24.7) cleanly when one recipe consumes another’s terminal outputs. The choice of register-time ordering is not normative. The execute phase walks the unified DAG; the order in which recipe bodies were invoked during the register pass does not constrain execute-time parallelism of the resulting work units.
A conforming implementation MUST NOT expose register-phase grouping (sometimes informally called “waves” in earlier reference implementations) as a normative concept to authoring users. See § C.13.5.