Target Profiles
There is one runtime codebase, configured by levers. A target profile is the complete set of lever positions — how a value is represented, how memory is collected, what the scheduler looks like, what the runtime is allowed to do. Every artifact the build produces carries the profile it was made for, and a runtime refuses an artifact built for a different one.
A target name is a label a recipe declares for a set of lever positions, not a thing the system knows. Nothing in the VM, the compiler, the shop, cake, or the runtime branches on “is this a PS1.” There is no supported-target list to be added to, and no per-platform mode. A recipe sets the levers it wants — 32-bit word, fixed-point numbers, single-threaded scheduler, no native loading, a cart carrying no compiler — and that combination is the PS1 build. Any other combination is equally expressible, whether or not anyone has named it.
So this page is normative and The Target Matrix is not: the matrix records recipes that have been built and run, as advice about which positions suit which machine. Recipes is the file these levers are written in, and which of them enter the profile string at all.
The one place a target name appears outside a recipe is a package’s own modules arms,
where the package declares how it varies and the recipe’s name selects an arm. That is the
package speaking about itself, matched against a string; nothing derives platform, ABI, or
toolchain facts from the name, because the recipe already states them. Raw endowments need
no arm at all — they are per-platform by being in a per-platform package.
This is also the object the rest of the documentation refers to when it says an image “carries the complete target profile.”
The axes
| Axis | Chooses | Values |
|---|---|---|
| target / architecture | which packages supply platform C, and which toolchain builds it | whatever the recipe names |
| value word | how wide a PitValue is | 64, 32, or 16 bits (nano) |
| numeric representation | how numbers are encoded | see below |
| endian | byte order of the mach payload | little, big |
| text | the text body layout and its hash | utf32, kim8 |
| record | record layout | profile-selected |
| GC | the collector | copy, compact, arena |
| wota width | the in-runtime encoding’s word width | independent lever: 16, 32, 64 — chosen for what the target CPU handles fast, which may differ from the value word |
| provider set | which endowments the binary supplies | per target |
| development powers | native build/load; $vm, $hook, $pgo, $inspect | linked in or left out |
The first eight are physical: they change how bytes are laid out, so an artifact built under one is meaningless under another. The last two are about authority rather than layout, and both are read off the provider catalog the binary was linked from — so the binary itself answers what it can grant. See Capabilities and Debugging.
The scheduler arrangement — threaded or single-threaded — is a build lever rather than an axis of this profile. It selects which provider files a binary compiles and moves no byte in any artifact, so identical pools load under either. See Recipes.
That division is load-bearing, and only the physical axes belong in the profile string.
Installing the call hook adds a branch to the interpreter; it does not move a byte in any
artifact. If $hook, $pgo, $inspect or the native-build power 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 one it was
built for. Which endowments a binary supplies is a link-time fact, invisible to
everything it produces.
The practical consequence, since a provider normally enters a build only because something
claimed it: a runtime that wants to offer $hook and $pgo with nothing claiming them takes
their provider package whole. See
Endowments.
Numeric representations
The numeric axis is the one that varies most, because it is where a target’s hardware shows through.
| Rep | Value word | For |
|---|---|---|
nan64 | 64-bit | the desktop default: tagged integers and short doubles |
nan32 | 32-bit | the handheld and console default: NaN banks over binary32, region-relative references |
fixed32 | 32-bit | fixed-point (Q16.16-class) for a tiny handheld with no FPU and no room for soft-float |
val16 | 16-bit | the nano word: small immediate integers and a fixed-point immediate lane, references into a banked pool |
fixed | 64-bit | a fixed-point pilot under the 64-bit word (Q47.16) |
f32 | 64-bit | a binary32 pilot under the 64-bit word |
fixed and f32 share nan64’s profile name and widths and differ only in the numeric
lane, which is why the profile string carries rep= separately from value=.
A dec64 rep — decimal, exact for decimal fractions — is the reason the axis exists in this shape. Numbers cross between runtimes as nota, which carries a decimal coefficient and exponent, so a runtime built one way and a runtime built another agree on what a number is without agreeing on how to store it.
The profile string
The axes that affect layout are concatenated into one string that every artifact carries:
host-nan64-v1;value=64;rep=nan64;endian=le;text=utf32;record=…;gc=copy;wota=64
A Mach pool, a cart, and the runtime binary all carry it. Mounting compares them, and a mismatch is refused by naming both — the artifact’s and the runtime’s. That refusal is the point: a cart built for a Playdate is not slightly wrong on a desktop, it is uninterpretable, and failing at mount is better than failing later.
Artifacts never cross profiles
Because the profile decides output bytes, it is part of every derivation key. Two builds differing on any axis are different computations with different keys, so each runtime draws the artifacts built for its own configuration, and the store keeps every profile’s images under their own names.
Portable artifacts sit above this line. Source and mcode are target-neutral by construction — an mcode unit records no target, no layout, and no representation — so one mcode object serves every profile, and only finalization binds it to one. See Build and Artifacts.
Building for another profile
A build does not have to target the machine it runs on. A desktop can produce the complete artifact set for a handheld or a console — including the boot artifacts that machine needs to start at all — by selecting a different profile.
This is more than cross-compiling for another CPU: the value representation, the collector, and the text layout all change, so what is produced is a different runtime and a matching set of artifacts, neither of which the host can run. The host builds them, stamps them, and ships them.
Porting
Bringing up a new machine is writing a recipe and filling a provider package with the C that compiles there. Nothing in the runtime learns its name, no list grows, and no existing page becomes wrong.
The build itself is ordinary tooling — a recipe that picks the lever positions, asks cake for the C, and cements a cart from the actor closure it wants. Nothing about that has to live inside pit, in the same way that targeting a new architecture with a C compiler is a matter of flags and a backend rather than a new mode in the language.
Two things stay separate while porting. What the machine can physically do is the profile — word width, numbers, collector, threads — plus whichever raw endowments the packages the recipe names supply. What a particular build is allowed to do is which actors its boot cart carries and which endowments they hold. A PS1 build wants a 32-bit word because that is the machine, and wants no compiler in its cart because that is a choice; those are different kinds of decision and neither implies the other. See Compilation and Shipping.