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 +84/−38
- phino.cabal +1/−1
- src/Atoms.hs +122/−57
- src/CLI/Parsers.hs +4/−3
- src/Dataize.hs +46/−12
- src/XMIR.hs +28/−16
- test/AtomsSpec.hs +94/−4
- test/CLISpec.hs +33/−1
- test/DataizeSpec.hs +12/−1
- test/Fixtures.hs +22/−8
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.