← Back

How rocemit threads the device, memory and machine: a cold review

A fresh agent with no history in this work read rust-codex-compiler at 5ede0f4 (2026-09-14) to answer Steve's question: how many places does the emitter thread a value like Machine through every Codex function that reaches it, is that one pattern or several, and is there a better generalization? It was read-only: it ran rocemit on single units and nothing else. This is its report, lightly edited. Line numbers are in src/roc_emit.rs unless a file is named.

Summary

  1. One pattern, three settings. Device (the GPU kernels, f943473, 2026-09-12) came first; Mem followed the same day (37a5abb, "threaded like a device"); Machine on 09-13 (b756062). All three run through one code path, switched by a state string. They differ only in the trigger (a type, or a scan of who calls whom) and in their door table. Safari threads nothing in the emitter.
  2. Roc's own effects are genuinely different. => arrows and ! names answer the same question ("this property must reach every caller"), but the Codex checker has already written the effect label onto every caller's type, and Roc does the plumbing itself. The emitter only picks the spelling.
  3. The ten "a function value carries no state" refusals are one missing feature, a function value that is generic over whether it touches the outside world. The fix would copy each higher-order definition for the kind of function it is handed: roughly 300 to 500 lines. It would unlock none of the ten, because every one of those units also hands a lambda to process-spawn, which has no door.
  4. Sign-off on the technique, with four tidy-ups worth doing: one door table (door facts live in four places today, and every door commit edits them), a state enum, one "bind the pair" helper, and reconciling three "does this touch the state" tests that disagree about heap marks.
  5. One small change might move the ledger: a constant with an effect (x : [Console] T) as a {} => T function, which the state mechanism already does for a constant [Device] T. Six units hit that refusal; how many would then pass is not checked.

1. Inventory

A. The threaded value: Device, Mem, Machine

Which one a unit uses is the field state (687), chosen when the emitter's context is built (778-851). There are two triggers:

What all three emit:

Only for Mem and Machine, because Codex lets a poke sit inside an expression:

Nine places branch on the state string: 495-499, 1081, 1158, 1170-1174, 1548, 1573, 1699-1703, 1710, 1968-1972.

Refusals it produces:

B. Roc's native effects: => and !

Trigger: the effect labels on Codex types. The checker has already carried them up to every caller, so no closure is needed.

Emission: a Console act is a plain block (2397), print-line becomes line! (2921), and the opening becomes main! (1539).

Where A and B meet:

Refusals:

C. Related, but not this pattern

None of these is emitter machinery.

Duplication

By reading, not reproduced on a unit: with memory threaded, let r = __heap-restore h in <pure> in a pure position skips the let switch (2254, because has_effect is false). It goes through the plain let path to builtin (2680) and becomes 0, so the rewind is dropped. Likewise, a definition whose only state use is a heap mark never joins the closure. - D2. "Bind the pair, move to the next state name" is written six times on the expression side (2171-2174, 2225-2228, 2243-2246, 2256-2259, 2350-2353, 2543-2546) and three times on the statement side (1800-1802, 1815-1819, 1925-1929). The by_closure() && dev.is_some() gate repeats six times. - D3. Door facts live in four places: - the builtin names (44-88); - the arity table (2007-2018) plus the want() checks (2054-2098); - the ! name test (2021-2031); - the Codex-to-Roc name rule, replace('-', "_") (2032).

Machine.roc has its own ! signatures besides. The door commits 4718fd7, b886af1, 4d4e3aa and 1b41722 each edited the builtin lists and two to four places in the emitter. - D4. The nine string branches listed under A. - D5. "Is this row an effect" is computed five ways: fun_parts (1049, which ignores threads), effect_left (1137), has_device (1213), apply (2580-2586) and opening_effects (1519).

2. One pattern or several?

lowered to a threaded value lowered to a Roc effect
trigger: a label on the Codex type Device Console and every other label
trigger: a closure over builtins Mem, Machine (none)

Device, Mem and Machine are one pattern. Each is a set of doors plus every definition that reaches them, taking the state first and answering it back, with the body as blocks that answer a pair.

It is implemented once already; "threaded like a device" is literally true in the code.

Roc effects are a different pattern. The question is the same, but none of the machinery is: Codex already labels every caller, Roc threads the world itself, and there is no state name, pair or lifting. The emitter decides one ! and one arrow. The two meet only where effect_left subtracts what the state answers, and where Machine's host doors are both.

3. Generalizations

G1. Let a function value carry the state (or an effect): not now

The idea:

Types cannot drive it for memory. web-mux-feed's obs : Text -> Integer (codex/os/net/WebServer.codex:333) is pure in Codex, and GopWeb hands it a lambda that pokes (apps/works/GopWeb.codex:177, gopweb-log at 87-95). So the pass would follow where each function value goes. A function value stored in a record or a list, or returned, would still be refused.

Size and risk:

It unlocks none of the ten:

At most G1 would unlock effect-map-effctx. The levers for the ten are a process model in the Machine and, for the desk six, a separate decision about whether an unreached definition may refuse without refusing the unit. That cuts against "refuse what is not built", so it is Steve's call.

G2. One door table (recommended)

The change:

Size and risk: about 80 fewer lines, low risk; a spelling slip fails the ladder or the gpu gate.

What it gets: no behaviour change. A new door becomes one row instead of three or four edits (eight door commits in two days).

One side effect: if heap marks become rows, a definition whose only state use is a heap mark starts being threaded. That fixes half of D1 and moves output, so it needs a ladder run.

G3. A state enum instead of a string

enum State { Device, Mem, Machine }, with the base name, the module, threads, whether definitions are always =>, and the opening's constructor.

G4. One bind-the-pair helper, and drop the by_closure() gate

G5. A constant with an effect as a {} => T function (small)

Sign-off

The core technique is sound and already generalized where the code allows.

The work worth doing is housekeeping (G2 with G3, G4 including D1) and the G5 experiment.

4. Blind spots