Recipes

A recipe is a named bundle of lever positions, committed as an ordinary file. It is the whole of what a target is. Nothing in the runtime, the VM, cake, or the shop takes a target name to mean “these things” — the name is a label the recipe declares for its own settings, and inventing a target is writing a file.

recipes/darwin.json
recipes/playdate.json
recipes/n64.json

Listing that directory is reading the list of targets. There is no enum, no supported-target array, and no second place where a target’s facts are kept.

What a recipe is made of

A recipe has four parts: the platform package it draws its C from, the stamp levers, the build levers, and the shop’s lists.

recipes/ps1.json, whole — the smallest target in the tree, and every field the schema has:

{
  "pit.recipe": 1,
  "name": "ps1",
  "platform": "ps1",
  "system": "ps1",

  "stamp": {
    "profile_name": "ps1-nan32-v1",
    "rep": "nan32", "endian": "le",
    "text": "utf32", "record": "hash",
    "gc": "compact", "wota": 32
  },

  "build": {
    "sched": "single",
    "budgets": {
      "ar_seconds": 60, "fast_turn_seconds": 1,
      "slow_turn_seconds": 60, "slow_strikes": 3,
      "dynamic_region": 65536, "static_region": 16384, "max_images": 2
    },
    "capabilities": {
      "native_payload": false, "dynamic_native": false,
      "daemon": false, "press": false
    },
    "linked_packages": [
      "shoplib", "pitlib", "std", "cart", "wgpu", "libgit", "daemon-stub"
    ]
  },

  "lists": {
    "aliases": {},
    "fallback": ["pitlib", "std"],
    "endowments": ["lang-endowments", "std_endowments"],
    "endowments_whole": ["std_endowments", "ps1"],
    "c_native": [],
    "bin": ["shop_tools", "cake"]
  }
}

name is what the recipe calls itself and must match the filename. system is the token a package’s module arms are matched against — the one sanctioned place a target name appears outside a recipe, where a package declares how it varies. platform names the package that supplies this machine’s distinguished C roles. It is a package identity, resolved by the shop exactly like linked_packages and the provider lists; the recipe assigns no filesystem location. In this repository the Makefile links identities such as ps1, darwin, and playdate to directories below platform/ for development. Those directories can move to separate repositories without changing Cake or the recipe format.

When a platform package also contains provider modules, the recipe includes it in endowments_whole. platform assigns the target-specific roles described below; endowments_whole selects all of its top-level C providers for static linking. Other provider packages are selected the same way. Darwin, for example, selects posix, bsd, apple, darwin, metal, and spritekit there, while its linked_packages selects support packages such as host, posix-runtime, posix-daemon, and cjson.

The format is JSON because a recipe is data. The loader validates it before anything acts on it, and a validator cannot validate a module that has already run.

The two lever groups never mix

This is the structural rule of the schema, and it is enforced rather than documented: a stamp lever written under build, or a build lever written under stamp, is a load failure that names the offending lever.

Stamp-entering levers change interpretability

value, rep, endian, text, record, gc, wota — and the profile_name they hang off. All but value are written; value is derived from rep (below) and a recipe that states it fails to load. These decide the layout of bytes, so an artifact built under one position is not slightly wrong under another, it is uninterpretable. They render back into the profile string exactly:

ps1-nan32-v1;value=32;rep=nan32;endian=le;text=utf32;record=hash;gc=compact;wota=32

The stamp group is the profile string and nothing more — the axis list and its order are the schema. sched is a build lever, not a stamp axis: the scheduler arrangement selects provider files and changes no artifact byte, so identical pools load under a single-threaded and a threaded runtime alike. Stamping it would refuse byte-identical pools over an axis that never touches bytes — the exact false fork the stamp’s own rule (interpretability axes only) exists to prevent.

Every Mach pool, every cart, and the runtime binary carry that string. Mounting compares them and refuses a mismatch by naming both.

The recipe is the authority, and the build derives from it. Every stamp fact a compiled binary states about itself is taken from the recipe at forge time, never hardcoded per axis arm. A combination the compiling toolchain cannot honor is refused at forge, by naming the lever and the reason; a binary whose self-description disagrees with its recipe is a build that must not exist. One record decides, the build renders it, and the artifact stamps repeat it — three copies of one truth, flowing one direction.

value is derived, not written. A rep names a complete value-word scheme, and the word width is part of what it names — so the recipe states rep and forge derives value from it, rendering both into the profile string. A contradiction between them is not refused; it is unwritable. wota stays a recipe lever: it usually matches the value width because both follow the machine, but they answer different questions — rep is chosen for the value representation the target wants to live in, wota for what the architecture serializes fastest — and a build may legitimately pair a small rep with a wide wota when memory and messaging pull in different directions.

Non-stamp levers change what a build contains or may do

budgets, capabilities, linked_packages. None of them moves a byte in any artifact, and none may enter the stamp.

capabilities says what a target is capable of — a machine with no unix sockets cannot have a daemon at any setting. It does not say what one product built for that target carries: that is a per-product feature selection, derived from the product’s own start declaration and its closure, which narrows linked_packages on the way past. So capabilities.daemon: true on darwin means “darwin can”, while a one-shot product forged for darwin still compiles none of the daemon’s C and swaps posix-daemon for daemon-stub. See Features are per product, not per target.

budgets carries two kinds of number. The paging numbers — dynamic_region, static_region, max_images — say how the runtime windows memory. The lifetime budgets say what the scheduler decides about actors:

BudgetMeaning
ar_secondshow long an actor may sit idle before it is reaped; -1 is never
fast_turn_secondshow long one turn runs before it is frozen and re-queued
slow_turn_secondshow long a resumed turn runs before that counts as a strike
slow_strikeshow many strikes halt the actor

These are the whole of the runtime’s liveness policy (see Actors and Memory), and they belong to a target rather than to the code: a console reclaims sooner and tolerates less than a workstation, and the same source should not have to know which it is running on. Each has a compiled-in fallback, so a recipe states only what it wants to move.

That restraint is load-bearing rather than tidy. If a capability lever entered the stamp, every development runtime would sit in its own artifact universe: the realization cache would fork, and a cart built against a dev binary would refuse to mount on the shipped binary it was built for. A budget is the same argument in miniature — a larger region window holds the same bytes, so keying artifacts on it would invalidate a cache for a number no reader of those bytes consults.

The two groups are two records, not one flat bag with a comment, because that is what lets the loader tell them apart.

The recipe describes the runtime; composition is the composer’s arguments

A recipe describes one product: the runtime binary. The other two products — the cart and the bundle — are described per build, by whoever composes them: which actors go in, which payload form ships, whether the cart carries a compiler. Those are the composing invocation’s own arguments:

pit bundle app my/game::main.ce --name Game --payload mcode \
  --with pit-shop/logger,pit-shop/courier

--payload is the form this build’s modules ship as; --with is the service fleet the build carries beyond its own root, and passing none is a real answer — it is what a bare console build wants. There is no schema and no file, because these are one verb’s parameters: everything they decide is recorded in the product’s own manifest, so reproducibility lives in the output while the invocation keeps a makefile’s agility. The standard invocations are committed where invocations live — in the makefiles and scripts that build each product — and a cart for a small device that carries a minimal compiler plus game source, compiling on first boot, is one more such script. No per-target table exists on either side: a console gets an empty fleet because its build script passes no --with, not because it is a console.

The line that earns a file is the recipe’s: data several tools must agree on, that keys artifacts. One invocation’s parameters never qualify.

The platform package names roles; packages supply membership

The platform package answers for what it supplies. Its package.json declares requirements — only the distinguished roles a runtime for this platform needs.

{
  "requirements": {
    "entry": "runtime::platform/n64/source/main_n64.c",
    "scheduler": "runtime::platform/n64/source/sys_thread_single.c",
    "daemon": "runtime::platform/daemon-stub/source/daemon_stub.c",
    "provides": ["runtime::internal/os"]
  }
}

There is no source-file list. Every selected package contributes the C below its source/ directory as runtime support, and a provider package selected by endowments_whole contributes its top-level C as providers. entry, scheduler, and daemon merely identify which structurally discovered files play those roles. provides names modules that support C defines by hand rather than as an extension: os_n64.c is runtime::internal/os on that machine, so the package says so.

There is no platform-owned link list either. Every selected package states what it needs in its ordinary compilation.LDFLAGS. Thus apple owns Foundation, CFNetwork, Security, and Network; metal owns Metal and QuartzCore; and spritekit owns SpriteKit. An optional package’s flags remain only when its PIT_USE_PROBE module compiled, so a separate link_probe table would restate the same dependency.

Toolchain accommodations go in each package’s ordinary compilation block, the same field every package has: -x objective-c on the Apple platforms, -fPIC where a build needs it, the linker’s dynamic-export spelling. They are flags a package needs for its own C, so that package says them, and the runtime builder reads the selected package closure without knowing which platform it is reading.

None of it is a target name interpreted by code. The recipe selects packages; directory position supplies membership; the platform package assigns the few distinguished roles.

The toolchain is the gate. Whether a combination works is answered by compiling it with the target’s toolchain: a build that pulls in a package whose headers don’t exist on that toolchain stops right there, and that failure is the signal — precise, current, and maintained by nobody. The same is true of levers. Every position is expressible on every platform, because at this level it is just portable C; a 64-bit double-math build for a 32-bit console compiles if its toolchain can, and how it runs is the machine’s answer.

The committed recipes are suggestions: worked lever positions that suit each machine, written down so the next person starts from experience. Writing a different recipe for the same machine is the normal way to disagree with one.

The other way is pit forge <name> --set stamp.<lever>=<value>, which is one build disagreeing rather than one machine. The override goes through this same validator, so it can no more mix the groups than a file can. What decides between the two is whether the position is anybody’s: a target somebody builds for earns a file, because a file is what other tools can agree on and what artifacts key against. A position that exists for the length of one run — the thirteen the profile sweep walks to prove the space still compiles — describes no machine and outlives no command, so it is an argument. That is the same line the composition parameters fall on, one product over.

How a build selects one

A build names a recipe, and everything downstream reads it:

  1. The build names a target — --target n64. That name selects recipes/n64.json and does nothing else. It never reaches code that knows what an n64 is.
  2. The loader reads the file and validates the schema and the two-group separation.
  3. The stamp levers render the profile string that every artifact this build produces will carry, and that every artifact it mounts must match.
  4. The build levers decide what compiles in and what the result may do; the lists decide what use and $start mean inside it.
  5. The system token selects the module arms in each package’s own manifest, so a package with per-platform implementations binds the right one.

Nothing in that sequence consults a table keyed by target name. The name selects a file; the file says everything else.

Cake is where that promise is kept or broken, and it is kept: hand cake a name no recipe describes and every per-target answer comes back empty — no runtime C, no link flags, no static rows, no endowments. There is no “everything else” branch left to fall into.

The one reader

shoplib::recipe reads recipes and nothing else does. It lives in shoplib because both sides need the same answer and cake cannot import the shop: cake asks it what compiles, and the shop folds its answer into recipe_fingerprint and therefore into every realization key.

Two consequences follow from that fold. Editing a recipe re-keys every realization, exactly as editing the compiler does — an artifact realized under the old recipe was realized for a different target description. And the fold takes every recipe rather than the host’s, because a shop realizes artifacts for other targets too, so keying on the host’s alone would let a cross-target lever move without re-keying what it moved.

Adding a target

Write a recipe and fill a platform package with the C that compiles on the machine. No page in this track changes, no enum grows, nothing in the VM or in cake learns a name, and the only things that gain a row are the recipe file itself and the target matrix, which is a record of recipes people have written rather than a list anything consults.

The profile axes are the reference for what each stamp lever means, and Building C is how cake turns the answer into binaries.