Skip to content

§ 14 — Capture mode

14. Capture mode

Normative. This chapter defines the determinism contract for register-phase Lua execution.

14.1. Capture-mode semantics

Capture-mode Cook Lua API calls MUST be pure with respect to the Cookfile’s observable build state: they MUST NOT spawn subprocesses that affect declared outputs, modify files under the working directory, or block on external I/O. Their only effect on conforming evaluation MUST be to append entries to the register-phase record or to read metadata already established (for example, the terminal outputs of a prerequisite recipe that has already completed its register phase — see § 21).

Capture-mode calls MUST return synchronously. Any value a capture-mode call produces — for example, the output path string assigned by a cook step’s expansion — MUST be a deterministic function of the Cookfile source and the register-phase record accumulated so far. It MUST NOT depend on wall-clock time, process identifiers, or the contents of files that the recipe’s own work units produce.

An implementation MAY permit narrowly-scoped capture-mode side effects that participate in register-phase Lua control flow rather than in the build itself. Any such exception MUST be documented as part of the Cook Lua API contract in § 21 and MUST preserve the determinism requirement above with respect to the register-phase record.

Example 14.1.1

register
    local flag = var.OPTIMISE
    cook.recipe("build", {}, function()
        cook.add_unit({
            inputs  = { "src/app.c" },
            output  = "build/app.o",
            command = "gcc " .. flag .. " -c src/app.c -o build/app.o",
        })
    end)

The register block body runs during the register phase (§ 5.4). It reads var.OPTIMISE — a variable a config block declared, resolved before the register phase began (§ 5.3, CS-0172) — and binds flag, a local visible to the register-phase Lua that follows in the same block. The cook.add_unit call then records the recipe’s work unit, folding flag into the command text at registration time. No subprocess is spawned during registration.

The env read is register-phase precisely because it sits in a register block (equivalently, it could be a recipe-body module_call, § 8.1); its result influences which units are recorded and how. Execute-phase Lua — the > / >{ body of a cook / test step (§ 8.7) — runs on a worker VM during execute, after every unit has already been registered, and so could not influence registration. As of CS-0134 the register-phase doubled-> prefixes (>> / >>{) no longer exist (§ 3.7), and a recipe body cannot host a loose register-phase local assignment; an env read that must shape registration therefore belongs in a register block, as shown.

The register-phase VM evaluates register_block bodies (§ 5.4) and top-level module_call source (§ 4.4) in the source order of their containing top-level items, interleaved with recipe registrations. The VM persists across all toplevel_items of a single Cookfile; local variables declared at the chunk-top-level of an earlier register_block body remain in scope for subsequent register_block bodies, top-level module_calls, and recipe-body module_call lines in later recipes. The implementation MUST splice each register_block body AND each top-level module_call into the compiled register-phase Lua program at its source position; no per-block VM is permitted.

The reference codegen in cli/crates/cook-luagen/src/recipe.rs implements this rule by merging recipes, chores, register blocks, and top-level module_calls into a source-line-ordered iteration before emission (informative). In the reference implementation, the register phase’s whole-workspace artifact — the per-Cookfile registration results merged under their qualified import prefixes — is the RegisteredWorkspace type defined in cli/crates/cook-contracts (registration module) as plain contract data; cli/crates/cook-register produces it and the execution engine consumes it without depending on the crate that runs registration, which enforces the rule that registration is closed before execute begins (informative).

Note 14.1.1

The capture-mode contract makes work-unit discovery finite, deterministic, and independent of the order in which work units are later executed. A register phase that blocked on a running subprocess would serialise the register phase of other recipes against that subprocess, defeating the two-phase design.