Helpful library updates
This commit is contained in:
@@ -2,45 +2,27 @@
|
||||
!import "../../lib/io.tri" !Local
|
||||
!import "../../lib/socket.tri" !Local
|
||||
|
||||
-- Preserve the host-driver Result shape on error, run okCase on success.
|
||||
onOk = action okCase :
|
||||
bind action (result :
|
||||
matchResult
|
||||
(err rest : pure result)
|
||||
okCase
|
||||
result)
|
||||
|
||||
-- Convenience: print a string and continue.
|
||||
printLn = s : bind (putStr (append s "\n")) (_ : pure t)
|
||||
|
||||
-- Main accept+echo loop. Recursion via y.
|
||||
echoLoop = y (self server :
|
||||
bind (accept server) (acceptResult :
|
||||
matchResult
|
||||
(err rest :
|
||||
bind (printLn (append "accept error: " err)) (_ :
|
||||
echoLoop = y (self : server :
|
||||
withAccepted_ server
|
||||
(err :
|
||||
bind (putStrLn (append "accept error: " err)) (_ :
|
||||
self server))
|
||||
(accepted rest :
|
||||
matchPair
|
||||
(clientSock addr :
|
||||
bind (printLn (append "client from " addr)) (_ :
|
||||
bind (recv clientSock 4096) (msgResult :
|
||||
matchResult
|
||||
(err rest :
|
||||
bind (putStrLn (append "client from " addr)) (_ :
|
||||
onResult_ (recv clientSock 4096)
|
||||
(err :
|
||||
bind (closeSocket clientSock) (_ :
|
||||
self server))
|
||||
(msg rest :
|
||||
(msg :
|
||||
bind (send clientSock msg) (_ :
|
||||
bind (closeSocket clientSock) (_ :
|
||||
self server)))
|
||||
msgResult)))
|
||||
accepted)
|
||||
acceptResult))
|
||||
self server))))))
|
||||
|
||||
main = io (
|
||||
onOk socket (server rest :
|
||||
onOk (bindSocket server "127.0.0.1" 0) (_ rest :
|
||||
onOk (listen server 5) (_ rest :
|
||||
onOk (getSocketName server) (port rest :
|
||||
bind (printLn (append "Echo server listening on port " (showNumber port))) (_ :
|
||||
onOk_ socket (server :
|
||||
onOk_ (bindSocket server "127.0.0.1" 0) (_ :
|
||||
onOk_ (listen server 5) (_ :
|
||||
onOk_ (getSocketName server) (port :
|
||||
bind (putStrLn (append "Echo server listening on port " (showNumber port))) (_ :
|
||||
echoLoop server))))))
|
||||
|
||||
100
lib/base.tri
100
lib/base.tri
@@ -33,6 +33,15 @@ lOr = (triage
|
||||
|
||||
matchPair = a : triage _ _ a
|
||||
|
||||
fst = p : matchPair (a b : a) p
|
||||
snd = p : matchPair (a b : b) p
|
||||
|
||||
resultIsOk = result :
|
||||
matchResult (err rest : false) (val rest : true) result
|
||||
|
||||
resultIsErr = result :
|
||||
matchResult (err rest : true) (val rest : false) result
|
||||
|
||||
not? = matchBool false true
|
||||
and? = matchBool id (_ : false)
|
||||
|
||||
@@ -87,3 +96,94 @@ matchResult = (errCase okCase result :
|
||||
tag)
|
||||
payload)
|
||||
result)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Maybe / Option type
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
nothing = t
|
||||
just = x : t x
|
||||
|
||||
matchMaybe = (nothingCase justCase maybe :
|
||||
triage
|
||||
nothingCase
|
||||
justCase
|
||||
(_ _ : nothingCase)
|
||||
maybe)
|
||||
|
||||
maybe = default f m : matchMaybe default f m
|
||||
maybeMap = f m : matchMaybe nothing (x : just (f x)) m
|
||||
maybeBind = m f : matchMaybe nothing f m
|
||||
maybeOr = default m : matchMaybe default id m
|
||||
maybe? = matchMaybe false (_ : true)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Basic arithmetic
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
pred = y (self : triage
|
||||
0
|
||||
(_ : 0)
|
||||
(bit rest :
|
||||
matchBool
|
||||
(matchBool
|
||||
0
|
||||
(pair 0 rest)
|
||||
(equal? rest 0))
|
||||
(matchBool
|
||||
0
|
||||
(pair 1 (self rest))
|
||||
(equal? rest 0))
|
||||
bit))
|
||||
|
||||
isZero? = triage true (_ : false) (_ _ : false)
|
||||
|
||||
add = y (self x y :
|
||||
triage
|
||||
y
|
||||
(_ : succ y)
|
||||
(_ _ : succ (self (pred x) y))
|
||||
x)
|
||||
|
||||
sub = y (self a b :
|
||||
matchBool
|
||||
a
|
||||
(self (pred a) (pred b))
|
||||
(isZero? b))
|
||||
|
||||
lt? = a b : not? (isZero? (sub b a))
|
||||
lte? = a b : isZero? (sub a b)
|
||||
|
||||
mul = y (self a b :
|
||||
matchBool
|
||||
0
|
||||
(add a (self a (pred b)))
|
||||
(isZero? b))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Result combinators
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
mapResult = (f result :
|
||||
matchResult
|
||||
(code rest : err code rest)
|
||||
(value rest : ok (f value) rest)
|
||||
result)
|
||||
|
||||
bindResult = (result f :
|
||||
matchResult
|
||||
(code rest : err code rest)
|
||||
(value rest : f value rest)
|
||||
result)
|
||||
|
||||
resultOr = (default result :
|
||||
matchResult
|
||||
(_ _ : default)
|
||||
(value _ : value)
|
||||
result)
|
||||
|
||||
resultMapErr = (f result :
|
||||
matchResult
|
||||
(code rest : err (f code) rest)
|
||||
(value rest : ok value rest)
|
||||
result)
|
||||
|
||||
@@ -54,19 +54,65 @@ expectU8 = (expected bs :
|
||||
(byteEq? actual expected))
|
||||
(readU8 bs))
|
||||
|
||||
mapResult = (f result :
|
||||
matchResult
|
||||
(code rest : err code rest)
|
||||
(value rest : ok (f value) rest)
|
||||
result)
|
||||
|
||||
bindResult = (result f :
|
||||
matchResult
|
||||
(code rest : err code rest)
|
||||
(value rest : f value rest)
|
||||
result)
|
||||
|
||||
read2 = (bs : readBytes 2 bs)
|
||||
read4 = (bs : readBytes 4 bs)
|
||||
readU16BEBytes = (bs : read2 bs)
|
||||
readU32BEBytes = (bs : read4 bs)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Parser combinators
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
pureParser = value bs : ok value bs
|
||||
failParser = code bs : err code bs
|
||||
|
||||
mapParser = f p bs : mapResult f (p bs)
|
||||
bindParser = p f bs : bindResult (p bs) f
|
||||
thenParser = p q bs : bindResult (p bs) (_ : q)
|
||||
|
||||
orParser = (p q bs :
|
||||
matchResult
|
||||
(_ _ : q bs)
|
||||
(value rest : ok value rest)
|
||||
(p bs))
|
||||
|
||||
readWhile_ = y (self pred bs acc :
|
||||
matchResult
|
||||
(code rest : ok (reverse acc) bs)
|
||||
(value rest :
|
||||
matchBool
|
||||
(self pred rest (pair value acc))
|
||||
(ok (reverse acc) (pair value rest))
|
||||
(pred value))
|
||||
(readU8 bs))
|
||||
|
||||
readWhile = pred bs : readWhile_ pred bs t
|
||||
|
||||
readUntil = pred : readWhile (x : not? (pred x))
|
||||
|
||||
readRemaining = bs : ok bs t
|
||||
|
||||
peekU8 = (bs :
|
||||
matchResult
|
||||
(code rest : err code bs)
|
||||
(value rest : ok value bs)
|
||||
(readU8 bs))
|
||||
|
||||
eof? = (bs :
|
||||
matchBool
|
||||
(ok t bs)
|
||||
(err errUnexpectedEof bs)
|
||||
(emptyList? bs))
|
||||
|
||||
expectAscii = expectBytes
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Endian / int conversion helpers
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
u16BE = bytes : add (mul 256 (head bytes)) (head (tail bytes))
|
||||
|
||||
u16LE = bytes : add (mul 256 (head (tail bytes))) (head bytes)
|
||||
|
||||
readU16BE = bs : bindParser read2 (bytes rest : ok (u16BE bytes) rest) bs
|
||||
readU16LE = bs : bindParser read2 (bytes rest : ok (u16LE bytes) rest) bs
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
!import "base.tri" !Local
|
||||
!import "list.tri" !Local
|
||||
|
||||
nothing = t
|
||||
just = x : t x
|
||||
|
||||
bytesNil? = emptyList?
|
||||
|
||||
bytesHead = matchList nothing (h _ : just h)
|
||||
|
||||
@@ -1,23 +1,6 @@
|
||||
!import "base.tri" !Local
|
||||
!import "list.tri" !Local
|
||||
|
||||
pred = y (self : triage
|
||||
0
|
||||
(_ : 0)
|
||||
(bit rest :
|
||||
matchBool
|
||||
-- odd: 2n + 1 -> 2n
|
||||
(matchBool
|
||||
0
|
||||
(pair 0 rest)
|
||||
(equal? rest 0))
|
||||
-- even: 2n -> 2n - 1
|
||||
(matchBool
|
||||
0
|
||||
(pair 1 (self rest))
|
||||
(equal? rest 0))
|
||||
bit))
|
||||
|
||||
incDecRev = y (self : matchList
|
||||
"1"
|
||||
(digit rest :
|
||||
|
||||
72
lib/io.tri
72
lib/io.tri
@@ -37,6 +37,55 @@ sleep = ms : pair 63 ms
|
||||
|
||||
thenIO = a b : bind a (_ : b)
|
||||
mapIO = action f : bind action (x : pure (f x))
|
||||
void = action : bind action (_ : pure t)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Conditional execution
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
when = cond action : matchBool action (pure t) cond
|
||||
unless = cond action : matchBool (pure t) action cond
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Infinite loop
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
forever = y (self : action :
|
||||
bind action (_ :
|
||||
self action))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Result-aware combinators
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Propagate driver Result on error; run okCase on success.
|
||||
onOk = action okCase :
|
||||
bind action (result :
|
||||
matchResult
|
||||
(err rest : pure result)
|
||||
okCase
|
||||
result)
|
||||
|
||||
-- Same as onOk, but the okCase only receives the value (rest is dropped).
|
||||
onOk_ = action okCase :
|
||||
bind action (result :
|
||||
matchResult
|
||||
(err rest : pure result)
|
||||
(val _ : okCase val)
|
||||
result)
|
||||
|
||||
-- Generalized Result handler with explicit branches.
|
||||
onResult = action errCase okCase :
|
||||
bind action (result :
|
||||
matchResult errCase okCase result)
|
||||
|
||||
-- Same as onResult, but handlers only receive the value/msg (rest is dropped).
|
||||
onResult_ = action errCase okCase :
|
||||
bind action (result :
|
||||
matchResult
|
||||
(err _ : errCase err)
|
||||
(val _ : okCase val)
|
||||
result)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Convenience helpers
|
||||
@@ -49,13 +98,9 @@ putStrLn = s : bind (putStr (append s "\n")) (_ : pure t)
|
||||
-- Result-aware file helpers
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
onReadFile = (path errCase okCase :
|
||||
bind (readFile path) (result :
|
||||
matchResult errCase okCase result))
|
||||
onReadFile = path : onResult (readFile path)
|
||||
|
||||
onWriteFile = (path contents errCase okCase :
|
||||
bind (writeFile path contents) (result :
|
||||
matchResult errCase okCase result))
|
||||
onWriteFile = path contents : onResult (writeFile path contents)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Convenience helpers for the common cases
|
||||
@@ -84,3 +129,18 @@ copyFile = (src dst :
|
||||
(ok rest : pure t)
|
||||
wr))
|
||||
result))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Resource-safe combinators
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
finally = action cleanup :
|
||||
bind action (result :
|
||||
bind cleanup (_ :
|
||||
pure result))
|
||||
|
||||
bracket = acquire release use :
|
||||
bind acquire (resource :
|
||||
bind (use resource) (result :
|
||||
bind (release resource) (_ :
|
||||
pure result)))
|
||||
|
||||
173
lib/list.tri
173
lib/list.tri
@@ -37,9 +37,13 @@ length = y (self : matchList
|
||||
0
|
||||
(_ tail : succ (self tail)))
|
||||
|
||||
reverse = y (self : matchList
|
||||
t
|
||||
(head tail : append (self tail) (pair head t)))
|
||||
reverse_ = y (self xs acc :
|
||||
matchList
|
||||
acc
|
||||
(h r : self r (pair h acc))
|
||||
xs)
|
||||
|
||||
reverse = xs : reverse_ xs t
|
||||
|
||||
snoc = y (self x : matchList
|
||||
(pair x t)
|
||||
@@ -80,3 +84,166 @@ nth_ = y (self n xs i :
|
||||
xs)
|
||||
|
||||
nth = n xs : nth_ n xs 0
|
||||
|
||||
headMaybe = matchList nothing (h _ : just h)
|
||||
|
||||
lastMaybe = y (self : matchList
|
||||
nothing
|
||||
(hd tl : matchBool
|
||||
(just hd)
|
||||
(self tl)
|
||||
(emptyList? tl)))
|
||||
|
||||
nthMaybe_ = y (self n xs i :
|
||||
matchList
|
||||
nothing
|
||||
(h r :
|
||||
matchBool
|
||||
(just h)
|
||||
(self n r (succ i))
|
||||
(equal? i n))
|
||||
xs)
|
||||
|
||||
nthMaybe = n xs : nthMaybe_ n xs 0
|
||||
|
||||
take_ = y (self n xs i :
|
||||
matchList
|
||||
t
|
||||
(h r :
|
||||
matchBool
|
||||
t
|
||||
(pair h (self n r (succ i)))
|
||||
(equal? i n))
|
||||
xs)
|
||||
|
||||
take = n xs : take_ n xs 0
|
||||
|
||||
drop_ = y (self n xs i :
|
||||
matchBool
|
||||
xs
|
||||
(matchList
|
||||
t
|
||||
(_ r : self n r (succ i))
|
||||
xs)
|
||||
(equal? i n))
|
||||
|
||||
drop = n xs : drop_ n xs 0
|
||||
|
||||
splitAt = n xs : pair (take n xs) (drop n xs)
|
||||
|
||||
concatMap_ = y (self f xs :
|
||||
matchList
|
||||
t
|
||||
(h r : append (f h) (self f r))
|
||||
xs)
|
||||
|
||||
concatMap = f xs : concatMap_ f xs
|
||||
|
||||
find = y (self pred xs :
|
||||
matchList
|
||||
nothing
|
||||
(h r : matchBool (just h) (self pred r) (pred h))
|
||||
xs)
|
||||
|
||||
partition_ = y (self pred xs trues falses :
|
||||
matchList
|
||||
(pair (reverse trues) (reverse falses))
|
||||
(h r :
|
||||
matchBool
|
||||
(self pred r (pair h trues) falses)
|
||||
(self pred r trues (pair h falses))
|
||||
(pred h))
|
||||
xs)
|
||||
|
||||
partition = pred xs : partition_ pred xs t t
|
||||
|
||||
partition = pred xs : partition_ pred xs t t
|
||||
|
||||
strLength = length
|
||||
strAppend = append
|
||||
strEq? = equal?
|
||||
strEmpty? = emptyList?
|
||||
|
||||
startsWith? = y (self prefix str :
|
||||
matchList
|
||||
true
|
||||
(ph pr :
|
||||
matchList
|
||||
false
|
||||
(sh sr :
|
||||
matchBool
|
||||
(self pr sr)
|
||||
false
|
||||
(equal? ph sh))
|
||||
str)
|
||||
prefix)
|
||||
|
||||
endsWith? = prefix str : startsWith? (reverse prefix) (reverse str)
|
||||
|
||||
contains? = y (self needle haystack :
|
||||
matchBool
|
||||
true
|
||||
(matchList
|
||||
false
|
||||
(_ r : self needle r)
|
||||
haystack)
|
||||
(startsWith? needle haystack))
|
||||
|
||||
lines_ = y (self str :
|
||||
matchList
|
||||
(acc current : append acc [(reverse current)])
|
||||
(h r :
|
||||
acc current :
|
||||
matchBool
|
||||
(self r (append acc [(reverse current)]) t)
|
||||
(self r acc (pair h current))
|
||||
(equal? h 10))
|
||||
str)
|
||||
|
||||
lines = str : lines_ str t t
|
||||
|
||||
unlines = y (self lines :
|
||||
matchList
|
||||
""
|
||||
(h r : append h (append "\n" (self r)))
|
||||
lines)
|
||||
|
||||
words_ = y (self str :
|
||||
matchList
|
||||
(acc current :
|
||||
matchBool
|
||||
acc
|
||||
(append acc [(reverse current)])
|
||||
(emptyList? current))
|
||||
(h r :
|
||||
acc current :
|
||||
matchBool
|
||||
(matchBool
|
||||
(self r acc current)
|
||||
(self r (append acc [(reverse current)]) t)
|
||||
(emptyList? current))
|
||||
(self r acc (pair h current))
|
||||
(equal? h 32))
|
||||
str)
|
||||
|
||||
words = str : words_ str t t
|
||||
|
||||
unwords = y (self words :
|
||||
matchList
|
||||
""
|
||||
(h r :
|
||||
matchBool
|
||||
h
|
||||
(append h (append " " (self r)))
|
||||
(emptyList? r))
|
||||
words)
|
||||
|
||||
zipWith = y (self f xs ys :
|
||||
matchList
|
||||
t
|
||||
(xh xt :
|
||||
matchList
|
||||
t
|
||||
(yh yt : pair (f xh yh) (self f xt yt))
|
||||
ys)
|
||||
xs)
|
||||
|
||||
@@ -16,48 +16,68 @@ recv = sock maxBytes : pair 76 (pair sock maxBytes)
|
||||
send = sock bytes : pair 77 (pair sock bytes)
|
||||
getSocketName = sock : pair 78 sock
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Convenience helpers
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Result-aware wrappers over raw socket actions.
|
||||
onSocket = onResult socket
|
||||
onBindSocket = sock addr port : onResult (bindSocket sock addr port)
|
||||
onListen = sock backlog : onResult (listen sock backlog)
|
||||
onAccept = sock : onResult (accept sock)
|
||||
onConnect = sock addr port : onResult (connect sock addr port)
|
||||
onRecv = sock maxBytes : onResult (recv sock maxBytes)
|
||||
onSend = sock bytes : onResult (send sock bytes)
|
||||
onGetSocketName = sock : onResult (getSocketName sock)
|
||||
|
||||
onSocket = (action errCase okCase :
|
||||
bind action (result :
|
||||
matchResult errCase okCase result))
|
||||
-- Result-aware wrappers that drop the useless 'rest' parameter.
|
||||
onSocket_ = onResult_ socket
|
||||
onBindSocket_ = sock addr port : onResult_ (bindSocket sock addr port)
|
||||
onListen_ = sock backlog : onResult_ (listen sock backlog)
|
||||
onAccept_ = sock : onResult_ (accept sock)
|
||||
onConnect_ = sock addr port : onResult_ (connect sock addr port)
|
||||
onRecv_ = sock maxBytes : onResult_ (recv sock maxBytes)
|
||||
onSend_ = sock bytes : onResult_ (send sock bytes)
|
||||
onGetSocketName_ = sock : onResult_ (getSocketName sock)
|
||||
|
||||
-- Close a socket, ignoring errors.
|
||||
closeSocket_ = sock : void (closeSocket sock)
|
||||
|
||||
-- Create a listening socket bound to an address and port.
|
||||
-- Returns ok listenSocket or err message.
|
||||
listenSocket = addr port backlog :
|
||||
bind (socket) (result :
|
||||
matchResult
|
||||
(err rest : pure (err "socket creation failed"))
|
||||
(sock rest :
|
||||
bind (bindSocket sock addr port) (bindResult :
|
||||
matchResult
|
||||
(err rest : pure (err "bind failed"))
|
||||
(_ rest :
|
||||
bind (listen sock backlog) (listenResult :
|
||||
matchResult
|
||||
(err rest : pure (err "listen failed"))
|
||||
(_ rest : pure (ok sock))
|
||||
listenResult))
|
||||
bindResult))
|
||||
result)
|
||||
onOk_ socket (server :
|
||||
onOk_ (bindSocket server addr port) (_ :
|
||||
onOk_ (listen server backlog) (_ :
|
||||
pure (ok server))))
|
||||
|
||||
-- Accept a connection and return (clientSocket, peerAddr).
|
||||
-- The returned peerAddr is a string like "127.0.0.1:8080".
|
||||
onAccept = (sock errCase okCase :
|
||||
bind (accept sock) (result :
|
||||
matchResult errCase okCase result))
|
||||
-- Accept a connection with explicit error and ok branches.
|
||||
-- okHandler receives (clientSocket, peerAddr).
|
||||
withAccepted = (server errHandler okHandler :
|
||||
onResult (accept server)
|
||||
errHandler
|
||||
(accepted rest :
|
||||
okHandler (fst accepted) (snd accepted)))
|
||||
|
||||
-- Receive all available bytes up to maxBytes.
|
||||
onRecv = (sock maxBytes errCase okCase :
|
||||
bind (recv sock maxBytes) (result :
|
||||
matchResult errCase okCase result))
|
||||
-- Same as withAccepted, but handlers drop the useless 'rest' parameter.
|
||||
withAccepted_ = (server errHandler okHandler :
|
||||
onResult_ (accept server)
|
||||
errHandler
|
||||
(accepted :
|
||||
okHandler (fst accepted) (snd accepted)))
|
||||
|
||||
-- Send bytes and return number of bytes sent.
|
||||
onSend = (sock bytes errCase okCase :
|
||||
bind (send sock bytes) (result :
|
||||
matchResult errCase okCase result))
|
||||
serveOnce = (server handler :
|
||||
withAccepted_ server
|
||||
(err : pure t)
|
||||
(client peer :
|
||||
handler client peer))
|
||||
|
||||
-- Close a socket, ignoring errors.
|
||||
closeSocket_ = sock : bind (closeSocket sock) (_ : pure t)
|
||||
serveForkingOnce = (server handler :
|
||||
withAccepted_ server
|
||||
(err : pure t)
|
||||
(client peer :
|
||||
fork (handler client peer)))
|
||||
|
||||
serveForever = (server handler :
|
||||
forever (serveForkingOnce server handler))
|
||||
|
||||
connectTo = (addr port :
|
||||
onOk socket (client rest :
|
||||
onOk (connect client addr port) (_ rest :
|
||||
pure (ok client rest))))
|
||||
|
||||
@@ -1,749 +0,0 @@
|
||||
Below is the implementation handoff for replacing the current recursive/rebuilding IO small-step interpreter with an explicit machine stack, primarily to support `Reader` via `ask` and `local`, while setting up the right shape for eventual async.
|
||||
|
||||
## Goal
|
||||
|
||||
Refactor `IODriver` from this model:
|
||||
|
||||
```haskell
|
||||
stepIO :: IOPermissions -> T -> IO Step
|
||||
|
||||
data Step
|
||||
= Done T
|
||||
| Continue T
|
||||
```
|
||||
|
||||
to an explicit abstract machine:
|
||||
|
||||
```haskell
|
||||
Machine = Runtime + current action + continuation frames
|
||||
```
|
||||
|
||||
This is required because `local` is dynamically scoped. It needs to modify the Reader environment for a sub-computation, then restore the previous environment exactly when that sub-computation completes. The current “rebuild `Bind left' k`” approach has nowhere to store that restoration behavior.
|
||||
|
||||
This change should support:
|
||||
|
||||
```tricu
|
||||
ask
|
||||
local
|
||||
```
|
||||
|
||||
now, and keep the structure compatible with future async suspension/resumption.
|
||||
|
||||
Do not implement async in this pass.
|
||||
|
||||
---
|
||||
|
||||
## New action tags
|
||||
|
||||
Extend the tricu IO action language with Reader tags:
|
||||
|
||||
```tricu
|
||||
ask = _ : pair 6 t
|
||||
local = f action : pair 7 (pair f action)
|
||||
```
|
||||
|
||||
Host-side:
|
||||
|
||||
```haskell
|
||||
data Action
|
||||
= APure T
|
||||
| ABind T T
|
||||
| APutStr T
|
||||
| AGetLine
|
||||
| AReadFile T
|
||||
| AWriteFile T T
|
||||
| AAsk
|
||||
| ALocal T T
|
||||
deriving (Show)
|
||||
```
|
||||
|
||||
Recommended tag allocation:
|
||||
|
||||
```text
|
||||
0 = pure
|
||||
1 = bind
|
||||
2 = putStr
|
||||
3 = getLine
|
||||
4 = readFile
|
||||
5 = writeFile
|
||||
6 = ask
|
||||
7 = local
|
||||
```
|
||||
|
||||
State tags can come later:
|
||||
|
||||
```text
|
||||
8 = get
|
||||
9 = put
|
||||
```
|
||||
|
||||
Do not add `bindR`, `bindS`, or `bindRS` yet. Reader is being added as an effect inside the existing IO action language, so the existing IO `bind` remains the only sequencing operator.
|
||||
|
||||
---
|
||||
|
||||
## New runtime model
|
||||
|
||||
Add a runtime context:
|
||||
|
||||
```haskell
|
||||
data Runtime = Runtime
|
||||
{ rtPerms :: IOPermissions
|
||||
, rtEnv :: T
|
||||
}
|
||||
deriving (Show)
|
||||
```
|
||||
|
||||
Later this can become:
|
||||
|
||||
```haskell
|
||||
data Runtime = Runtime
|
||||
{ rtPerms :: IOPermissions
|
||||
, rtEnv :: T
|
||||
, rtState :: T
|
||||
}
|
||||
```
|
||||
|
||||
but for this pass, keep it minimal unless State is implemented at the same time.
|
||||
|
||||
Add continuation frames:
|
||||
|
||||
```haskell
|
||||
data Frame
|
||||
= BindFrame T
|
||||
| LocalFrame T
|
||||
deriving (Show)
|
||||
```
|
||||
|
||||
Frame meanings:
|
||||
|
||||
```text
|
||||
BindFrame k:
|
||||
When the current action produces value x, continue with apply k x.
|
||||
|
||||
LocalFrame oldEnv:
|
||||
When the current action produces value x, restore oldEnv, then continue with x.
|
||||
```
|
||||
|
||||
Add the machine state:
|
||||
|
||||
```haskell
|
||||
data Machine = Machine
|
||||
{ machineRuntime :: Runtime
|
||||
, machineCurrent :: T
|
||||
, machineFrames :: [Frame]
|
||||
}
|
||||
deriving (Show)
|
||||
```
|
||||
|
||||
Frames should be treated as a stack, with the head as the top:
|
||||
|
||||
```haskell
|
||||
push frame machine = machine { machineFrames = frame : machineFrames machine }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## New step result
|
||||
|
||||
Replace the current `Step` with machine-oriented stepping:
|
||||
|
||||
```haskell
|
||||
data Step
|
||||
= Halt Runtime T
|
||||
| Continue Machine
|
||||
deriving (Show)
|
||||
```
|
||||
|
||||
`Halt runtime value` means the entire IO program is done.
|
||||
|
||||
`Continue machine` means the machine can take another step.
|
||||
|
||||
---
|
||||
|
||||
## Core stepping semantics
|
||||
|
||||
The central function should become:
|
||||
|
||||
```haskell
|
||||
stepMachine :: Machine -> IO Step
|
||||
```
|
||||
|
||||
It should decode `machineCurrent`.
|
||||
|
||||
### `pure`
|
||||
|
||||
When the current action is `APure value`, do not immediately halt. First inspect the frame stack.
|
||||
|
||||
Pseudo-code:
|
||||
|
||||
```haskell
|
||||
finishValue :: Machine -> T -> IO Step
|
||||
finishValue machine value =
|
||||
case machineFrames machine of
|
||||
[] ->
|
||||
pure (Halt (machineRuntime machine) value)
|
||||
|
||||
BindFrame k : rest ->
|
||||
pure (Continue machine
|
||||
{ machineCurrent = apply k value
|
||||
, machineFrames = rest
|
||||
})
|
||||
|
||||
LocalFrame oldEnv : rest ->
|
||||
let runtime' = (machineRuntime machine) { rtEnv = oldEnv }
|
||||
in pure (Continue machine
|
||||
{ machineRuntime = runtime'
|
||||
, machineCurrent = pureAction value
|
||||
, machineFrames = rest
|
||||
})
|
||||
```
|
||||
|
||||
You will need a helper:
|
||||
|
||||
```haskell
|
||||
pureAction :: T -> T
|
||||
pureAction x = Fork (ofNumber 0) x
|
||||
```
|
||||
|
||||
This is important: restoring a `LocalFrame` should not discard the value. It restores the environment and re-enters the machine as `pure value`, allowing the next frame to receive the value.
|
||||
|
||||
### `bind`
|
||||
|
||||
For:
|
||||
|
||||
```haskell
|
||||
ABind left k
|
||||
```
|
||||
|
||||
do not recursively step `left`, and do not rebuild `Bind left' k`.
|
||||
|
||||
Instead:
|
||||
|
||||
```haskell
|
||||
Continue machine
|
||||
{ machineCurrent = left
|
||||
, machineFrames = BindFrame k : machineFrames machine
|
||||
}
|
||||
```
|
||||
|
||||
This is the major refactor. Continuations move out of the tree and into the frame stack.
|
||||
|
||||
### `ask`
|
||||
|
||||
For:
|
||||
|
||||
```haskell
|
||||
AAsk
|
||||
```
|
||||
|
||||
produce the current Reader environment:
|
||||
|
||||
```haskell
|
||||
finishValue machine (rtEnv (machineRuntime machine))
|
||||
```
|
||||
|
||||
or equivalently:
|
||||
|
||||
```haskell
|
||||
Continue machine { machineCurrent = pureAction currentEnv }
|
||||
```
|
||||
|
||||
Prefer `finishValue` because it avoids an extra step.
|
||||
|
||||
### `local`
|
||||
|
||||
For:
|
||||
|
||||
```haskell
|
||||
ALocal f action
|
||||
```
|
||||
|
||||
do:
|
||||
|
||||
```haskell
|
||||
let runtime = machineRuntime machine
|
||||
oldEnv = rtEnv runtime
|
||||
newEnv = apply f oldEnv
|
||||
runtime' = runtime { rtEnv = newEnv }
|
||||
|
||||
Continue machine
|
||||
{ machineRuntime = runtime'
|
||||
, machineCurrent = action
|
||||
, machineFrames = LocalFrame oldEnv : machineFrames machine
|
||||
}
|
||||
```
|
||||
|
||||
This is the central correctness point.
|
||||
|
||||
`local` enters a scoped environment by pushing a restoration frame. When the scoped action finishes, `LocalFrame oldEnv` restores the previous environment and passes the produced value onward.
|
||||
|
||||
Nested `local` works naturally because frames stack:
|
||||
|
||||
```tricu
|
||||
local f (
|
||||
local g ask
|
||||
)
|
||||
```
|
||||
|
||||
becomes:
|
||||
|
||||
```text
|
||||
push LocalFrame env0
|
||||
set env = f env0
|
||||
|
||||
push LocalFrame env1
|
||||
set env = g env1
|
||||
|
||||
ask returns env2
|
||||
|
||||
pop LocalFrame env1
|
||||
restore env1
|
||||
|
||||
pop LocalFrame env0
|
||||
restore env0
|
||||
```
|
||||
|
||||
### Normal IO actions
|
||||
|
||||
For host IO actions, perform the side effect and then call `finishValue`.
|
||||
|
||||
Examples:
|
||||
|
||||
```haskell
|
||||
APutStr str ->
|
||||
case decodeString str "PutStr" of
|
||||
Right s -> do
|
||||
putStr s
|
||||
finishValue machine Leaf
|
||||
Left _ ->
|
||||
finishValue machine (errResult 6)
|
||||
```
|
||||
|
||||
```haskell
|
||||
AReadFile path ->
|
||||
case decodeString path "ReadFile" of
|
||||
Right p -> do
|
||||
result <- ...
|
||||
finishValue machine result
|
||||
Left _ ->
|
||||
finishValue machine (errResult 6)
|
||||
```
|
||||
|
||||
Important: IO actions should no longer return `Done value` directly. They should return a value to the frame stack via `finishValue`.
|
||||
|
||||
---
|
||||
|
||||
## Decode changes
|
||||
|
||||
Extend `decodeAction`:
|
||||
|
||||
```haskell
|
||||
decodeAction :: T -> Either String Action
|
||||
decodeAction tree =
|
||||
case tree of
|
||||
Fork tag payload ->
|
||||
case toNumber tag of
|
||||
Right 0 -> Right (APure payload)
|
||||
|
||||
Right 1 -> case payload of
|
||||
Fork left k -> Right (ABind left k)
|
||||
_ -> Left "Invalid Bind: expected pair action continuation"
|
||||
|
||||
Right 2 -> Right (APutStr payload)
|
||||
|
||||
Right 3 -> Right AGetLine
|
||||
|
||||
Right 4 -> Right (AReadFile payload)
|
||||
|
||||
Right 5 -> case payload of
|
||||
Fork path contents -> Right (AWriteFile path contents)
|
||||
_ -> Left "Invalid WriteFile: expected pair path contents"
|
||||
|
||||
Right 6 -> Right AAsk
|
||||
|
||||
Right 7 -> case payload of
|
||||
Fork f action -> Right (ALocal f action)
|
||||
_ -> Left "Invalid Local: expected pair function action"
|
||||
|
||||
Right n -> Left $ "Unknown IO action tag: " ++ show n
|
||||
|
||||
Left err -> Left $ "Invalid action tag: " ++ err
|
||||
|
||||
_ ->
|
||||
Left $ "Invalid action tree: expected pair tag payload, got " ++ show tree
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Runner API
|
||||
|
||||
Add a new Reader-aware runner:
|
||||
|
||||
```haskell
|
||||
runIOWithEnv :: IOPermissions -> T -> T -> IO T
|
||||
runIOWithEnv perms env action = loop initialMachine
|
||||
where
|
||||
initialMachine = Machine
|
||||
{ machineRuntime = Runtime
|
||||
{ rtPerms = perms
|
||||
, rtEnv = env
|
||||
}
|
||||
, machineCurrent = action
|
||||
, machineFrames = []
|
||||
}
|
||||
|
||||
loop machine = do
|
||||
step <- stepMachine machine
|
||||
case step of
|
||||
Halt _ value -> pure value
|
||||
Continue machine' -> loop machine'
|
||||
```
|
||||
|
||||
Keep the existing API as a compatibility wrapper:
|
||||
|
||||
```haskell
|
||||
runIO :: IOPermissions -> T -> IO T
|
||||
runIO perms action =
|
||||
runIOWithEnv perms Leaf action
|
||||
```
|
||||
|
||||
If State is added in the same branch, prefer:
|
||||
|
||||
```haskell
|
||||
runIOWith :: IOPermissions -> T -> T -> T -> IO (T, T)
|
||||
```
|
||||
|
||||
where:
|
||||
|
||||
```text
|
||||
permissions
|
||||
initial reader env
|
||||
initial state
|
||||
action
|
||||
```
|
||||
|
||||
returns:
|
||||
|
||||
```text
|
||||
final result
|
||||
final state
|
||||
```
|
||||
|
||||
But if this handoff is only for Reader, use `runIOWithEnv`.
|
||||
|
||||
---
|
||||
|
||||
## Permission helpers
|
||||
|
||||
The current permission helper functions can mostly stay as-is, but they should read permissions from runtime:
|
||||
|
||||
```haskell
|
||||
let perms = rtPerms (machineRuntime machine)
|
||||
```
|
||||
|
||||
The current helpers are nested inside `stepIO`. After the refactor, either:
|
||||
|
||||
1. keep them in a `where` block under `stepMachine`, or
|
||||
2. lift them to top-level helper functions.
|
||||
|
||||
Prefer lifting pure/reusable helpers to top-level if this file is getting large:
|
||||
|
||||
```haskell
|
||||
decodeString :: T -> String -> Either String String
|
||||
canonicalizeSafe :: FilePath -> IO (Either String FilePath)
|
||||
pathAllowed :: FilePath -> [FilePath] -> IO Bool
|
||||
tryReadFile :: FilePath -> IO T
|
||||
tryWriteFile :: FilePath -> String -> IO T
|
||||
okResult :: T -> T
|
||||
errResult :: Integer -> T
|
||||
ioErrorCode :: IOException -> Integer
|
||||
```
|
||||
|
||||
This will make `stepMachine` much easier to read.
|
||||
|
||||
---
|
||||
|
||||
## `io.tri` changes
|
||||
|
||||
Add the Reader constructors:
|
||||
|
||||
```tricu
|
||||
ask = _ : pair 6 t
|
||||
local = f action : pair 7 (pair f action)
|
||||
```
|
||||
|
||||
No new bind is required.
|
||||
|
||||
Example usage:
|
||||
|
||||
```tricu
|
||||
program =
|
||||
bind ask (env :
|
||||
putStrLn env)
|
||||
```
|
||||
|
||||
Example `local` usage:
|
||||
|
||||
```tricu
|
||||
program =
|
||||
bind ask (outer :
|
||||
bind (local (env : append env "-inner")
|
||||
(bind ask (inner :
|
||||
pure inner)))
|
||||
(result :
|
||||
bind ask (after :
|
||||
pure result)))
|
||||
```
|
||||
|
||||
Expected behavior:
|
||||
|
||||
```text
|
||||
outer ask sees original env
|
||||
inner ask sees transformed env
|
||||
after ask sees original env again
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tests to add
|
||||
|
||||
Add tests around behavior, not implementation details.
|
||||
|
||||
### 1. `ask` returns initial environment
|
||||
|
||||
Program:
|
||||
|
||||
```tricu
|
||||
io (bind ask (env : pure env))
|
||||
```
|
||||
|
||||
Run with env:
|
||||
|
||||
```text
|
||||
"dev"
|
||||
```
|
||||
|
||||
Expected result:
|
||||
|
||||
```text
|
||||
"dev"
|
||||
```
|
||||
|
||||
### 2. `local` transforms environment
|
||||
|
||||
Program:
|
||||
|
||||
```tricu
|
||||
io (
|
||||
local (env : append env "-local")
|
||||
(bind ask (env : pure env))
|
||||
)
|
||||
```
|
||||
|
||||
Initial env:
|
||||
|
||||
```text
|
||||
"root"
|
||||
```
|
||||
|
||||
Expected result:
|
||||
|
||||
```text
|
||||
"root-local"
|
||||
```
|
||||
|
||||
### 3. `local` restores environment afterward
|
||||
|
||||
Program structure:
|
||||
|
||||
```tricu
|
||||
bind ask (before :
|
||||
bind (local transform scopedAsk) (inside :
|
||||
bind ask (after :
|
||||
pure (pair before (pair inside after)))))
|
||||
```
|
||||
|
||||
Initial env:
|
||||
|
||||
```text
|
||||
"root"
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
```text
|
||||
pair "root" (pair "root-local" "root")
|
||||
```
|
||||
|
||||
### 4. nested `local` composes correctly
|
||||
|
||||
Program:
|
||||
|
||||
```tricu
|
||||
local f (
|
||||
local g ask
|
||||
)
|
||||
```
|
||||
|
||||
Initial env:
|
||||
|
||||
```text
|
||||
"root"
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```tricu
|
||||
f = x : append x "-f"
|
||||
g = x : append x "-g"
|
||||
```
|
||||
|
||||
Expected inner ask:
|
||||
|
||||
```text
|
||||
"root-f-g"
|
||||
```
|
||||
|
||||
Also verify after both locals, environment is restored by doing a final `ask`.
|
||||
|
||||
### 5. `local` result passes through bind correctly
|
||||
|
||||
Program:
|
||||
|
||||
```tricu
|
||||
bind
|
||||
(local transform (pure "value"))
|
||||
(x : pure x)
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
```text
|
||||
"value"
|
||||
```
|
||||
|
||||
This catches a common bug where `LocalFrame` restores env but loses the value.
|
||||
|
||||
### 6. IO still works through bind
|
||||
|
||||
Existing IO tests should continue passing unchanged through `runIO`.
|
||||
|
||||
### 7. IO inside local
|
||||
|
||||
Program:
|
||||
|
||||
```tricu
|
||||
local transform (
|
||||
bind ask (env :
|
||||
bind (putStrLn env) (_ :
|
||||
pure env))
|
||||
)
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
```text
|
||||
prints transformed env
|
||||
returns transformed env
|
||||
```
|
||||
|
||||
Then optionally ask after local to verify restoration.
|
||||
|
||||
---
|
||||
|
||||
## Invariants to preserve
|
||||
|
||||
The implementation should maintain these invariants:
|
||||
|
||||
```text
|
||||
1. The current action is always the next instruction to evaluate.
|
||||
|
||||
2. The frame stack contains all pending continuations and cleanup scopes.
|
||||
|
||||
3. Bind does not recursively step its left side.
|
||||
It pushes BindFrame and switches current to the left action.
|
||||
|
||||
4. local does not run its action to completion.
|
||||
It pushes LocalFrame and switches current to the scoped action.
|
||||
|
||||
5. Only LocalFrame restores Reader environment.
|
||||
|
||||
6. State, when added later, should not be restored by LocalFrame.
|
||||
|
||||
7. Existing runIO behavior remains source-compatible.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common failure modes
|
||||
|
||||
The likely bugs are:
|
||||
|
||||
```text
|
||||
Bug: local leaks environment.
|
||||
Cause: setting rtEnv but never restoring it.
|
||||
Fix: push LocalFrame oldEnv and restore in finishValue.
|
||||
|
||||
Bug: local restores environment but loses result.
|
||||
Cause: popping LocalFrame and halting directly.
|
||||
Fix: after restoration, continue with pureAction value.
|
||||
|
||||
Bug: bind continuations run under the wrong env.
|
||||
Cause: LocalFrame and BindFrame pop order is wrong.
|
||||
Fix: use stack head as top. Push LocalFrame when entering local; push BindFrame when entering bind. Pop exactly one frame when a value is produced.
|
||||
|
||||
Bug: existing IO bind tests fail.
|
||||
Cause: IO actions halt instead of passing result to finishValue.
|
||||
Fix: every completed primitive action should call finishValue.
|
||||
|
||||
Bug: nested binds still rebuild trees.
|
||||
Cause: old ABind logic left in place.
|
||||
Fix: ABind should only push BindFrame and switch current to left.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Async relevance, but not implementation
|
||||
|
||||
This machine representation is intentionally compatible with async.
|
||||
|
||||
A future scheduler can store:
|
||||
|
||||
```haskell
|
||||
Machine runtime current frames
|
||||
```
|
||||
|
||||
when a task blocks, then resume it later by restoring the same `Machine`.
|
||||
|
||||
Do not implement any of these now:
|
||||
|
||||
```haskell
|
||||
AFork
|
||||
AAwait
|
||||
ASleep
|
||||
TaskId
|
||||
Scheduler
|
||||
Runnable queue
|
||||
Blocked table
|
||||
```
|
||||
|
||||
But avoid designs that would make future suspension impossible, especially recursive “run sub-computation to completion” implementations of `local`. The point of the frame machine is that every effect remains small-step and resumable.
|
||||
|
||||
---
|
||||
|
||||
## Recommended implementation order
|
||||
|
||||
1. Add `Runtime`, `Frame`, and `Machine`.
|
||||
2. Add `pureAction`.
|
||||
3. Replace `Step` with `Halt Runtime T | Continue Machine`.
|
||||
4. Implement `finishValue`.
|
||||
5. Rewrite `ABind` to push `BindFrame`.
|
||||
6. Rewrite existing primitive IO actions to call `finishValue`.
|
||||
7. Add `AAsk` and `ALocal`.
|
||||
8. Add `runIOWithEnv`.
|
||||
9. Rewrite `runIO` as a wrapper.
|
||||
10. Add `ask` and `local` to `io.tri`.
|
||||
11. Add Reader behavior tests.
|
||||
12. Run all existing IO tests and confirm no regressions.
|
||||
|
||||
The key handoff instruction is: implement `local` as a continuation frame, not as a recursive nested run. This keeps the interpreter genuinely small-step and gives the eventual async runtime the exact representation it will need for suspension and resumption.
|
||||
262
notes/stdlib-todo.md
Normal file
262
notes/stdlib-todo.md
Normal file
@@ -0,0 +1,262 @@
|
||||
# Standard Library TODO / Expansion Plan
|
||||
|
||||
> Foundational expansions that improve safety, composability, and ergonomics across the entire tricu ecosystem.
|
||||
|
||||
---
|
||||
|
||||
## 1. Extract a `maybe.tri` / Option layer
|
||||
|
||||
**Motivation:** `head`, `tail`, `last`, `nth`, `bytesHead`, and `bytesTail` currently return `t` on failure, which is ambiguous because `t` is also a valid tree value. A proper option layer makes all of these APIs safer and self-describing.
|
||||
|
||||
**Additions:**
|
||||
|
||||
```tricu
|
||||
nothing = t
|
||||
just = x : t x
|
||||
|
||||
matchMaybe = nothingCase justCase maybe :
|
||||
triage
|
||||
nothingCase
|
||||
justCase
|
||||
(_ _ : nothingCase)
|
||||
maybe
|
||||
|
||||
maybe = default f m : matchMaybe default f m
|
||||
maybeMap = f m : matchMaybe nothing (x : just (f x)) m
|
||||
maybeBind = m f : matchMaybe nothing f m
|
||||
maybeOr = default m : matchMaybe default id m
|
||||
maybe? = matchMaybe false (_ : true)
|
||||
```
|
||||
|
||||
**Then update existing list/bytes primitives:**
|
||||
|
||||
```tricu
|
||||
headMaybe
|
||||
lastMaybe
|
||||
nthMaybe
|
||||
bytesHead
|
||||
bytesTail
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Move `Result` combinators from `binary.tri` into `base.tri`
|
||||
|
||||
**Motivation:** `Result` is defined in `base.tri`, but `mapResult` and `bindResult` currently live in `binary.tri`. This makes them unavailable to IO, socket, and file helpers unless every consumer imports binary parsing.
|
||||
|
||||
**Move / add to `base.tri`:**
|
||||
|
||||
```tricu
|
||||
mapResult = f result :
|
||||
matchResult
|
||||
(code rest : err code rest)
|
||||
(value rest : ok (f value) rest)
|
||||
result
|
||||
|
||||
bindResult = result f :
|
||||
matchResult
|
||||
(code rest : err code rest)
|
||||
(value rest : f value rest)
|
||||
result
|
||||
|
||||
resultOr = default result :
|
||||
matchResult
|
||||
(_ _ : default)
|
||||
(value _ : value)
|
||||
result
|
||||
|
||||
resultMapErr = f result :
|
||||
matchResult
|
||||
(code rest : err (f code) rest)
|
||||
(value rest : ok value rest)
|
||||
result
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Add basic numeric comparison and arithmetic
|
||||
|
||||
**Motivation:** Without comparison and arithmetic, list slicing, parser limits, counters, socket loops, and CLI utilities are awkward or impossible.
|
||||
|
||||
**Priority order:**
|
||||
|
||||
1. `isZero?`
|
||||
2. `add`
|
||||
3. `sub`
|
||||
4. `lt?`
|
||||
5. `lte?`
|
||||
6. `mul`
|
||||
|
||||
Even simple Peano / binary-tree versions unlock a huge amount of functionality.
|
||||
|
||||
---
|
||||
|
||||
## 4. Expand `list.tri` with safer and more complete primitives
|
||||
|
||||
**Motivation:** `reverse` currently uses `append` recursively, which is quadratic. Many common operations (`take`, `drop`, `concatMap`) are missing entirely.
|
||||
|
||||
**High-impact additions:**
|
||||
|
||||
```tricu
|
||||
take
|
||||
drop
|
||||
splitAt
|
||||
concatMap
|
||||
find
|
||||
partition
|
||||
zipWith
|
||||
```
|
||||
|
||||
**Performance fix:**
|
||||
|
||||
```tricu
|
||||
reverse_ = y (self xs acc :
|
||||
matchList
|
||||
acc
|
||||
(h r : self r (pair h acc))
|
||||
xs)
|
||||
|
||||
reverse = xs : reverse_ xs t
|
||||
```
|
||||
|
||||
This is low-effort, high-impact because `reverse` is already used by `binary.tri` and `conversions.tri`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Add string aliases / helpers
|
||||
|
||||
**Motivation:** IO and CLI programs almost always need line/string manipulation. Socket protocols are also easier with canonical string utilities.
|
||||
|
||||
**Highest value:**
|
||||
|
||||
```tricu
|
||||
startsWith?
|
||||
contains?
|
||||
lines
|
||||
unlines
|
||||
```
|
||||
|
||||
Also consider:
|
||||
|
||||
```tricu
|
||||
strLength
|
||||
strAppend
|
||||
strEq?
|
||||
strEmpty?
|
||||
words
|
||||
unwords
|
||||
endsWith?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Expand `binary.tri` into a small parser-combinator layer
|
||||
|
||||
**Motivation:** `binary.tri` already has `readU8`, `readBytes`, `expectBytes`, etc. A thin combinator layer makes binary parsing much more ergonomic and reusable for protocols/file formats.
|
||||
|
||||
**Suggested additions:**
|
||||
|
||||
```tricu
|
||||
pureParser = value bs : ok value bs
|
||||
failParser = code bs : err code bs
|
||||
|
||||
mapParser = mapResult
|
||||
bindParser = bindResult
|
||||
thenParser = p q : bindResult p (_ : q)
|
||||
orParser = p q bs :
|
||||
matchResult
|
||||
(_ _ : q bs)
|
||||
(value rest : ok value rest)
|
||||
(p bs)
|
||||
```
|
||||
|
||||
Then common parsers:
|
||||
|
||||
```tricu
|
||||
readWhile
|
||||
readUntil
|
||||
readRemaining
|
||||
peekU8
|
||||
eof?
|
||||
expectAscii
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Add endian / int conversion helpers
|
||||
|
||||
**Motivation:** `readU16BEBytes` and `readU32BEBytes` return raw byte lists. Either rename them or add actual numeric decoding.
|
||||
|
||||
**Suggested additions:**
|
||||
|
||||
```tricu
|
||||
u16BE
|
||||
u16LE
|
||||
u32BE
|
||||
u32LE
|
||||
|
||||
readU16BE
|
||||
readU16LE
|
||||
readU32BE
|
||||
readU32LE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Add resource-safe IO helpers
|
||||
|
||||
**Motivation:** Sockets, files, and process-like resources need predictable cleanup.
|
||||
|
||||
```tricu
|
||||
finally = action cleanup :
|
||||
bind action (result :
|
||||
bind cleanup (_ :
|
||||
pure result))
|
||||
|
||||
bracket = acquire release use :
|
||||
bind acquire (resource :
|
||||
bind (use resource) (result :
|
||||
bind (release resource) (_ :
|
||||
pure result)))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Add socket server loops
|
||||
|
||||
**Motivation:** Almost every socket example repeats the same `forever` + `withAccepted_` scaffolding.
|
||||
|
||||
```tricu
|
||||
serveForever = server handler :
|
||||
forever
|
||||
(withAccepted_ server
|
||||
(err : pure t)
|
||||
(client peer :
|
||||
fork (handler client peer)))
|
||||
```
|
||||
|
||||
Also consider:
|
||||
|
||||
```tricu
|
||||
connectTo = addr port :
|
||||
onOk_ socket (client :
|
||||
onOk_ (connect client addr port) (_ :
|
||||
pure (ok client)))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Add a curated `prelude.tri`
|
||||
|
||||
**Motivation:** As libraries grow, users need a stable starting point.
|
||||
|
||||
```tricu
|
||||
!import "base.tri" !Local
|
||||
!import "maybe.tri" !Local
|
||||
!import "list.tri" !Local
|
||||
!import "bytes.tri" !Local
|
||||
!import "conversions.tri" !Local
|
||||
```
|
||||
|
||||
This gives a standard baseline without importing IO / socket / binary by default.
|
||||
|
||||
1042
test/Spec.hs
1042
test/Spec.hs
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user