diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -173,9 +173,10 @@
 A program started for the fire is asked one request, always `id` 1, and its
 `stdin` is closed behind it, so it may read its input whole or line by line, as
 it pleases. It is waited for once it has answered, and a non-zero exit fails
-the run. So does a reply that is not JSON, carries no `𝑛`, answers another
-`id`, or an `𝑛` that does not parse, or a program that quits without
-answering — always with the program's own `stderr` in the message.
+the run. So does a reply that is not JSON, carries neither `𝑛` nor `ask` (the
+next section is about `ask`), answers another `id`, or an `𝑛` that does not
+parse, or a program that quits without answering — always with the program's
+own `stderr` in the message.
 
 Each key of the registry is a regular expression, and it must match the whole
 λ name, so a plain name such as `L_number_plus` means that one atom and nothing
@@ -220,34 +221,54 @@
 
 ### Reducing the operands of an atom
 
-A program gets at the parts of `𝑏` by calling `phino` again, so no API has to
-be exposed for it. The `--inside` option is how it asks: the expression it
-names is bound to a fresh synthetic attribute of the input expression, which
-the run takes as the universe, normalized there, and then dataized. This is the
-same trick `phino` plays internally whenever it has to reduce a sub-expression
-the program does not contain:
+An operand reaches a program as it was written: `5.plus( 6.plus( 7 ) )` fires
+`L_number_plus` with `x ↦ Φ.number( … ).plus( … )`, and getting a number out of
+that is dataization, which is `phino`'s business and not a program's. So the
+program asks. It writes a line of its own, an `id` it mints and, under `ask`,
+the 𝜑-expression it wants reduced, and `phino` answers with that `id` and the
+result under `𝑛`:
 
-```bash
-$ phino dataize --atoms=atoms.json --inside='5.plus( 6 )' universe.phi
-40-26-00-00-00-00-00-00
+```text
+{"𝑒": "⟦ bytes ↦ ⟦ … ⟧, number ↦ ⟦ … ⟧, φ ↦ … ⟧"}
+{"id": 1, "λ": "L_number_plus", "𝑏": "⟦ x ↦ Φ.number( … ).plus( … ), ρ ↦ … ⟧"}
+{"id": 7, "ask": "⟦ x ↦ Φ.number( … ).plus( … ), ρ ↦ … ⟧.ρ"}
+{"id": 7, "𝑛": "⟦ Δ ⤍ 40-14-00-00-00-00-00-00 ⟧"}
+{"id": 8, "ask": "⟦ x ↦ Φ.number( … ).plus( … ), ρ ↦ … ⟧.x"}
+{"id": 8, "𝑛": "⟦ Δ ⤍ 40-2A-00-00-00-00-00-00 ⟧"}
+{"id": 1, "𝑛": "Φ.number( φ ↦ Φ.bytes( φ ↦ ⟦ Δ ⤍ 40-32-00-00-00-00-00-00 ⟧ ) )"}
 ```
 
-Here `universe.phi` is the 𝜑-program the atom is being fired inside — the very
-text the program was told under `𝑒`, which it feeds back on `stdin`.
+The universe, the request and the two answers are `phino`'s; the two questions
+and the last line are the program's. A question mints an `id` of its own,
+which `phino` echoes, so a program may keep several of them open and still
+tell the answers apart.
 
-So a `L_number_plus` that reduces its own operands reads like this:
+`phino` serves a question by binding the expression to a fresh synthetic
+attribute of the universe, normalizing it there and dataizing it — the same
+trick `--inside` plays — so the answer is a byte formation and the program
+reads its `Δ`; where an atom on the way cannot fire and `--partial` parks it,
+the answer is the residual program instead.
 
+Serving a question re-enters the evaluator, so a question may cost a fire of
+the very atom that asked it. That request arrives while the question is still
+open, which is why a program that asks reads on instead of waiting for one
+line. The step budget of the run, `--max-steps`, bounds the nesting.
+
+Only a program kept for the run may ask. `phino` closes the `stdin` of a
+program started for the fire behind its request, since such a program may read
+its input whole before it answers, so there is nothing left to answer a
+question over, and one that asks anyway fails the fire.
+
+So a `serve` entry of `L_number_plus` that has `phino` reduce its operands
+reads like this:
+
 ```js
 const readline = require('readline');
-const { execFileSync } = require('child_process');
-let universe;
-const dataized = (expr) => execFileSync(
-  'phino',
-  ['dataize', '--atoms=atoms.json', `--inside=${expr}`],
-  { input: universe, encoding: 'utf8' }
-).trim();
-const number = (expr) => Buffer
-  .from(dataized(expr).replace(/-/g, ''), 'hex')
+const open = new Map();
+let minted = 0;
+const said = (message) => process.stdout.write(`${JSON.stringify(message)}\n`);
+const number = (answer) => Buffer
+  .from(/Δ ⤍ ([0-9A-F-]+)/.exec(answer)[1].replace(/-/g, ''), 'hex')
   .readDoubleBE(0);
 const hex = (value) => {
   const bytes = Buffer.alloc(8);
@@ -256,26 +277,51 @@
     .map((octet) => octet.toString(16).toUpperCase().padStart(2, '0'))
     .join('-');
 };
-readline.createInterface({ input: process.stdin }).on('line', (line) => {
-  const message = JSON.parse(line);
-  if ('𝑒' in message) {
-    universe = message['𝑒'];
+function* plus(b) {
+  const rho = number(yield `${b}.ρ`);
+  const x = number(yield `${b}.x`);
+  return `Φ.number( φ ↦ Φ.bytes( φ ↦ ⟦ Δ ⤍ ${hex(rho + x)} ⟧ ) )`;
+}
+const advance = (atom, id, answer) => {
+  const step = atom.next(answer);
+  if (step.done) {
+    said({ id, '𝑛': step.value });
     return;
   }
-  if (message['λ'] !== 'L_number_plus') {
-    throw new Error(`unsupported atom ${message['λ']}`);
+  minted += 1;
+  open.set(minted, { atom, id });
+  said({ id: minted, ask: step.value });
+};
+readline.createInterface({ input: process.stdin }).on('line', (line) => {
+  const message = JSON.parse(line);
+  if ('λ' in message) {
+    advance(plus(message['𝑏']), message.id, undefined);
+  } else if ('𝑛' in message) {
+    const waiting = open.get(message.id);
+    open.delete(message.id);
+    advance(waiting.atom, waiting.id, message['𝑛']);
   }
-  const b = message['𝑏'];
-  const sum = hex(number(`${b}.ρ`) + number(`${b}.x`));
-  process.stdout.write(`${JSON.stringify({
-    id: message.id,
-    '𝑛': `Φ.number( φ ↦ Φ.bytes( φ ↦ ⟦ Δ ⤍ ${sum} ⟧ ) )`,
-  })}\n`);
 });
 ```
 
-Written this way, reading until its `stdin` closes, the same program runs once
-per fire and serves the whole run alike; only the registry entry decides.
+Every request is a coroutine there, so a question suspends the request that
+asked it rather than the program: whatever `phino` says next, the answer or
+another request, is served on the spot.
+
+A program may run a `phino` of its own instead of asking, and the `--inside`
+option is how it does that: the expression it names is bound to a fresh
+synthetic attribute of the input expression, which the run takes as the
+universe, normalized there, and then dataized.
+
+```bash
+$ phino dataize --atoms=atoms.json --inside='5.plus( 6 )' universe.phi
+40-26-00-00-00-00-00-00
+```
+
+Here `universe.phi` is the 𝜑-program the atom is being fired inside — the very
+text the program was told under `𝑒`, which it feeds back on `stdin`. That costs
+a process and a re-parse of the whole universe per operand, which is what the
+`ask` line is for.
 
 The `--inside` option cannot be combined with `--locator`, since it aims the
 run at the binding it mints itself. Both `dataize` and `morph` take `--atoms`
diff --git a/phino.cabal b/phino.cabal
--- a/phino.cabal
+++ b/phino.cabal
@@ -1,6 +1,6 @@
 cabal-version: 3.0
 name: phino
-version: 0.0.124
+version: 0.0.125
 license: MIT
 synopsis: Command-Line Manipulator of 𝜑-Calculus Expressions
 description: Please see the README on GitHub at <https://github.com/objectionary/phino#readme>
diff --git a/src/Atoms.hs b/src/Atoms.hs
--- a/src/Atoms.hs
+++ b/src/Atoms.hs
@@ -37,12 +37,23 @@
 -- 'λ' and the formation under '𝑏', answered by a line with the same 'id' and
 -- the 𝜑-expression under '𝑛'.
 --
+-- The channel carries questions as well as answers. An operand reaches a
+-- program unreduced, since reducing it may take the very atom being fired, so
+-- instead of running a phino of its own on the universe with the operand
+-- spliced into its text, a program writes a line of its own: an 'id' it minted
+-- and, under 'ask', the 𝜑-expression it wants reduced. phino reduces it by
+-- re-entering its own evaluator and answers with that 'id' and the reduced
+-- expression under '𝑛'. Only a program kept for the run may ask: the stdin of
+-- one started for the fire is closed behind its request, so there is nothing
+-- left to answer it over.
+--
 -- A name no key matches has no λ function at all: 𝔼 gets stuck on it, exactly
 -- as it does for a name no one ever declared (see 'Stuck' in 'Dataize').
 module Atoms
   ( Atom (..)
   , AtomException (..)
   , Program (..)
+  , ReduceFunc
   , Registry
   , Runtime (..)
   , Session (_program)
@@ -57,7 +68,7 @@
 
 import AST
 import Control.Concurrent.MVar (MVar, modifyMVar, modifyMVar_, newMVar)
-import Control.Exception (Exception, SomeException, catch, onException, throwIO, try)
+import Control.Exception (Exception, catch, onException, throwIO)
 import Control.Monad (foldM, unless)
 import Data.Aeson (FromJSON (parseJSON), eitherDecodeStrict', object, withObject, withText, (.!=), (.:), (.:?), (.=))
 import qualified Data.Aeson as A
@@ -69,6 +80,7 @@
 import qualified Data.ByteString as BS
 import qualified Data.ByteString.Char8 as BC
 import qualified Data.ByteString.Lazy as BSL
+import Data.IORef (IORef, atomicModifyIORef', newIORef, readIORef, writeIORef)
 import Data.List (find, intercalate)
 import Data.Map.Strict (Map)
 import qualified Data.Map.Strict as Map
@@ -136,17 +148,34 @@
 -- A program while it runs: its streams, the file its complaints go to, the
 -- file its script is staged in, if it is a script, the universe it was told
 -- last, so it is told again only when the universe changes, and how many
--- requests it has been asked, which numbers the next one.
+-- requests it has been asked, which numbers the next one. The last two are
+-- mutable, since a fire may nest: serving a question of the program takes an
+-- evaluator that fires atoms of its own, and the one it reaches may be this
+-- very program, asked again over these very handles while its question is
+-- still open.
 data Running = Running
   { _input :: Handle
   , _output :: Handle
   , _process :: ProcessHandle
   , _complaints :: FilePath
   , _staged :: Maybe FilePath
-  , _told :: Maybe Expression
-  , _requests :: Int
+  , _told :: IORef (Maybe Expression)
+  , _requests :: IORef Int
   }
 
+-- What is left of the channel to a program once its request is pushed through:
+-- the stdin of a program started for the fire is closed behind the request,
+-- since the program may read its input whole before it answers, so nothing
+-- more can be said to it; the stdin of one kept for the run is flushed and
+-- stays open, so its questions can be answered.
+data Channel = Closed | Open
+
+-- How phino reduces a 𝜑-expression a program asks about. Only the caller of
+-- 'fireAtom' can do it, since it alone holds the universe to reduce inside and
+-- the context to reduce under, so it hands the way down (see 'reduction' in
+-- 'Dataize').
+type ReduceFunc = Expression -> IO Expression
+
 -- One entry of the registry, as the file spells it: the program and whether
 -- it is to be kept for the run.
 data Entry = Entry Program Bool
@@ -168,8 +197,8 @@
   | -- The program exited with a non-zero status; the message carries its stderr.
     AtomBroke T.Text Int String
   | -- The program said nothing phino can use: its reply is not a JSON object,
-    -- carries no 𝜑-expression, answers another request, or the 𝜑-expression
-    -- does not parse.
+    -- carries no 𝜑-expression, answers another request, asks a question phino
+    -- has no channel left to answer, or the 𝜑-expression does not parse.
     AtomMute T.Text String String
   deriving anyclass (Exception)
 
@@ -226,15 +255,24 @@
 instance FromJSON Entry where
   parseJSON value = Entry <$> parseJSON value <*> withObject "atom" (\entry -> entry .:? "serve" .!= False) value
 
--- What a program writes back for one request: the 'id' of the request it
--- answers and, under '𝑛', the 𝜑-expression the atom answers with.
-data Reply = Reply Int T.Text
+-- What a program writes back: the answer to the request it was asked, the
+-- 𝜑-expression under '𝑛', or a question of its own, the 𝜑-expression under
+-- 'ask' that it needs reduced before it can answer. An answer echoes the 'id'
+-- of the request it answers, a question mints an 'id' of its own, which phino
+-- echoes back.
+data Said
+  = Answer Int T.Text
+  | Question Int T.Text
 
-instance FromJSON Reply where
-  parseJSON = withObject "reply" $ \reply -> do
-    number <- reply .: "id"
-    raw <- reply .:? "𝑛"
-    maybe (fail "there is no '𝑛' in it") (pure . Reply number) raw
+instance FromJSON Said where
+  parseJSON = withObject "reply" $ \said -> do
+    number <- said .: "id"
+    answer <- said .:? "𝑛"
+    question <- said .:? "ask"
+    case (answer, question) of
+      (Just raw, _) -> pure (Answer number raw)
+      (Nothing, Just raw) -> pure (Question number raw)
+      (Nothing, Nothing) -> fail "there is neither '𝑛' nor 'ask' in it"
 
 -- No λ function at all: every atom gets stuck. This is what a run without
 -- '--atoms' fires against.
@@ -322,28 +360,28 @@
     dismissed (Resident Session{..}) = modifyMVar_ _running (maybe (pure Nothing) (\running -> Nothing <$ stopped briefly running))
     dismissed _ = pure ()
 
--- Fire the λ function 'func' by asking its program. A transient program is
--- started for the fire and waited for once it has answered, so that its exit
--- status has its say; a resident one is started on the first fire and stays
--- for the run, kept whatever the fire ended with, so that 'closeRegistry'
--- finds it. Whichever way, the 𝜑-expression the program answers with becomes
--- the atom's raw result, which 𝔼 normalizes exactly as it normalized the
--- answer of a built-in one.
-fireAtom :: T.Text -> Atom -> Expression -> Expression -> IO Expression
-fireAtom func (Transient program) form univ = do
+-- Fire the λ function 'func' by asking its program, reducing with 'reduce'
+-- whatever the program asks about on the way. A transient program is started
+-- for the fire and waited for once it has answered, so that its exit status
+-- has its say; a resident one is started on the first fire and stays for the
+-- run, whatever the fire ended with, so that 'closeRegistry' finds it. The
+-- session is let go of before the program is spoken to, since serving a
+-- question may fire the same atom again and a fire waiting for the session it
+-- is already inside would wait forever. Whichever way, the 𝜑-expression the
+-- program answers with becomes the atom's raw result, which 𝔼 normalizes
+-- exactly as it normalized the answer of a built-in one.
+fireAtom :: T.Text -> Atom -> Expression -> Expression -> ReduceFunc -> IO Expression
+fireAtom func (Transient program) form univ reduce = do
   running <- started func program
-  (_, answer) <- asked func running form univ hClose `onException` stopped patiently running
+  answer <- asked func running form univ Closed reduce `onException` stopped patiently running
   (status, complaint) <- stopped patiently running
   unless (null complaint) (logDebug (printf "Atom '%s' wrote to stderr: %s" (T.unpack func) complaint))
   case status of
     ExitFailure code -> throwIO (AtomBroke func code complaint)
     ExitSuccess -> pure answer
-fireAtom func (Resident Session{..}) form univ = do
-  outcome <- modifyMVar _running $ \current -> do
-    running <- maybe (started func _program) pure current
-    attempt <- try (asked func running form univ hFlush) :: IO (Either SomeException (Running, Expression))
-    pure (Just (either (const running) fst attempt), snd <$> attempt)
-  either throwIO pure outcome
+fireAtom func (Resident Session{..}) form univ reduce = do
+  running <- modifyMVar _running (\current -> (\kept -> (Just kept, kept)) <$> maybe (started func _program) pure current)
+  asked func running form univ Open reduce
 
 -- Start the program, with its input and its output on pipes and its complaints
 -- in a file that lives as long as the process does: a script is staged in a
@@ -358,7 +396,7 @@
   (executable, arguments, staged) <- commanded dir
   logDebug (printf "Starting atom '%s' as '%s'" (T.unpack func) (unwords (executable : arguments)))
   (input, output, process) <- spawned executable arguments handle `onException` discarded complaints staged
-  pure (Running input output process complaints staged Nothing 0)
+  Running input output process complaints staged <$> newIORef Nothing <*> newIORef 0
   where
     -- The command line the program is started with, and the file staged for
     -- it, if it is a script.
@@ -383,28 +421,58 @@
     missing executable failure = throwIO (NoRuntime func executable (show failure))
 
 -- Ask the running program to fire the λ function: it is told the universe,
--- unless it was told already, then the request, and its reply is read back.
--- How the request is pushed through is the caller's: a transient program has
--- its stdin closed behind it, since it may read its input whole before it
--- answers, a resident one has it flushed, since it reads on. A reply that is
--- not JSON, carries no '𝑛', answers another request, or a program that hangs
--- up fails the fire, with the program's stderr in the message.
-asked :: T.Text -> Running -> Expression -> Expression -> (Handle -> IO ()) -> IO (Running, Expression)
-asked func running@Running{..} form univ pushed = do
-  let number = _requests + 1
-      universe = lined (object ["𝑒" .= rendered univ])
-      request = lined (object ["id" .= number, "λ" .= func, "𝑏" .= rendered form])
+-- unless it was told already, then the request, and its lines are read back
+-- until it answers. A line carrying '𝑛' with the 'id' of the request is the
+-- answer; a line carrying 'ask' is a question of the program's own, which
+-- phino reduces and replies to before it goes on reading. What is left of the
+-- channel is the caller's: a transient program has its stdin closed behind the
+-- request, since it may read its input whole before it answers, a resident one
+-- has it flushed, since it reads on. A reply that is not JSON, carries neither
+-- '𝑛' nor 'ask', answers another request, or a program that hangs up fails the
+-- fire, with the program's stderr in the message.
+asked :: T.Text -> Running -> Expression -> Expression -> Channel -> ReduceFunc -> IO Expression
+asked func Running{..} form univ channel reduce = do
+  number <- atomicModifyIORef' _requests (\spent -> (spent + 1, spent + 1))
+  told <- readIORef _told
   logDebug (printf "Asking atom '%s' as request %d" (T.unpack func) number)
-  said (if _told == Just univ then request else universe <> request)
-  reply <- BC.hGetLine _output `catch` hungUp
-  answer <- replied number reply
-  pure (running{_told = Just univ, _requests = number}, answer)
+  said (if told == Just univ then request number else universe <> request number)
+  writeIORef _told (Just univ)
+  heard number
   where
+    universe :: BS.ByteString
+    universe = lined (object ["𝑒" .= rendered univ])
+    request :: Int -> BS.ByteString
+    request number = lined (object ["id" .= number, "λ" .= func, "𝑏" .= rendered form])
+    -- Read the program's lines until it answers the request phino asked,
+    -- serving every question it asks on the way.
+    heard :: Int -> IO Expression
+    heard number = do
+      reply <- BC.hGetLine _output `catch` hungUp
+      case eitherDecodeStrict' reply of
+        Left failure -> throwIO (AtomMute func (spoken reply) failure)
+        Right (Answer echoed raw)
+          | echoed /= number -> throwIO (AtomMute func (spoken reply) (printf "it answers request %d, while phino asked request %d" echoed number))
+          | otherwise -> either (throwIO . AtomMute func (T.unpack raw)) pure (parseExpression (T.unpack raw))
+        Right (Question minted raw) -> served minted raw >> heard number
+    -- Reduce the 𝜑-expression the program asks about and say it back under
+    -- '𝑛', with the 'id' the question minted. A program started for the fire
+    -- has nothing to be answered over, since phino closed its stdin behind the
+    -- request, so its question fails the fire instead of hanging it.
+    served :: Int -> T.Text -> IO ()
+    served minted raw = case channel of
+      Closed -> throwIO (AtomMute func (T.unpack raw) "it asks phino to reduce an expression, while its stdin is closed, since its entry does not say 'serve'")
+      Open -> do
+        logDebug (printf "Atom '%s' asks phino to reduce '%s' as question %d" (T.unpack func) (T.unpack raw) minted)
+        target <- either (unreadable raw) pure (parseExpression (T.unpack raw))
+        answer <- reduce target
+        said (lined (object ["id" .= minted, "𝑛" .= rendered answer]))
+    unreadable :: T.Text -> String -> IO a
+    unreadable raw failure = throwIO (AtomMute func (T.unpack raw) (printf "it asks phino to reduce an expression that does not parse: %s" failure))
     -- A program that has died leaves the write with nobody to drain it. The
     -- failure worth reporting is the one the program made, so a broken pipe is
     -- swallowed here and the read that follows finds out.
     said :: BS.ByteString -> IO ()
-    said content = (BS.hPut _input content >> pushed _input) `catch` unheard
+    said content = (BS.hPut _input content >> pushed channel _input) `catch` unheard
     -- The program closed its stdout instead of answering: if it has quit with
     -- a failure, that is the failure; otherwise it went mute.
     hungUp :: IOError -> IO BS.ByteString
@@ -415,16 +483,13 @@
         Just (ExitFailure code) -> throwIO (AtomBroke func code complaint)
         Just ExitSuccess -> throwIO (AtomMute func "" (unwords ("the program quit without answering" : [complaint | not (null complaint)])))
         Nothing -> throwIO (AtomMute func "" (unwords ("the program closed its stdout without answering" : [complaint | not (null complaint)])))
-    -- Parse what the program said back: a JSON object answering this very
-    -- request, with the raw 𝜑-expression under '𝑛'.
-    replied :: Int -> BS.ByteString -> IO Expression
-    replied number reply = case eitherDecodeStrict' reply of
-      Left failure -> throwIO (AtomMute func (spoken reply) failure)
-      Right (Reply echoed raw)
-        | echoed /= number -> throwIO (AtomMute func (spoken reply) (printf "it answers request %d, while phino asked request %d" echoed number))
-        | otherwise -> case parseExpression (T.unpack raw) of
-            Left failure -> throwIO (AtomMute func (T.unpack raw) failure)
-            Right expr -> pure expr
+
+-- Push the request through the channel: closing the stdin of a program started
+-- for the fire is the cue a program reading its input whole waits for, while a
+-- program kept for the run reads on and needs no more than a flush.
+pushed :: Channel -> Handle -> IO ()
+pushed Closed = hClose
+pushed Open = hFlush
 
 -- Hang up on the program: close its stdin, which is its cue to quit, wait for
 -- it the given way and remove the files it was given, its complaints read
diff --git a/src/CLI/Parsers.hs b/src/CLI/Parsers.hs
--- a/src/CLI/Parsers.hs
+++ b/src/CLI/Parsers.hs
@@ -230,9 +230,10 @@
 
 -- The external face of the trick phino plays internally to reduce a
 -- sub-expression against a universe: prepend a synthetic binding holding it to
--- that universe and aim the locator at the binding. An atom script needs it to
--- reduce the parts of the formation it was given, so it does not have to splice
--- them into the text of the universe by hand.
+-- that universe and aim the locator at the binding. An atom script started for
+-- the fire needs it to reduce the parts of the formation it was given, so it
+-- does not have to splice them into the text of the universe by hand; one kept
+-- for the run asks phino over the channel it answers on instead (see 'Atoms').
 optInside :: Parser (Maybe String)
 optInside =
   optional
diff --git a/src/Dataize.hs b/src/Dataize.hs
--- a/src/Dataize.hs
+++ b/src/Dataize.hs
@@ -13,7 +13,7 @@
 module Dataize (morph, morph', dataize, dataize', insideUniverse, DataizeContext (..), DataizeException (..), Outcome (..), Steps (..), State, emptyState, execBuildTerm) where
 
 import AST
-import Atoms (Registry, fireAtom, registeredAtom)
+import Atoms (ReduceFunc, Registry, fireAtom, registeredAtom)
 import Builder (buildBytesThrows, buildExpressionThrows, contextualize)
 import Control.Exception (Exception, catch, throwIO, try)
 import Control.Monad (foldM, when)
@@ -95,11 +95,17 @@
     -- everything reduced before it already in place: the residual program that
     -- '_partial' turns into the 'Residual' outcome.
     StuckAt T.Text (NonEmpty Rewritten)
+  | -- An 'OutOfSteps' caught by a spine frame, carrying that frame's derivation
+    -- just like 'StuckAt': a term that never reduces is a stuck site too, so
+    -- '_partial' parks it and hands back the residual instead of failing hard
+    -- (#1078)
+    OutOfStepsAt Int (NonEmpty Rewritten)
   deriving anyclass (Exception)
 
 instance Show DataizeException where
   show (OutOfSteps limit) =
     printf "Dataization did not finish before reaching the limit of steps: --max-steps=%d" limit
+  show (OutOfStepsAt limit _) = show (OutOfSteps limit)
   show (Stuck func) = printf "Atom '%s' does not exist" (T.unpack func)
   show (StuckAt func _) = show (Stuck func)
 
@@ -153,20 +159,23 @@
     filled (BiVoid _) = False
     filled _ = True
 
--- Run one frame of the 𝕄/𝔻 spine, attaching its derivation to a stuck atom
--- escaping it. 'Stuck' is raised deep inside an atom, which knows nothing about
--- the chain, so the innermost spine frame it reaches is the one to record where
--- the derivation stopped: the head of that frame's chain is the working
--- expression with the stuck application intact and everything reduced before
--- it already in place. Outer frames see 'StuckAt' and let it pass, since their
--- chains are prefixes of that one; a side-computation running on a chain of its
--- own strips the chain off again (see 'unparked') before the signal reaches
--- the spine.
+-- Run one frame of the 𝕄/𝔻 spine, attaching its derivation to a stuck atom or
+-- an exhausted budget escaping it. 'Stuck' is raised deep inside an atom, which
+-- knows nothing about the chain, so the innermost spine frame it reaches is the
+-- one to record where the derivation stopped: the head of that frame's chain is
+-- the working expression with the stuck application intact and everything
+-- reduced before it already in place. The same holds for 'OutOfSteps': a term
+-- cycling through the universe is no more a failure of the chain than a missing
+-- atom is, and under '_partial' it deserves the same parked residual (#1078).
+-- Outer frames see the '…At' signals and let them pass, since their chains are
+-- prefixes of that one; a side-computation running on a chain of its own strips
+-- the chain off again (see 'unparked') before the signal reaches the spine.
 parking :: NonEmpty Rewritten -> IO a -> IO a
 parking seq action = action `catch` rethrow
   where
     rethrow :: DataizeException -> IO a
     rethrow (Stuck func) = throwIO (StuckAt func seq)
+    rethrow (OutOfSteps limit) = throwIO (OutOfStepsAt limit seq)
     rethrow failure = throwIO failure
 
 -- Strip the derivation off a stuck atom escaping a side-computation that ran
@@ -179,6 +188,7 @@
   where
     rethrow :: DataizeException -> IO a
     rethrow (StuckAt func _) = throwIO (Stuck func)
+    rethrow (OutOfStepsAt limit _) = throwIO (OutOfSteps limit)
     rethrow failure = throwIO failure
 
 -- The Morphing function 𝕄 maps normal forms to formations. It is ternary,
@@ -274,6 +284,9 @@
     Left (StuckAt _ seq) | _partial -> do
       residue <- locatedExpression _locator (fst (NE.head seq))
       walked residue seq emptyState
+    Left (OutOfStepsAt _ seq) | _partial -> do
+      residue <- locatedExpression _locator (fst (NE.head seq))
+      walked residue seq emptyState
     Left failure -> throwIO (failure :: DataizeException)
   where
     -- The answer 𝕄 reached, walked by '_deep' before it is handed back (see
@@ -406,7 +419,7 @@
     evaluated ctx state' (func, self) = case registeredAtom ctx._atoms func of
       Nothing -> pure Nothing
       Just registered -> do
-        answer <- fireAtom func registered self univ
+        answer <- fireAtom func registered self univ (reduction univ ctx)
         ctx._saveEval (Evaluation func self (Just answer))
         again <- fired answer univ state' ctx
         pure (Just (fromMaybe (answer, state') again))
@@ -430,6 +443,7 @@
   case result of
     Right ((bytes, seq), _state) -> pure (Dataized bytes, reverse seq)
     Left (StuckAt _ seq) | _partial -> pure (Residual (fst (NE.head seq)), reverse (NE.toList seq))
+    Left (OutOfStepsAt _ seq) | _partial -> pure (Residual (fst (NE.head seq)), reverse (NE.toList seq))
     Left failure -> throwIO (failure :: DataizeException)
 
 -- The Dataization function 𝔻 retrieves bytes from an expression. It is partial
@@ -642,6 +656,26 @@
     pure (ExFormation (BiTau attr normal : bds), aiming)
   _ -> throwIO (userError "Can't reduce an expression inside a universe which is not a formation")
 
+-- What phino answers a program that asks it to reduce a 𝜑-expression (see
+-- 'ReduceFunc' in 'Atoms'): the expression is bound to a synthetic attribute
+-- of the universe and dataized there, exactly the way the '--inside' option
+-- does it, so the bytes come back as a Δ formation — or, where an atom on the
+-- way could not fire and '_partial' parked it, the residual program instead.
+-- An operand reaches a program unreduced, since reducing it may take the very
+-- atom being fired, and before the channel carried questions the program had
+-- no way to ask: it had to splice the operand into the text of the universe
+-- and run a phino of its own on it (see #1160). The context is the one the
+-- fire descended with, so the step budget of the run bounds the nesting.
+reduction :: Expression -> DataizeContext -> ReduceFunc
+reduction univ ctx expr = do
+  (universe, aiming) <- insideUniverse expr univ ctx
+  (outcome, _) <- dataize universe aiming
+  pure (reduced outcome)
+  where
+    reduced :: Outcome -> Expression
+    reduced (Dataized bytes) = ExFormation [BiDelta bytes]
+    reduced (Residual residue) = residue
+
 -- phino implements no λ function of its own. Which atoms exist is a property of
 -- the object model being dataized, not of the calculus, so they come from the
 -- '--atoms' registry and run as external scripts (see 'Atoms'). A name the
@@ -654,7 +688,7 @@
 atom func self univ state ctx = case registeredAtom ctx._atoms func of
   Nothing -> throwIO (Stuck func)
   Just registered -> do
-    raw <- fireAtom func registered self univ
+    raw <- fireAtom func registered self univ (reduction univ ctx)
     pure (raw, state)
 
 -- Augment the injected, context-free term builder with the dataization and
diff --git a/src/XMIR.hs b/src/XMIR.hs
--- a/src/XMIR.hs
+++ b/src/XMIR.hs
@@ -194,9 +194,9 @@
   ExRoot -> programToXMIR expr ctx
   _ -> throwIO (UnsupportedTopExpression expr)
 -- The top of a '--partial' residual and the result of 'merge' are arbitrary
--- formations: several τ/λ bindings, voids and a bound ρ. Every binding such a
--- formation carries becomes a child of <object>; 'xmirToPhi' reads the list
--- back (#1076)
+-- formations: several τ/λ bindings, voids and a bound ρ. The schema allows a
+-- single <o> under <object>, so the formation goes beneath one attribute-free
+-- <o> whose children are its bindings; 'xmirToPhi' reads that shape back (#1076)
 expressionToXMIR expr@(ExFormation bds) ctx =
   documentWith ctx [] expr rootNodes
   where
@@ -204,7 +204,7 @@
     rootNodes = do
       roots <- nestedBindings bds ctx
       unless (any isElement roots) (throwIO (UnsupportedTopExpression expr))
-      pure roots
+      pure [object [] roots]
     isElement :: Node -> Bool
     isElement (NodeElement _) = True
     isElement _ = False
@@ -441,11 +441,9 @@
         NodeElement el
           | nameLocalName (elementName el) == "object" -> do
               unless (null (strayNodes doc)) (throwIO (InvalidXMIRFormat "No processing instructions or bare text are allowed in <object>" doc))
-              bds <- case doc C.$/ C.element (toName "o") of
-                [] -> throwIO (InvalidXMIRFormat "Expected at least one <o> element in <object>" doc)
-                -- A residual document (printed by '--partial', #1076) carries
-                -- one <o> per binding of the stuck formation, so read them all
-                os -> uniqueBindings' =<< mapM (`xmirToFormationBinding` []) os
+              o <- case doc C.$/ C.element (toName "o") of
+                [single] -> pure single
+                _ -> throwIO (InvalidXMIRFormat "Expected single <o> element in <object>" doc)
               let pckg =
                     [ T.unpack t
                     | meta <- doc C.$/ C.element (toName "metas") C.&/ C.element (toName "meta")
@@ -454,15 +452,29 @@
                     , tail' <- meta C.$/ C.element (toName "tail") C.&/ C.content
                     , t <- T.splitOn "." tail'
                     ]
-              if null pckg
-                then pure (ExFormation (withVoidRho bds))
-                else case bds of
-                  [obj] ->
-                    let bd = foldr (\part acc -> BiTau (AtLabel (T.pack part)) (ExFormation [acc, BiLambda (Function "Package"), BiVoid AtRho])) obj pckg
-                     in pure (ExFormation [bd, BiVoid AtRho])
-                  _ -> throwIO (InvalidXMIRFormat "A <object> with <metas> package must hold a single <o>" doc)
+              -- An attribute-free <o> is a residual formation printed by
+              -- '--partial': its children are the bindings themselves (#1076)
+              if bareRoot o
+                then
+                  if null pckg
+                    then xmirToFormation o []
+                    else throwIO (InvalidXMIRFormat "A <object> with <metas> package must hold a named <o>" doc)
+                else
+                  if null pckg
+                    then do
+                      bd <- xmirToFormationBinding o []
+                      pure (ExFormation (withVoidRho [bd]))
+                    else do
+                      obj <- xmirToFormationBinding o []
+                      let bd = foldr (\part acc -> BiTau (AtLabel (T.pack part)) (ExFormation [acc, BiLambda (Function "Package"), BiVoid AtRho])) obj pckg
+                      pure (ExFormation [bd, BiVoid AtRho])
           | otherwise -> throwIO (InvalidXMIRFormat "Expected single <object> element" doc)
         _ -> throwIO (InvalidXMIRFormat "NodeElement is expected as root element" doc)
+
+-- The single <o> of a residual document carries no attributes of its own:
+-- it is the formation, not one of its bindings (#1076)
+bareRoot :: C.Cursor -> Bool
+bareRoot o = not (any (`hasAttr` o) ["name", "base", "as"])
 
 xmirToFormationBinding :: C.Cursor -> [String] -> IO Binding
 xmirToFormationBinding cur fqn
diff --git a/test/AtomsSpec.hs b/test/AtomsSpec.hs
--- a/test/AtomsSpec.hs
+++ b/test/AtomsSpec.hs
@@ -7,13 +7,14 @@
 module AtomsSpec (spec) where
 
 import AST
-import Atoms (Atom (..), Program (..), Registry, Runtime (RtNode), Session (_program), closeRegistry, emptyRegistry, fireAtom, readRegistry, registeredAtom)
+import Atoms (Atom (..), Program (..), ReduceFunc, Registry, Runtime (RtNode), Session (_program), closeRegistry, emptyRegistry, fireAtom, readRegistry, registeredAtom)
 import Control.Exception (SomeException, finally)
 import Control.Monad (forM_)
 import Data.Aeson (Value, object, (.=))
 import Data.Aeson.Key qualified as Key
 import Data.Aeson.Types (Pair)
 import Data.ByteString qualified as BS
+import Data.IORef (IORef, newIORef, readIORef, writeIORef)
 import Data.List (isInfixOf)
 import Data.Text qualified as T
 import Data.Text.Encoding (encodeUtf8)
@@ -62,13 +63,28 @@
   withExecutable (resident snippet) $ \file ->
     withRegistered (registryOf names (served (executing file))) action
 
+-- What phino answers a program that asks it to reduce an expression: reducing
+-- one is 'Dataize's business and not this module's, so every question is
+-- answered here with the same bytes
+reducing :: ReduceFunc
+reducing _ = parseExpressionThrows "⟦ Δ ⤍ 2A- ⟧"
+
+-- The same, keeping the expression it was asked about, so a case may assert on
+-- what reached phino
+recording :: IORef (Maybe Expression) -> ReduceFunc
+recording seen expr = writeIORef seen (Just expr) >> reducing expr
+
 -- Fire the given λ function out of the registry, against the same formation
 -- 'fired' uses, inside the given universe
 firedFrom :: Registry -> T.Text -> String -> IO Expression
-firedFrom registry func universe = do
+firedFrom registry func universe = firedFrom' registry func universe reducing
+
+-- The same, with phino reducing whatever the program asks about the given way
+firedFrom' :: Registry -> T.Text -> String -> ReduceFunc -> IO Expression
+firedFrom' registry func universe reduce = do
   form <- parseExpressionThrows "⟦ x ↦ ⟦ Δ ⤍ 01- ⟧ ⟧"
   univ <- parseExpressionThrows universe
-  maybe (fail (printf "'%s' is not registered" (T.unpack func))) (\atom -> fireAtom func atom form univ) (registeredAtom registry func)
+  maybe (fail (printf "'%s' is not registered" (T.unpack func))) (\atom -> fireAtom func atom form univ reduce) (registeredAtom registry func)
 
 -- Fire the λ function 'L_answer' out of the given atom, against a formation
 -- binding 'x' inside a universe binding 'y'
@@ -76,7 +92,7 @@
 fired atom = do
   form <- parseExpressionThrows "⟦ x ↦ ⟦ Δ ⤍ 01- ⟧ ⟧"
   univ <- parseExpressionThrows "⟦ y ↦ ⟦ Δ ⤍ 02- ⟧ ⟧"
-  fireAtom "L_answer" atom form univ
+  fireAtom "L_answer" atom form univ reducing
 
 -- The program a λ function is kept for the run with, if it is kept at all
 kept :: Maybe Atom -> Maybe Program
@@ -156,6 +172,41 @@
 replying :: T.Text -> T.Text
 replying bytes = "printf '{\"id\": %s, \"𝑛\": \"⟦ Δ ⤍ %s ⟧\"}\\n' \"$id\" \"" <> bytes <> "\""
 
+-- A resident program that cannot answer its request before phino reduces
+-- something for it: it asks about the given 𝜑-expression under the question
+-- 'id' 7, then answers with 'FF-' when what phino said back matches the given
+-- shell pattern and with '00-' when it does not
+asking :: T.Text -> T.Text -> T.Text
+asking expr pattern =
+  T.unlines
+    [ "printf '{\"id\": 7, \"ask\": \"" <> expr <> "\"}\\n'"
+    , "IFS= read -r reply"
+    , "case \"$reply\" in"
+    , "  " <> pattern <> ") " <> replying "FF-" <> ";;"
+    , "  *) " <> replying "00-" <> ";;"
+    , "esac"
+    ]
+
+-- A resident program whose question phino cannot answer without firing the
+-- same program again: it asks, then serves every request phino sends while its
+-- question is open, and answers its own request once the answer to the
+-- question arrives, telling phino whether that answer carried '2A-'
+nesting :: T.Text
+nesting =
+  T.unlines
+    [ "printf '{\"id\": 7, \"ask\": \"Q.x\"}\\n'"
+    , "while IFS= read -r reply; do"
+    , "  case \"$reply\" in"
+    , "    *'\"λ\"'*) printf '{\"id\": %s, \"𝑛\": \"⟦ Δ ⤍ 2A- ⟧\"}\\n' \"$(printf '%s' \"$reply\" | sed 's/.*\"id\":\\([0-9]*\\).*/\\1/')\";;"
+    , "    *) break;;"
+    , "  esac"
+    , "done"
+    , "case \"$reply\" in"
+    , "  *'2A-'*) " <> replying "FF-" <> ";;"
+    , "  *) " <> replying "00-" <> ";;"
+    , "esac"
+    ]
+
 spec :: Spec
 spec = do
   -- phino implements no λ function, so an empty registry is what a run without
@@ -461,6 +512,45 @@
 
     it "fails when the resident program writes something other than JSON" $
       refuses "echo almost" ["L_answer", "almost"]
+
+    -- An operand reaches a program unreduced, since reducing it may take the
+    -- very atom being fired, so the program asks phino for it over the channel
+    -- it answers on, instead of running a phino of its own
+    it "answers the question a resident program asks with what phino reduced" $
+      serves (asking "Q.x" "*'2A-'*") "⟦ Δ ⤍ FF- ⟧"
+
+    -- A question mints an 'id' of its own, which phino echoes, so a program
+    -- that has several of them open tells the answers apart
+    it "echoes in its answer the 'id' the question minted" $
+      serves (asking "Q.x" "*'\"id\":7'*") "⟦ Δ ⤍ FF- ⟧"
+
+    it "hands the 𝜑-expression of the question over to be reduced" $
+      withShell $
+        withServed ["L_answer"] (asking "⟦ z ↦ ⟦ Δ ⤍ 03- ⟧ ⟧" "*'2A-'*") $ \registry -> do
+          seen <- newIORef Nothing
+          _ <- firedFrom' registry "L_answer" "⟦ y ↦ ⟦ Δ ⤍ 02- ⟧ ⟧" (recording seen)
+          wanted <- parseExpressionThrows "⟦ z ↦ ⟦ Δ ⤍ 03- ⟧ ⟧"
+          readIORef seen `shouldReturn` Just wanted
+
+    -- Serving a question re-enters the evaluator, which fires atoms of its
+    -- own, and one of them may be the very atom that asked: that request
+    -- reaches the same program, over the same handles, while its question is
+    -- still open
+    it "fires the same program again while its question is open" $
+      withShell $
+        withServed ["L_answer"] nesting $ \registry -> do
+          answer <- firedFrom' registry "L_answer" "⟦ y ↦ ⟦ Δ ⤍ 02- ⟧ ⟧" (const (firedFrom registry "L_answer" "⟦ y ↦ ⟦ Δ ⤍ 02- ⟧ ⟧"))
+          wanted <- parseExpressionThrows "⟦ Δ ⤍ FF- ⟧"
+          answer `shouldBe` wanted
+
+    it "fails when what a resident program asks about is not a 𝜑-expression" $
+      refuses "printf '{\"id\": 7, \"ask\": \"⟦ ⟧⟧\"}\\n'" ["L_answer", "does not parse"]
+
+    -- The stdin of a program started for the fire is closed behind its
+    -- request, since it may read its input whole before it answers, so there
+    -- is nothing left to answer a question of its own over
+    it "fails when a script started for the fire asks a question" $
+      fails "process.stdout.write(JSON.stringify({id: 7, ask: 'Q.x'}))" ["L_answer", "serve"]
 
   describe "closeRegistry" $ do
     -- The program is told to quit by its stdin closing, which its read loop
diff --git a/test/CLISpec.hs b/test/CLISpec.hs
--- a/test/CLISpec.hs
+++ b/test/CLISpec.hs
@@ -16,7 +16,7 @@
 import Data.Time.Clock (addUTCTime, getCurrentTime)
 import Data.Time.Clock.POSIX (getPOSIXTime)
 import Data.Version (showVersion)
-import Fixtures (withFixtureRegistry, withNode, withServing, withShell)
+import Fixtures (withAskingRegistry, withFixtureRegistry, withNode, withServing, withShell)
 import GHC.IO.Handle
 import Paths_phino (version)
 import System.Directory (createDirectoryIfMissing, doesDirectoryExist, doesFileExist, getTemporaryDirectory, listDirectory, removeDirectoryRecursive, removeFile, removePathForcibly, setModificationTime)
@@ -128,6 +128,11 @@
 withAtoms :: (String -> Expectation) -> Expectation
 withAtoms action = withNode (withFixtureRegistry (action . ("--atoms=" ++)))
 
+-- The same, for the fixture that reduces no operand of its own and asks phino
+-- for every one of them instead (see 'Fixtures')
+withAsking :: (String -> Expectation) -> Expectation
+withAsking action = withNode (withAskingRegistry (action . ("--atoms=" ++)))
+
 testCLIFailed :: [String] -> [String] -> Expectation
 testCLIFailed args outputs = testCLI' args outputs (Left (ExitFailure 1))
 
@@ -1108,6 +1113,15 @@
             ["dataize", atoms, "--max-steps=40"]
             ["[ERROR]: Dataization did not finish before reaching the limit of steps: --max-steps=40"]
 
+    -- Under '--partial' the same term does not fail: the spent budget is a
+    -- stuck site too, and the run ends on the residual the spine reached (#1078)
+    it "parks --max-steps on a residual with --partial" $
+      withAtoms $ \atoms ->
+        withStdin "⟦ @ ↦ ⟦ λ ⤍ L_number_div, ρ ↦ ⟦ Δ ⤍ 40-45-00-00-00-00-00-00 ⟧, x ↦ ⟦ Δ ⤍ 40-00-00-00-00-00-00-00 ⟧ ⟧ ⟧" $
+          testCLISucceeded
+            ["dataize", atoms, "--max-steps=40", "--partial", "--flat"]
+            ["Φ.number( φ ↦ Φ.bytes( φ ↦ ⟦ Δ ⤍ 40-35-00-00-00-00-00-00"]
+
     it "dataizes with --sequence" $
       withStdin "[[ @ -> [[ x -> [[ D> 01-, y -> ? ]](y -> [[ ]]) ]].x ]]" $
         testCLISucceeded
@@ -1322,6 +1336,16 @@
           withStdin sum' $
             testCLIFailed ["dataize", "--atoms=" ++ path] ["cannot be read"]
 
+      -- An operand reaches an atom as it was written, so 'x' arrives here as
+      -- '6.plus( 7 )': a program that needs it reduced asks phino for it over
+      -- the very channel it answers on, and serving that question costs
+      -- another fire of the same program, which arrives while the question is
+      -- still open
+      it "reduces the operand a program asks it about" $
+        withAsking $ \atoms ->
+          withStdin "[[ bytes ↦ ⟦ φ ↦ ∅ ⟧, number(φ) -> [[ plus(x) -> [[ L> L_number_plus ]] ]], @ -> 5.plus(6.plus(7)) ]]" $
+            testCLISucceeded ["dataize", atoms] ["40-32-00-00-00-00-00-00"]
+
     -- An atom script cannot reduce the operands it was handed by itself, so it
     -- asks phino for them: '--inside' binds an expression to a synthetic
     -- attribute of the universe and aims the run at it
@@ -1515,6 +1539,14 @@
         testCLIFailed
           ["morph", "--locator=Q.@", "--max-steps=3"]
           ["[ERROR]: Dataization did not finish before reaching the limit of steps: --max-steps=3"]
+
+    -- '--partial' parks a spent 𝕄 budget the same way it parks a stuck atom:
+    -- the answer is the term the walk had reached, dispatch intact (#1078)
+    it "parks the spent budget as a residual with --partial" $
+      withStdin "⟦ φ ↦ 5.gt(Φ.nan) ⟧" $
+        testCLISucceeded
+          ["morph", "--locator=Q.@", "--max-steps=10", "--partial", "--flat", "--hide-rho", "--sweet"]
+          ["5.gt( Φ.nan )"]
 
     -- 𝕄 never fires a bare λ-formation, so only the atoms sitting under a
     -- dispatch ('ml') can get stuck; '--partial' parks them exactly as under 𝔻
diff --git a/test/DataizeSpec.hs b/test/DataizeSpec.hs
--- a/test/DataizeSpec.hs
+++ b/test/DataizeSpec.hs
@@ -574,12 +574,23 @@
   -- through md → ma → universe → mf → mphi → ml forever and no CLI option could
   -- stop it (#1052). '--max-steps' bounds that recursion and fails once the
   -- budget is gone.
-  describe "stops a dataization that never reaches bytes" $
+  describe "stops a dataization that never reaches bytes" $ do
     it "fails on the step limit instead of morphing forever" $
       withNode $ do
         expr <- parseExpressionThrows "⟦ @ ↦ ⟦ λ ⤍ L_number_div, ρ ↦ ⟦ Δ ⤍ 40-45-00-00-00-00-00-00 ⟧, x ↦ ⟦ Δ ⤍ 40-00-00-00-00-00-00-00 ⟧ ⟧ ⟧"
         dataize expr (DataizeContext ExRoot 25 25 (Steps 40 0) False True False False registry buildTerm dontSaveStep dontSaveEval)
           `shouldThrow` (\e -> "--max-steps=40" `isInfixOf` show (e :: SomeException))
+
+    -- A budget spent on a cycle is a stuck site just as an atom that cannot
+    -- fire is: under '_partial' the run ends on the residual the spine had
+    -- reached instead of failing hard (#1078)
+    it "parks the step limit as a residual with --partial" $
+      withNode $ do
+        expr <- parseExpressionThrows "⟦ @ ↦ ⟦ λ ⤍ L_number_div, ρ ↦ ⟦ Δ ⤍ 40-45-00-00-00-00-00-00 ⟧, x ↦ ⟦ Δ ⤍ 40-00-00-00-00-00-00-00 ⟧ ⟧ ⟧"
+        (outcome, _) <- dataize expr (DataizeContext ExRoot 25 25 (Steps 40 0) False True True False registry buildTerm dontSaveStep dontSaveEval)
+        case outcome of
+          Residual _ -> pure ()
+          Dataized bts -> expectationFailure ("expected a residual, dataized to " ++ show bts)
 
   -- An atom phino does not know — a name the '--atoms' registry does not carry,
   -- such as the placeholder ⟦ λ ⤍ Sym_arg_0 ⟧ standing in for a data input
diff --git a/test/Fixtures.hs b/test/Fixtures.hs
--- a/test/Fixtures.hs
+++ b/test/Fixtures.hs
@@ -6,13 +6,15 @@
 -- The λ functions the specs fire. phino implements none of them, so a spec that
 -- needs an atom to answer brings its own: one JavaScript fixture,
 -- 'test-resources/atoms/primitives.js', registered under every name in
--- 'fixtureAtoms' and branching on the one each request names under 'λ', or a
--- POSIX shell script written for the occasion, either run once per fire or
--- kept resident for the run.
+-- 'fixtureAtoms' and branching on the one each request names under 'λ',
+-- another, 'test-resources/atoms/asking.js', which reduces nothing itself and
+-- asks phino for its operands, or a POSIX shell script written for the
+-- occasion, either run once per fire or kept resident for the run.
 module Fixtures
   ( fixtureAtoms
   , fixtureRegistry
   , resident
+  , withAskingRegistry
   , withExecutable
   , withFixtureRegistry
   , withNode
@@ -50,10 +52,10 @@
   , "L_bytes_not"
   ]
 
--- The fixture script itself, read as UTF-8 rather than through the locale,
--- since it spells 𝜑 expressions.
-fixtureScript :: IO T.Text
-fixtureScript = decodeUtf8 <$> BS.readFile "test-resources/atoms/primitives.js"
+-- One of the fixture scripts, read as UTF-8 rather than through the locale,
+-- since they spell 𝜑 expressions.
+fixtureScript :: FilePath -> IO T.Text
+fixtureScript name = decodeUtf8 <$> BS.readFile ("test-resources/atoms/" ++ name)
 
 -- The registry the specs that drive 'Dataize' directly run against: the same
 -- file '--atoms' reads, read once and gone.
@@ -64,11 +66,23 @@
 -- removed afterwards, for the specs that go through the command line.
 withFixtureRegistry :: (FilePath -> IO a) -> IO a
 withFixtureRegistry action = do
-  script <- fixtureScript
+  script <- fixtureScript "primitives.js"
   withRegistryOf (object [Key.fromText name .= entry script | name <- fixtureAtoms]) action
   where
     entry :: T.Text -> Value
     entry script = object ["rt" .= ("node" :: T.Text), "script" .= script]
+
+-- The registry of the one λ function the asking fixture answers,
+-- 'L_number_plus', kept for the run, as the JSON file '--atoms' reads: a
+-- program may ask phino to reduce an operand only while its stdin is open, and
+-- phino closes the stdin of a program started for the fire behind its request.
+withAskingRegistry :: (FilePath -> IO a) -> IO a
+withAskingRegistry action = do
+  script <- fixtureScript "asking.js"
+  withRegistryOf (object ["L_number_plus" .= entry script]) action
+  where
+    entry :: T.Text -> Value
+    entry script = object ["rt" .= ("node" :: T.Text), "script" .= script, "serve" .= True]
 
 -- The given JSON, as the registry file '--atoms' reads, in a temporary file
 -- removed afterwards.
