Helpful library updates

This commit is contained in:
2026-05-19 17:30:43 -05:00
parent 020fa769a9
commit e2a1744508
11 changed files with 1684 additions and 966 deletions

View File

@@ -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)) (_ :
self server))
(accepted rest :
matchPair
(clientSock addr :
bind (printLn (append "client from " addr)) (_ :
bind (recv clientSock 4096) (msgResult :
matchResult
(err rest :
bind (closeSocket clientSock) (_ :
self server))
(msg rest :
bind (send clientSock msg) (_ :
bind (closeSocket clientSock) (_ :
self server)))
msgResult)))
accepted)
acceptResult))
-- Main accept+echo loop. Recursion via y.
echoLoop = y (self : server :
withAccepted_ server
(err :
bind (putStrLn (append "accept error: " err)) (_ :
self server))
(clientSock addr :
bind (putStrLn (append "client from " addr)) (_ :
onResult_ (recv clientSock 4096)
(err :
bind (closeSocket clientSock) (_ :
self server))
(msg :
bind (send clientSock msg) (_ :
bind (closeSocket clientSock) (_ :
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))))))

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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 :

View File

@@ -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)))

View File

@@ -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)

View File

@@ -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))))

View File

@@ -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
View 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.

File diff suppressed because it is too large Load Diff