Skip to content

§ 21 — Cook Lua API surface

21. Cook Lua API surface

Normative. This chapter is the Cook Lua API’s table of contents. The four phase-classified surfaces — register-phase (§ 22), execute-phase (§ 23), both-phase (§ 24), and the filesystem/path helpers (§ 25, § 26) — are defined in the chapters that follow.

The Cook runtime executes Lua code in two disjoint phases:

  • The register phase runs the Lua image produced from the Cookfile exactly once on a single-threaded Lua VM. Its purpose is to record a set of work units without performing external effects. API functions in this phase typically return synchronously and mutate capture state.
  • The execute phase runs the recorded work units on a pool of worker Lua VMs (one per worker thread). Its purpose is to perform the external effects previously recorded.

Every function defined in this Part is classified as register-phase only, execute-phase only, or both phases. A conforming implementation MUST install a function in each of the phases indicated here, and MUST NOT silently install a register-phase-only function in the execute phase (or vice versa) — a Cookfile that calls a function in the wrong phase MUST observe either the documented behaviour for that phase or a Lua runtime error.

Code encountered via > expr (§ 8.7), >{ ... } (§ 3.9, § 8.7), >{ ... } (§ 8.4, § 8.5), module bodies (§ 12.1), and config blocks (§ 5.3) runs with this API in scope.

21.1. API surface overview

A conforming implementation MUST install the following top-level Lua globals before any Cookfile-derived Lua code runs:

GlobalTypePhasesDefined in
cooktableboth§ 22, § 23, § 24
fstableboth§ 25
pathtableboth§ 26
recipetableregister§ 21.2
inputstableexecute§ 23
outputstableexecute§ 23
inputstringexecute§ 23
outputstringexecute§ 23
input_Ntableexecute§ 23

The cook table carries recipe registration, work-unit registration, shell helpers, dependency-output accessors, module loading, platform information, export/import, and environment access. The full set of keys on the cook table is implementation-defined except for the members that are normatively specified in this Part.

cook.probes.get (cross-phase note)

cook.probes.get(key) on the execute-phase VM MUST read from the per-run probe-value store (§ 22.5.8).

On the register-phase VM the lookup has three steps, in order (CS-0219):

  1. The register-phase probe-value store, when key is already resolved there — put there by the gather <probe> pre-pass (§ 22.5.10) or by an earlier read through step 2.
  2. Otherwise, when key names a probe declared in this pass, the probe is resolved now and the read answers with its value (§ 22.5.10, “Register-phase reads”). Resolving at the moment of the read, rather than ahead of the bodies, is what keeps a declared probe nothing reads from being produced at all: the bound is over reads, not over recipe reachability, and that section says exactly how far it reaches. A read made from inside a produce body is further confined to that probe’s declared inputs.requires, and a discovery pass may skip step 2 entirely.
  3. Otherwise the module-scoped persistent kv store, which is what this name meant before probes existed and still means for a key no probe declares. A miss there reads as nil, unchanged.

Step 3 is intentional and transitional — that store is slated for removal alongside cook.probes.set in a later 1.x revision, and steps 1 and 2 are the destination: one name, meaning the probe’s value, on both VMs. The two phases still differ in where a resolved value comes from, and in what a miss means: on the execute VM an unmaterialised probe is an error (§ 22.5.8), because there the DAG was supposed to have scheduled it.

cook.probe

cook.probe(key, opts) is specified normatively in § 22.5.3. It MUST be available on the register-phase VM and MUST raise the register-only API error on the execute-phase VM.

21.2. The recipe global

During the register phase, and only within the body of the Lua function registered by cook.recipe(...) (§ 12), the recipe global is bound to a table with the following fields:

  • recipe.name — string; the declared recipe name (§ 6.1).
  • recipe.gather — a sequence (1-indexed Lua table) of input groups. recipe.gather[i] is itself a sequence of relative file paths resolved from the i-th gather glob pattern, minus any entries matching an exclude pattern (§ 8.2).

A conforming implementation MUST NOT expose the recipe global during the execute phase; it is a register-phase artefact.

21.2.1. Input pattern resolution and empty-expansion diagnostics

Each quoted include pattern on a gather line is resolved independently. Before matching, a pattern whose final path segment is **, or whose spelling ends in /, MUST be normalized to the equivalent recursive-files pattern. Thus both forms select regular files recursively below the named directory; they do not contribute the directory itself. After matching, implementations MUST filter out directories, so every entry exposed through recipe.gather denotes a file.

An ordinary pattern is resolved relative to the Cookfile member that declares the recipe. A pattern with a leading // anchor followed by a path is instead resolved relative to the workspace root (§ 20); the // prefix is an anchor and is not part of the matched path. The root-anchored spellings //, //.., //../x, and any spelling beginning with /// are invalid and MUST be rejected rather than normalized or matched. Any other parent traversal that would escape the pattern’s member-relative or workspace-root resolution base MUST likewise be rejected.

Ordinary exclusions are matched against those logical gathered paths, as specified in § 8.2; they do not contribute a separately expanded physical-path set. This preserves input groups and their ordering while preventing an unrelated normalized physical alias from excluding a gathered file. Permitted parent-component patterns retain their existence-sensitive behavior. This rule does not add support for physical filenames that the existing path encoding cannot represent faithfully. (CS-0255.)

For each include pattern, after normalization, matching, and directory filtering—but before applying excludes—a conforming implementation MUST emit exactly one warning attributed to the declaring recipe when that include’s expansion contains no files. Exclude patterns never emit an empty-expansion warning. If one or more includes initially resolve to files but the excludes remove every resolved file, the implementation MUST NOT warn merely because the recipe’s final input set is empty.

21.3. The var sink inside config_block bodies

Within the body of a config_block (§ 5.3), the global var MUST be bound to the writable declared-variable sink: assignments through it export names into the namespace. This is the only Lua context in which var is writable; everywhere else it is the read-only, type-preserving read surface of § 24.3, and a write through it MUST raise.

A conforming implementation MUST NOT expose the sink itself to any other Lua context — a recipe body, a register block, a module body, or an execute-phase body MUST NOT be able to reach past the read-only surface and mutate a declared value (CS-0172). The reference implementation keeps the sink table in the Lua registry and hands out only a guarded proxy.

The pre-CS-0164 spelling of the sink was a bare global env aliased to cook.env; both are retired. A config_body that reads a global named env MUST produce the load-time diagnostic of § 5.3.