packages feed

phino 0.0.124 → 0.0.125

raw patch · 10 files changed

+446/−141 lines, 10 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- Atoms: instance Data.Aeson.Types.FromJSON.FromJSON Atoms.Reply
+ Atoms: instance Data.Aeson.Types.FromJSON.FromJSON Atoms.Said
+ Atoms: type ReduceFunc = Expression -> IO Expression
+ Dataize: OutOfStepsAt :: Int -> NonEmpty Rewritten -> DataizeException
- Atoms: fireAtom :: Text -> Atom -> Expression -> Expression -> IO Expression
+ Atoms: fireAtom :: Text -> Atom -> Expression -> Expression -> ReduceFunc -> IO Expression

Files

README.md view
@@ -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`
phino.cabal view
@@ -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>
src/Atoms.hs view
@@ -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
src/CLI/Parsers.hs view
@@ -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
src/Dataize.hs view
@@ -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
src/XMIR.hs view
@@ -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
test/AtomsSpec.hs view
@@ -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
test/CLISpec.hs view
@@ -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 𝔻
test/DataizeSpec.hs view
@@ -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
test/Fixtures.hs view
@@ -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.