Interaction Trees in Zig and simple benchmarks
This commit is contained in:
240
bench/ApplyStats.hs
Normal file
240
bench/ApplyStats.hs
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
{-# LANGUAGE BangPatterns #-}
|
||||||
|
|
||||||
|
module ApplyStats
|
||||||
|
( ApplyStats(..)
|
||||||
|
, emptyApplyStats
|
||||||
|
, emptyApplyStatsSampled
|
||||||
|
, applyCounted
|
||||||
|
, runApplyCounted
|
||||||
|
, runApplySampledWithProgress
|
||||||
|
, runApplyGlobalCounted
|
||||||
|
, printApplyStats
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Research
|
||||||
|
import qualified Data.Map.Strict as M
|
||||||
|
import qualified Data.List as L
|
||||||
|
import Data.Ord (comparing)
|
||||||
|
import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import Debug.Trace (trace)
|
||||||
|
import System.IO.Unsafe (unsafePerformIO, unsafeDupablePerformIO)
|
||||||
|
import Data.IORef
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Threaded stats (slow but pure)
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Hash = Text
|
||||||
|
type AppKey = (Hash, Hash)
|
||||||
|
|
||||||
|
data ApplyStats = ApplyStats
|
||||||
|
{ totalApplyCalls :: !Int
|
||||||
|
, uniqueApps :: !(M.Map AppKey Int)
|
||||||
|
, sampleInterval :: !Int
|
||||||
|
, sampleCounter :: !Int
|
||||||
|
, progressEvery :: !Int
|
||||||
|
}
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
|
emptyApplyStats :: ApplyStats
|
||||||
|
emptyApplyStats = emptyApplyStatsSampled 1
|
||||||
|
|
||||||
|
emptyApplyStatsSampled :: Int -> ApplyStats
|
||||||
|
emptyApplyStatsSampled n = ApplyStats
|
||||||
|
{ totalApplyCalls = 0
|
||||||
|
, uniqueApps = M.empty
|
||||||
|
, sampleInterval = max 1 n
|
||||||
|
, sampleCounter = 0
|
||||||
|
, progressEvery = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
bump :: T -> T -> ApplyStats -> ApplyStats
|
||||||
|
bump !f !x !st =
|
||||||
|
let !counter' = sampleCounter st + 1
|
||||||
|
!total' = totalApplyCalls st + 1
|
||||||
|
!stBase = st { totalApplyCalls = total'
|
||||||
|
, sampleCounter = counter'
|
||||||
|
}
|
||||||
|
!st' = if counter' `mod` sampleInterval st /= 0
|
||||||
|
then stBase
|
||||||
|
else let !hf = termHash f
|
||||||
|
!hx = termHash x
|
||||||
|
!k = (hf, hx)
|
||||||
|
!m = M.insertWith (+) k 1 (uniqueApps st)
|
||||||
|
in stBase { uniqueApps = m }
|
||||||
|
in case progressEvery st of
|
||||||
|
0 -> st'
|
||||||
|
n | total' `mod` n == 0 ->
|
||||||
|
trace ("apply calls so far: " ++ show total') st'
|
||||||
|
_ -> st'
|
||||||
|
|
||||||
|
termHash :: T -> Hash
|
||||||
|
termHash Leaf =
|
||||||
|
nodeHash NLeaf
|
||||||
|
termHash (Stem t) =
|
||||||
|
nodeHash (NStem (termHash t))
|
||||||
|
termHash (Fork l r) =
|
||||||
|
nodeHash (NFork (termHash l) (termHash r))
|
||||||
|
|
||||||
|
applyCounted :: T -> T -> ApplyStats -> (T, ApplyStats)
|
||||||
|
applyCounted !f !x !st0 =
|
||||||
|
let !st1 = bump f x st0
|
||||||
|
in applyStepCounted f x st1
|
||||||
|
|
||||||
|
applyStepCounted :: T -> T -> ApplyStats -> (T, ApplyStats)
|
||||||
|
applyStepCounted (Fork Leaf a) _ st =
|
||||||
|
(a, st)
|
||||||
|
applyStepCounted (Fork (Stem a) b) c st =
|
||||||
|
let (!ac, !st1) = applyCounted a c st
|
||||||
|
(!bc, !st2) = applyCounted b c st1
|
||||||
|
in applyCounted ac bc st2
|
||||||
|
applyStepCounted (Fork (Fork a _b) _c) Leaf st =
|
||||||
|
(a, st)
|
||||||
|
applyStepCounted (Fork (Fork _a b) _c) (Stem u) st =
|
||||||
|
applyCounted b u st
|
||||||
|
applyStepCounted (Fork (Fork _a _b) c) (Fork u v) st =
|
||||||
|
let (!cu, !st1) = applyCounted c u st
|
||||||
|
in applyCounted cu v st1
|
||||||
|
applyStepCounted Leaf b st =
|
||||||
|
(Stem b, st)
|
||||||
|
applyStepCounted (Stem a) b st =
|
||||||
|
(Fork a b, st)
|
||||||
|
|
||||||
|
runApplyCounted :: T -> T -> (T, ApplyStats)
|
||||||
|
runApplyCounted !f !x =
|
||||||
|
applyCounted f x emptyApplyStats
|
||||||
|
|
||||||
|
runApplySampled :: Int -> T -> T -> (T, ApplyStats)
|
||||||
|
runApplySampled !n !f !x =
|
||||||
|
applyCounted f x (emptyApplyStatsSampled n)
|
||||||
|
|
||||||
|
runApplySampledWithProgress :: Int -> Int -> T -> T -> (T, ApplyStats)
|
||||||
|
runApplySampledWithProgress !interval !progress !f !x =
|
||||||
|
let st = (emptyApplyStatsSampled interval) { progressEvery = progress }
|
||||||
|
in applyCounted f x st
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Global mutable stats (fast, unsafe, single-threaded only)
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
{-# NOINLINE globalTotalCount #-}
|
||||||
|
globalTotalCount :: IORef Int
|
||||||
|
globalTotalCount = unsafePerformIO (newIORef 0)
|
||||||
|
|
||||||
|
{-# NOINLINE globalInterval #-}
|
||||||
|
globalInterval :: IORef Int
|
||||||
|
globalInterval = unsafePerformIO (newIORef 1)
|
||||||
|
|
||||||
|
{-# NOINLINE globalMap #-}
|
||||||
|
globalMap :: IORef (M.Map AppKey Int)
|
||||||
|
globalMap = unsafePerformIO (newIORef M.empty)
|
||||||
|
|
||||||
|
{-# NOINLINE globalProgress #-}
|
||||||
|
globalProgress :: IORef Int
|
||||||
|
globalProgress = unsafePerformIO (newIORef 0)
|
||||||
|
|
||||||
|
resetGlobalStats :: Int -> Int -> IO ()
|
||||||
|
resetGlobalStats !interval !progress = do
|
||||||
|
writeIORef globalTotalCount 0
|
||||||
|
writeIORef globalInterval (max 1 interval)
|
||||||
|
writeIORef globalMap M.empty
|
||||||
|
writeIORef globalProgress progress
|
||||||
|
|
||||||
|
readGlobalStats :: IO ApplyStats
|
||||||
|
readGlobalStats = do
|
||||||
|
total <- readIORef globalTotalCount
|
||||||
|
m <- readIORef globalMap
|
||||||
|
pure ApplyStats
|
||||||
|
{ totalApplyCalls = total
|
||||||
|
, uniqueApps = m
|
||||||
|
, sampleInterval = 0
|
||||||
|
, sampleCounter = 0
|
||||||
|
, progressEvery = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
{-# INLINE globalBump #-}
|
||||||
|
globalBump :: T -> T -> ()
|
||||||
|
globalBump !f !x = unsafeDupablePerformIO $ do
|
||||||
|
!total <- readIORef globalTotalCount
|
||||||
|
let !total' = total + 1
|
||||||
|
writeIORef globalTotalCount total'
|
||||||
|
!interval <- readIORef globalInterval
|
||||||
|
!progress <- readIORef globalProgress
|
||||||
|
let !_ = if progress > 0 && total' `mod` progress == 0
|
||||||
|
then trace ("apply calls so far: " ++ show total') ()
|
||||||
|
else ()
|
||||||
|
if total' `mod` interval /= 0
|
||||||
|
then pure ()
|
||||||
|
else do
|
||||||
|
let !hf = termHash f
|
||||||
|
!hx = termHash x
|
||||||
|
!k = (hf, hx)
|
||||||
|
!m <- readIORef globalMap
|
||||||
|
writeIORef globalMap (M.insertWith (+) k 1 m)
|
||||||
|
pure ()
|
||||||
|
|
||||||
|
applyGlobalCounted :: T -> T -> T
|
||||||
|
applyGlobalCounted !f !x =
|
||||||
|
let !_ = globalBump f x
|
||||||
|
in applyGlobalStep f x
|
||||||
|
|
||||||
|
applyGlobalStep :: T -> T -> T
|
||||||
|
applyGlobalStep (Fork Leaf a) _ = a
|
||||||
|
applyGlobalStep (Fork (Stem a) b) c =
|
||||||
|
applyGlobalCounted (applyGlobalCounted a c) (applyGlobalCounted b c)
|
||||||
|
applyGlobalStep (Fork (Fork a _b) _c) Leaf = a
|
||||||
|
applyGlobalStep (Fork (Fork _a b) _c) (Stem u) = applyGlobalCounted b u
|
||||||
|
applyGlobalStep (Fork (Fork _a _b) c) (Fork u v) =
|
||||||
|
applyGlobalCounted (applyGlobalCounted c u) v
|
||||||
|
applyGlobalStep Leaf b = Stem b
|
||||||
|
applyGlobalStep (Stem a) b = Fork a b
|
||||||
|
|
||||||
|
runApplyGlobalCounted :: Int -> Int -> T -> T -> IO (T, ApplyStats)
|
||||||
|
runApplyGlobalCounted !interval !progress !f !x = do
|
||||||
|
resetGlobalStats interval progress
|
||||||
|
let !result = applyGlobalCounted f x
|
||||||
|
!stats <- readGlobalStats
|
||||||
|
pure (result, stats)
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Printing
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
printApplyStats :: ApplyStats -> IO ()
|
||||||
|
printApplyStats st = do
|
||||||
|
let !total = totalApplyCalls st
|
||||||
|
!uniq = M.size (uniqueApps st)
|
||||||
|
!ratio =
|
||||||
|
if uniq == 0
|
||||||
|
then 0 :: Double
|
||||||
|
else fromIntegral total / fromIntegral uniq
|
||||||
|
|
||||||
|
counts =
|
||||||
|
reverse
|
||||||
|
. L.sortBy (comparing snd)
|
||||||
|
. M.toList
|
||||||
|
$ uniqueApps st
|
||||||
|
|
||||||
|
repeated =
|
||||||
|
filter ((> 1) . snd) counts
|
||||||
|
|
||||||
|
top20 = take 20 repeated
|
||||||
|
|
||||||
|
putStrLn $ "total apply calls: " ++ show total
|
||||||
|
putStrLn $ "unique application patterns: " ++ show uniq
|
||||||
|
putStrLn $ "duplication ratio total/unique: " ++ show ratio
|
||||||
|
putStrLn $ "repeated application patterns: " ++ show (length repeated)
|
||||||
|
|
||||||
|
putStrLn "top repeated application counts:"
|
||||||
|
mapM_ printTop top20
|
||||||
|
where
|
||||||
|
short h = T.unpack (T.take 12 h)
|
||||||
|
|
||||||
|
printTop ((hf, hx), n) =
|
||||||
|
putStrLn $
|
||||||
|
" " ++ show n
|
||||||
|
++ "x apply "
|
||||||
|
++ short hf
|
||||||
|
++ " "
|
||||||
|
++ short hx
|
||||||
125
bench/Bench.hs
Normal file
125
bench/Bench.hs
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
{-# LANGUAGE BangPatterns #-}
|
||||||
|
module Main where
|
||||||
|
|
||||||
|
import Criterion.Main
|
||||||
|
import qualified Data.ByteString as BS
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
|
||||||
|
import ApplyStats (runApplyCounted, runApplyGlobalCounted, printApplyStats)
|
||||||
|
import Eval
|
||||||
|
import FileEval
|
||||||
|
import Parser
|
||||||
|
import Research
|
||||||
|
|
||||||
|
-- | Pre-process a demo file and return its AST.
|
||||||
|
loadDemo :: FilePath -> IO [TricuAST]
|
||||||
|
loadDemo = preprocessFile
|
||||||
|
|
||||||
|
-- | Evaluate a pre-processed demo to its result term.
|
||||||
|
runDemo :: [TricuAST] -> T
|
||||||
|
runDemo ast = result (evalTricu Map.empty ast)
|
||||||
|
|
||||||
|
-- | Build an environment from a library file.
|
||||||
|
loadLib :: FilePath -> IO Env
|
||||||
|
loadLib = evaluateFile
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = do
|
||||||
|
!equalityAst <- loadDemo "demos/equality.tri"
|
||||||
|
!sizeAst <- loadDemo "demos/size.tri"
|
||||||
|
!toSourceAst <- loadDemo "demos/toSource.tri"
|
||||||
|
!levelOrderAst <- loadDemo "demos/levelOrderTraversal.tri"
|
||||||
|
!patternAst <- loadDemo "demos/patternMatching.tri"
|
||||||
|
!listLib <- loadLib "lib/list.tri"
|
||||||
|
|
||||||
|
-- Stress benchmark environment: Arboricx parser + size + toSource
|
||||||
|
!arboricxLib <- loadLib "lib/arboricx-dispatch.tri"
|
||||||
|
!sizeEnv <- evaluateFileWithContext arboricxLib "demos/size.tri"
|
||||||
|
!toSourceEnv <- evaluateFileWithContext sizeEnv "demos/toSource.tri"
|
||||||
|
|
||||||
|
-- Print apply stats for toSource not?
|
||||||
|
let Just toSource = Map.lookup "toSource" toSourceEnv
|
||||||
|
Just notTerm = Map.lookup "not?" toSourceEnv
|
||||||
|
(_result, stats) = runApplyCounted toSource notTerm
|
||||||
|
printApplyStats stats
|
||||||
|
|
||||||
|
-- Print apply stats for readArboricxContainer against id.arboricx
|
||||||
|
!idBundleBytes <- BS.readFile "test/fixtures/id.arboricx"
|
||||||
|
let Just readContainer = Map.lookup "readArboricxContainer" sizeEnv
|
||||||
|
bundleTree = ofBytes idBundleBytes
|
||||||
|
(_result2, stats2) <- runApplyGlobalCounted 100000 1000000 readContainer bundleTree
|
||||||
|
printApplyStats stats2
|
||||||
|
|
||||||
|
defaultMain
|
||||||
|
[ bgroup "demos"
|
||||||
|
[ bench "equality" $ whnf runDemo equalityAst
|
||||||
|
, bench "size" $ whnf runDemo sizeAst
|
||||||
|
, bench "toSource" $ whnf runDemo toSourceAst
|
||||||
|
, bench "levelOrderTraversal" $ whnf runDemo levelOrderAst
|
||||||
|
, bench "patternMatching" $ whnf runDemo patternAst
|
||||||
|
]
|
||||||
|
|
||||||
|
, bgroup "lib/list.tri"
|
||||||
|
[ bench "append strings" $ whnf
|
||||||
|
(result . evalTricu listLib . parseTricu)
|
||||||
|
"append \"Hello, \" \"world!\""
|
||||||
|
, bench "map over 3 elements" $ whnf
|
||||||
|
(result . evalTricu listLib . parseTricu)
|
||||||
|
"head (tail (map (a : (t t t)) [(t) (t) (t)]))"
|
||||||
|
, bench "equal? same" $ whnf
|
||||||
|
(result . evalTricu listLib . parseTricu)
|
||||||
|
"equal? (t t t) (t t t)"
|
||||||
|
, bench "equal? different" $ whnf
|
||||||
|
(result . evalTricu listLib . parseTricu)
|
||||||
|
"equal? (t t) (t t t)"
|
||||||
|
, bench "triage Leaf" $ whnf
|
||||||
|
(result . evalTricu listLib . parseTricu)
|
||||||
|
"test t"
|
||||||
|
, bench "triage Stem" $ whnf
|
||||||
|
(result . evalTricu listLib . parseTricu)
|
||||||
|
"test (t t)"
|
||||||
|
, bench "triage Fork" $ whnf
|
||||||
|
(result . evalTricu listLib . parseTricu)
|
||||||
|
"test (t t t)"
|
||||||
|
, bench "not? true" $ whnf
|
||||||
|
(result . evalTricu listLib . parseTricu)
|
||||||
|
"not? (t t)"
|
||||||
|
, bench "not? false" $ whnf
|
||||||
|
(result . evalTricu listLib . parseTricu)
|
||||||
|
"not? t"
|
||||||
|
]
|
||||||
|
|
||||||
|
, bgroup "stress"
|
||||||
|
[ bench "size runArboricxTyped" $ whnf
|
||||||
|
(result . evalTricu sizeEnv . parseTricu)
|
||||||
|
"size runArboricxTyped"
|
||||||
|
, bench "equal? runArboricxTyped runArboricxTyped" $ whnf
|
||||||
|
(result . evalTricu sizeEnv . parseTricu)
|
||||||
|
"equal? runArboricxTyped runArboricxTyped"
|
||||||
|
, bench "size readArboricxBundle" $ whnf
|
||||||
|
(result . evalTricu sizeEnv . parseTricu)
|
||||||
|
"size readArboricxBundle"
|
||||||
|
, bench "equal? readArboricxBundle readArboricxBundle" $ whnf
|
||||||
|
(result . evalTricu sizeEnv . parseTricu)
|
||||||
|
"equal? readArboricxBundle readArboricxBundle"
|
||||||
|
]
|
||||||
|
|
||||||
|
, bgroup "raw-apply"
|
||||||
|
[ bench "rule-1 (Fork Leaf a) b" $ whnf
|
||||||
|
(\n -> apply (Fork Leaf (ofNumber n)) (ofNumber 42))
|
||||||
|
1000
|
||||||
|
, bench "rule-2 (Fork (Stem a) b) c" $ whnf
|
||||||
|
(\n -> apply (Fork (Stem (ofNumber n)) (ofNumber n)) (ofNumber 42))
|
||||||
|
1000
|
||||||
|
, bench "rule-3a (Fork (Fork a b) c) Leaf" $ whnf
|
||||||
|
(\n -> apply (Fork (Fork (ofNumber n) (ofNumber n)) (ofNumber n)) Leaf)
|
||||||
|
1000
|
||||||
|
, bench "rule-3b (Fork (Fork a b) c) (Stem u)" $ whnf
|
||||||
|
(\n -> apply (Fork (Fork (ofNumber n) (ofNumber n)) (ofNumber n)) (Stem Leaf))
|
||||||
|
1000
|
||||||
|
, bench "rule-3c (Fork (Fork a b) c) (Fork u v)" $ whnf
|
||||||
|
(\n -> apply (Fork (Fork (ofNumber n) (ofNumber n)) (ofNumber n)) (Fork Leaf Leaf))
|
||||||
|
1000
|
||||||
|
]
|
||||||
|
|
||||||
|
]
|
||||||
26
demos/runArboricxBundle.tri
Normal file
26
demos/runArboricxBundle.tri
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
!import "../lib/base.tri" !Local
|
||||||
|
!import "../lib/list.tri" !Local
|
||||||
|
!import "../lib/io.tri" !Local
|
||||||
|
!import "../lib/arboricx.tri" !Local
|
||||||
|
|
||||||
|
-- Read an Arboricx bundle from disk and execute it.
|
||||||
|
-- This demo loads test/fixtures/id.arboricx and applies the
|
||||||
|
-- default export to the string "hi". The id bundle simply
|
||||||
|
-- returns its argument, so the expected output is:
|
||||||
|
-- hi
|
||||||
|
--
|
||||||
|
-- Run with --allow-read test/fixtures/id.arboricx or --unsafe-io.
|
||||||
|
|
||||||
|
runBundle = (path arg :
|
||||||
|
bind (readFile path)
|
||||||
|
(result :
|
||||||
|
matchResult
|
||||||
|
(err rest : putStrLn "ERROR: Could not read bundle file")
|
||||||
|
(bundleBytes rest :
|
||||||
|
matchResult
|
||||||
|
(err rest : putStrLn "ERROR: Could not execute bundle")
|
||||||
|
(value rest : putStrLn value)
|
||||||
|
(runArboricx bundleBytes arg))
|
||||||
|
result))
|
||||||
|
|
||||||
|
main = io (runBundle "test/fixtures/id.arboricx" "hi")
|
||||||
193
docs/zig-io.md
Normal file
193
docs/zig-io.md
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
# Zig Interaction-Tree IO Runtime Plan
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Port the Haskell `IODriver` interaction-tree system into the Zig host so that:
|
||||||
|
|
||||||
|
1. The Zig CLI (`tricu-zig`) can execute tricu programs with effects (`putStr`, `readFile`, `fork`, etc.).
|
||||||
|
2. The C FFI (`libarboricx`) exposes a single `arb_run_io` call, giving every language host (C, Python, PHP, Node) turnkey IO without reimplementing the protocol.
|
||||||
|
3. The fast native reduction path (currently ~0.005s for `id "hello"`) is used for pure computation; IO syscalls happen only at effect boundaries.
|
||||||
|
|
||||||
|
## Current State
|
||||||
|
|
||||||
|
| Host | Reduction Speed | IO Support |
|
||||||
|
|------|----------------|------------|
|
||||||
|
| Haskell interpreter | ~1.7s for `runArboricxTyped` demo | Full `IODriver.hs` with scheduler, async, permissions |
|
||||||
|
| Zig native | ~0.005s for `append` | None — pure reduction only |
|
||||||
|
| Zig kernel | ~0.235s for `id.arboricx` | None — runs self-hosted parser, no effects |
|
||||||
|
| C / Python / PHP FFI | Native Zig speed | None — can construct and reduce, cannot interpret interaction trees |
|
||||||
|
|
||||||
|
The Haskell `IODriver` is ~500 lines of stateful code (scheduler, frame stack, permission checks, async lifecycle). Replicating it in every host language is a maintenance hazard. We will implement it **once** in Zig and share it through the C ABI.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Layer 1 — Tree Inspection Primitives (C FFI)
|
||||||
|
|
||||||
|
Minimal functions that let C (or other FFIs) inspect raw tree shape. Used internally by the driver, and exposed for non-POSIX hosts that need custom effect handlers.
|
||||||
|
|
||||||
|
```c
|
||||||
|
int arb_is_leaf(arb_ctx_t* ctx, uint32_t root);
|
||||||
|
int arb_is_stem(arb_ctx_t* ctx, uint32_t root);
|
||||||
|
int arb_is_fork(arb_ctx_t* ctx, uint32_t root);
|
||||||
|
int arb_get_stem_child(arb_ctx_t* ctx, uint32_t root, uint32_t* out);
|
||||||
|
int arb_get_fork_children(arb_ctx_t* ctx, uint32_t root,
|
||||||
|
uint32_t* out_left, uint32_t* out_right);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Layer 2 — POSIX IO Driver (C FFI)
|
||||||
|
|
||||||
|
A single high-level call that runs the full interaction-tree loop:
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct {
|
||||||
|
int allow_read_all;
|
||||||
|
int allow_write_all;
|
||||||
|
const char** allowed_read_paths;
|
||||||
|
size_t allowed_read_count;
|
||||||
|
const char** allowed_write_paths;
|
||||||
|
size_t allowed_write_count;
|
||||||
|
} arb_io_perms_t;
|
||||||
|
|
||||||
|
// Reduce → decode action → perform syscall → feed result → repeat until pure.
|
||||||
|
// Returns the final pure tree value.
|
||||||
|
uint32_t arb_run_io(arb_ctx_t* ctx, uint32_t program,
|
||||||
|
const arb_io_perms_t* perms);
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the only call 99% of hosts need. It contains the exact same logic as `IODriver.hs`:
|
||||||
|
|
||||||
|
- **Frame stack** — `BindFrame` (sequencing) and `LocalFrame` (environment scoping)
|
||||||
|
- **Runtime** — permissions, environment tree, mutable state tree
|
||||||
|
- **Action dispatch** — decode the tag (pure, bind, putStr, getLine, readFile, writeFile, ask, local, get, put, fork, await, yield, sleep)
|
||||||
|
- **Scheduler** — runnable queue, blocked tasks, sleeping tasks, wake-on-completion, deadlock detection
|
||||||
|
- **Error protocol** — ok/err pairs with numeric codes
|
||||||
|
|
||||||
|
### Zig CLI Integration
|
||||||
|
|
||||||
|
Add `--io` and `--unsafe-io` flags to `tricu-zig`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Safe mode — no filesystem access (default when --io is used)
|
||||||
|
tricu-zig --io greet.arboricx
|
||||||
|
|
||||||
|
# Unsafe mode — full POSIX access (development / local scripts)
|
||||||
|
tricu-zig --io --unsafe-io writeThenRead.arboricx
|
||||||
|
|
||||||
|
# Specific paths
|
||||||
|
# (future: --allow-read ./foo --allow-write ./bar)
|
||||||
|
```
|
||||||
|
|
||||||
|
Under `--io`, the CLI loads the bundle, reduces it once to WHNF, then passes the root to `arb_run_io` instead of eagerly decoding the final value.
|
||||||
|
|
||||||
|
## Implementation Stages
|
||||||
|
|
||||||
|
### Stage 1 — Tree Inspection Primitives
|
||||||
|
|
||||||
|
Add the five inspection functions to `ext/zig/src/c_abi.zig` and `ext/zig/include/arboricx.h`. No logic changes to reduction; these just read arena node tags.
|
||||||
|
|
||||||
|
**Acceptance:** A C test program can walk an arbitrary tree built with `arb_fork`/`arb_stem`/`arb_leaf` without knowing the arena internals.
|
||||||
|
|
||||||
|
### Stage 2 — IO Protocol Decoder
|
||||||
|
|
||||||
|
Write `ext/zig/src/io_driver.zig` containing:
|
||||||
|
|
||||||
|
- `decodeAction` — inspect a reduced tree and identify the action tag (pure=0, bind=1, putStr=10, …)
|
||||||
|
- `isIOSentinel` — verify `"tricuIO"` sentinel and version
|
||||||
|
- `makePure`, `makeOkResult`, `makeErrResult` — construct standard response trees
|
||||||
|
|
||||||
|
These are pure Zig functions with no syscalls. They mirror `IODriver.hs` logic but operate on arena indices.
|
||||||
|
|
||||||
|
**Acceptance:** Unit tests decode each action type correctly from trees built via codecs.
|
||||||
|
|
||||||
|
### Stage 3 — Synchronous IO Loop
|
||||||
|
|
||||||
|
Implement the core driver loop with a frame stack:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
while (true) {
|
||||||
|
current = reduce.reduce(current, scratch_arena, fuel);
|
||||||
|
if (isIOSentinel(current)) |action| {
|
||||||
|
switch (decodeAction(action)) {
|
||||||
|
.pure => { /* pop frame or return */ },
|
||||||
|
.bind => { /* push BindFrame, recurse into left */ },
|
||||||
|
.putStr => { /* write stdout, continue with Leaf */ },
|
||||||
|
.getLine => { /* read stdin, continue with string */ },
|
||||||
|
// ... etc
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return current; // pure result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Support synchronous actions only: `pure`, `bind`, `putStr`, `getLine`, `readFile`, `writeFile`, `ask`, `local`, `get`, `put`.
|
||||||
|
|
||||||
|
**Acceptance:** `greet.tri` and `writeThenRead.tri` run correctly through `tricu-zig --io`.
|
||||||
|
|
||||||
|
### Stage 4 — Scheduler and Async Actions
|
||||||
|
|
||||||
|
Add the task scheduler for `fork`, `await`, `yield`, `sleep`:
|
||||||
|
|
||||||
|
- `Runnable` queue (FIFO)
|
||||||
|
- `BlockedOn` map (task → blocked task ID)
|
||||||
|
- `Sleeping` map (task → wake time)
|
||||||
|
- Round-robin scheduling with `yield` and `sleep` support
|
||||||
|
- Deadlock detection when no runnable tasks remain and no sleepers
|
||||||
|
|
||||||
|
This mirrors `IODriver.hs` exactly, including task handle encoding (`Fork("task", n)`).
|
||||||
|
|
||||||
|
**Acceptance:** `demos/interactionTrees/forkAwait.tri` and `yield.tri` pass.
|
||||||
|
|
||||||
|
### Stage 5 — Permission System
|
||||||
|
|
||||||
|
Port path canonicalization and permission checks from Haskell:
|
||||||
|
|
||||||
|
- Syntactic normalization (resolve `.`, reject `..`)
|
||||||
|
- `--unsafe-io` bypass (allow all)
|
||||||
|
- `--allow-read PATH` / `--allow-write PATH` allowlists
|
||||||
|
- Error code 20 (`errPolicyDeny`) on violation
|
||||||
|
|
||||||
|
**Acceptance:** File operations outside allowed paths return err pairs, not crashes.
|
||||||
|
|
||||||
|
### Stage 6 — FFI Integration and Host Rollout
|
||||||
|
|
||||||
|
- Expose `arb_run_io` in the C header
|
||||||
|
- Update Python FFI test to verify IO round-trip
|
||||||
|
- Update PHP wrapper to support `--io`
|
||||||
|
- Document the two-layer model for future hosts (use `arb_run_io` for POSIX, Layer 1 primitives for custom runtimes)
|
||||||
|
|
||||||
|
**Acceptance:** Every existing FFI test still passes; new IO test passes in Python.
|
||||||
|
|
||||||
|
## Design Decisions
|
||||||
|
|
||||||
|
### Why baked-in POSIX effects?
|
||||||
|
|
||||||
|
- Most hosts (C, Python, PHP, native CLI) want real stdout/stdin/files.
|
||||||
|
- One canonical implementation avoids divergence.
|
||||||
|
- The Haskell `IODriver.hs` remains the reference spec; the Zig driver is the production runtime.
|
||||||
|
|
||||||
|
### Why not callback-based by default?
|
||||||
|
|
||||||
|
Callbacks add complexity for the common case. If a non-POSIX host (e.g., browser JS) needs custom effects, it can use the Layer 1 inspection primitives to build a ~50-line shim without reimplementing the scheduler. We can add `arb_run_io_with_callbacks` later if demand exists.
|
||||||
|
|
||||||
|
### Why not implement in every host language?
|
||||||
|
|
||||||
|
The Haskell `IODriver` is subtle: frame stack unwinding, async lifecycle, deadlock detection, path canonicalization, error code protocol. Bugs in any reimplementation would fracture the language ecosystem. A shared native driver is the only maintainable answer.
|
||||||
|
|
||||||
|
## Risks and Open Questions
|
||||||
|
|
||||||
|
1. **Fuel exhaustion during IO loops** — `arb_run_io` internally calls `reduce.reduce` with a fuel parameter. Should it accept a total fuel budget, or reset fuel per reduction step? The Haskell side has no fuel limit; we may want `arb_run_io_unlimited` and `arb_run_io_fueled` variants.
|
||||||
|
|
||||||
|
2. **State threading** — The Haskell driver threads an environment and mutable state tree through the runtime. These are opaque `T` values manipulated by tricu code. The Zig driver must preserve them exactly across scheduler switches.
|
||||||
|
|
||||||
|
3. **Binary vs text I/O** — `readFile` currently returns bytes (via `ofBytes` / `toString` in Haskell). The Zig driver must match the encoding exactly so that tricu code sees the same values in both hosts.
|
||||||
|
|
||||||
|
4. **Error parity** — Every error code (1–99) and its corresponding tree shape must match Haskell exactly. Divergence here breaks cross-host compatibility.
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- `tricu-zig --io demos/interactionTrees/greet.tri` prints `Hello, tricu` in <10ms.
|
||||||
|
- `tricu-zig --io --unsafe-io demos/interactionTrees/writeThenRead.tri` writes and reads back a temp file correctly.
|
||||||
|
- `tricu-zig --io --unsafe-io demos/interactionTrees/forkAwait.tri` completes with correct async results.
|
||||||
|
- Python FFI can call `arb_run_io` and observe stdout from a tricu program.
|
||||||
|
- No regression in pure-reduction benchmarks (native path still ~0.005s for `id`).
|
||||||
@@ -31,6 +31,8 @@ pub fn build(b: *std.Build) void {
|
|||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
});
|
});
|
||||||
exe_mod.addImport("kernel_embed", kernel_mod);
|
exe_mod.addImport("kernel_embed", kernel_mod);
|
||||||
|
exe_mod.link_libc = true;
|
||||||
|
exe_mod.linkSystemLibrary("uv", .{});
|
||||||
const exe = b.addExecutable(.{
|
const exe = b.addExecutable(.{
|
||||||
.name = "tricu-zig",
|
.name = "tricu-zig",
|
||||||
.root_module = exe_mod,
|
.root_module = exe_mod,
|
||||||
@@ -50,6 +52,8 @@ pub fn build(b: *std.Build) void {
|
|||||||
});
|
});
|
||||||
lib_mod.pic = true;
|
lib_mod.pic = true;
|
||||||
lib_mod.addImport("kernel_embed", kernel_mod);
|
lib_mod.addImport("kernel_embed", kernel_mod);
|
||||||
|
lib_mod.link_libc = true;
|
||||||
|
lib_mod.linkSystemLibrary("uv", .{});
|
||||||
const static_lib = b.addLibrary(.{
|
const static_lib = b.addLibrary(.{
|
||||||
.name = "arboricx",
|
.name = "arboricx",
|
||||||
.root_module = lib_mod,
|
.root_module = lib_mod,
|
||||||
|
|||||||
@@ -40,6 +40,25 @@ int arb_to_bool(arb_ctx_t* ctx, uint32_t root, int* out);
|
|||||||
int arb_unwrap_result(arb_ctx_t* ctx, uint32_t root, int* out_ok, uint32_t* out_value, uint32_t* out_rest);
|
int arb_unwrap_result(arb_ctx_t* ctx, uint32_t root, int* out_ok, uint32_t* out_value, uint32_t* out_rest);
|
||||||
int arb_unwrap_host_value(arb_ctx_t* ctx, uint32_t root, uint64_t* out_tag, uint32_t* out_payload);
|
int arb_unwrap_host_value(arb_ctx_t* ctx, uint32_t root, uint64_t* out_tag, uint32_t* out_payload);
|
||||||
|
|
||||||
|
/* Tree inspection (Layer 1 — for custom IO drivers and non-POSIX hosts) */
|
||||||
|
int arb_is_leaf(arb_ctx_t* ctx, uint32_t root);
|
||||||
|
int arb_is_stem(arb_ctx_t* ctx, uint32_t root);
|
||||||
|
int arb_is_fork(arb_ctx_t* ctx, uint32_t root);
|
||||||
|
int arb_is_app(arb_ctx_t* ctx, uint32_t root);
|
||||||
|
int arb_get_stem_child(arb_ctx_t* ctx, uint32_t root, uint32_t* out);
|
||||||
|
int arb_get_fork_children(arb_ctx_t* ctx, uint32_t root,
|
||||||
|
uint32_t* out_left, uint32_t* out_right);
|
||||||
|
int arb_get_app_func_arg(arb_ctx_t* ctx, uint32_t root,
|
||||||
|
uint32_t* out_func, uint32_t* out_arg);
|
||||||
|
|
||||||
|
/* IO driver (Layer 2 — POSIX interaction-tree runtime) */
|
||||||
|
typedef struct {
|
||||||
|
int allow_read_all;
|
||||||
|
int allow_write_all;
|
||||||
|
} arb_io_perms_t;
|
||||||
|
|
||||||
|
uint32_t arb_run_io(arb_ctx_t* ctx, uint32_t program, const arb_io_perms_t* perms);
|
||||||
|
|
||||||
/* Kernel entrypoints */
|
/* Kernel entrypoints */
|
||||||
uint32_t arb_kernel_root(arb_ctx_t* ctx);
|
uint32_t arb_kernel_root(arb_ctx_t* ctx);
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const reduce = @import("reduce.zig");
|
|||||||
const codecs = @import("codecs.zig");
|
const codecs = @import("codecs.zig");
|
||||||
const kernel = @import("kernel.zig");
|
const kernel = @import("kernel.zig");
|
||||||
const bundle = @import("bundle.zig");
|
const bundle = @import("bundle.zig");
|
||||||
|
const io_driver = @import("io_driver.zig");
|
||||||
|
|
||||||
/// Opaque handle for the C API. Layout is not exposed to C.
|
/// Opaque handle for the C API. Layout is not exposed to C.
|
||||||
/// Holds a persistent arena for user-built terms and the kernel.
|
/// Holds a persistent arena for user-built terms and the kernel.
|
||||||
@@ -59,6 +60,57 @@ export fn arb_app(ctx: *ArbCtx, func: u32, arg: u32) u32 {
|
|||||||
return ctx.arena.alloc(.{ .app = .{ .func = func, .arg = arg } }) catch 0;
|
return ctx.arena.alloc(.{ .app = .{ .func = func, .arg = arg } }) catch 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tree inspection (Layer 1 — for custom IO drivers and non-POSIX hosts)
|
||||||
|
// All return 1 on success / true, 0 on failure / false.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export fn arb_is_leaf(ctx: *ArbCtx, root: u32) c_int {
|
||||||
|
if (root >= ctx.arena.len()) return 0;
|
||||||
|
return if (ctx.arena.nodes.items[root] == .leaf) 1 else 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn arb_is_stem(ctx: *ArbCtx, root: u32) c_int {
|
||||||
|
if (root >= ctx.arena.len()) return 0;
|
||||||
|
return if (ctx.arena.nodes.items[root] == .stem) 1 else 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn arb_is_fork(ctx: *ArbCtx, root: u32) c_int {
|
||||||
|
if (root >= ctx.arena.len()) return 0;
|
||||||
|
return if (ctx.arena.nodes.items[root] == .fork) 1 else 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn arb_is_app(ctx: *ArbCtx, root: u32) c_int {
|
||||||
|
if (root >= ctx.arena.len()) return 0;
|
||||||
|
return if (ctx.arena.nodes.items[root] == .app) 1 else 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn arb_get_stem_child(ctx: *ArbCtx, root: u32, out: *u32) c_int {
|
||||||
|
if (root >= ctx.arena.len()) return 0;
|
||||||
|
const node = ctx.arena.nodes.items[root];
|
||||||
|
if (node != .stem) return 0;
|
||||||
|
out.* = node.stem.child;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn arb_get_fork_children(ctx: *ArbCtx, root: u32, out_left: *u32, out_right: *u32) c_int {
|
||||||
|
if (root >= ctx.arena.len()) return 0;
|
||||||
|
const node = ctx.arena.nodes.items[root];
|
||||||
|
if (node != .fork) return 0;
|
||||||
|
out_left.* = node.fork.left;
|
||||||
|
out_right.* = node.fork.right;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn arb_get_app_func_arg(ctx: *ArbCtx, root: u32, out_func: *u32, out_arg: *u32) c_int {
|
||||||
|
if (root >= ctx.arena.len()) return 0;
|
||||||
|
const node = ctx.arena.nodes.items[root];
|
||||||
|
if (node != .app) return 0;
|
||||||
|
out_func.* = node.app.func;
|
||||||
|
out_arg.* = node.app.arg;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Reduction
|
// Reduction
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -157,6 +209,23 @@ export fn arb_unwrap_host_value(ctx: *ArbCtx, root: u32, out_tag: *u64, out_payl
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// IO driver (Layer 2 — POSIX interaction-tree runtime)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub const arb_io_perms_t = extern struct {
|
||||||
|
allow_read_all: c_int,
|
||||||
|
allow_write_all: c_int,
|
||||||
|
};
|
||||||
|
|
||||||
|
export fn arb_run_io(ctx: *ArbCtx, program: u32, perms: ?*const arb_io_perms_t) u32 {
|
||||||
|
const zig_perms = if (perms) |p| io_driver.IOPerms{
|
||||||
|
.allow_read_all = p.allow_read_all != 0,
|
||||||
|
.allow_write_all = p.allow_write_all != 0,
|
||||||
|
} else io_driver.IOPerms{};
|
||||||
|
return io_driver.runIO(ctx.gpa, &ctx.arena, program, zig_perms) catch 0;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Kernel entrypoints
|
// Kernel entrypoints
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
845
ext/zig/src/io_driver.zig
Normal file
845
ext/zig/src/io_driver.zig
Normal file
@@ -0,0 +1,845 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Arena = @import("arena.zig").Arena;
|
||||||
|
const codecs = @import("codecs.zig");
|
||||||
|
const reduce = @import("reduce.zig");
|
||||||
|
const tree = @import("tree.zig");
|
||||||
|
|
||||||
|
const c = @cImport({
|
||||||
|
@cInclude("uv.h");
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Action tag constants (must match lib/io.tri and IODriver.hs)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub const ActionTag = enum(u8) {
|
||||||
|
pure = 0,
|
||||||
|
bind = 1,
|
||||||
|
putStr = 10,
|
||||||
|
getLine = 11,
|
||||||
|
readFile = 20,
|
||||||
|
writeFile = 21,
|
||||||
|
ask = 30,
|
||||||
|
local = 31,
|
||||||
|
get = 40,
|
||||||
|
put = 41,
|
||||||
|
fork = 60,
|
||||||
|
await = 61,
|
||||||
|
yield = 62,
|
||||||
|
sleep = 63,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Action = union(ActionTag) {
|
||||||
|
pure: u32,
|
||||||
|
bind: struct { left: u32, k: u32 },
|
||||||
|
putStr: u32,
|
||||||
|
getLine,
|
||||||
|
readFile: u32,
|
||||||
|
writeFile: struct { path: u32, contents: u32 },
|
||||||
|
ask,
|
||||||
|
local: struct { f: u32, action: u32 },
|
||||||
|
get,
|
||||||
|
put: u32,
|
||||||
|
fork: u32,
|
||||||
|
await: u32,
|
||||||
|
yield,
|
||||||
|
sleep: u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Error codes (must match IODriver.hs)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const ERR_DOES_NOT_EXIST: u64 = 1;
|
||||||
|
const ERR_PERMISSION: u64 = 2;
|
||||||
|
const ERR_ALREADY_EXISTS: u64 = 3;
|
||||||
|
const ERR_IO_OTHER: u64 = 4;
|
||||||
|
const ERR_POLICY_DENY: u64 = 20;
|
||||||
|
const ERR_INVALID_ACTION: u64 = 40;
|
||||||
|
const ERR_INVALID_STRING: u64 = 41;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Permissions
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub const IOPerms = struct {
|
||||||
|
allow_read_all: bool = false,
|
||||||
|
allow_write_all: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// IO sentinel detection
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub fn isIOSentinel(arena: *Arena, root: u32) !?u32 {
|
||||||
|
const node = arena.get(root);
|
||||||
|
if (node.* != .fork) return null;
|
||||||
|
|
||||||
|
const sentinel = node.fork.left;
|
||||||
|
const rest = node.fork.right;
|
||||||
|
|
||||||
|
const sentinel_str = try codecs.toString(arena, sentinel);
|
||||||
|
defer {
|
||||||
|
if (sentinel_str) |s| {
|
||||||
|
arena.allocator.free(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sentinel_str == null) return null;
|
||||||
|
if (!std.mem.eql(u8, sentinel_str.?, "tricuIO")) return null;
|
||||||
|
|
||||||
|
const rest_node = arena.get(rest);
|
||||||
|
if (rest_node.* != .fork) return null;
|
||||||
|
|
||||||
|
const version_num = try codecs.toNumber(arena, rest_node.fork.left);
|
||||||
|
if (version_num == null or version_num.? != 1) return null;
|
||||||
|
|
||||||
|
return rest_node.fork.right;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Action decoding
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub fn decodeAction(arena: *Arena, root: u32) !?Action {
|
||||||
|
const node = arena.get(root);
|
||||||
|
if (node.* != .fork) return null;
|
||||||
|
|
||||||
|
const tag_num = try codecs.toNumber(arena, node.fork.left);
|
||||||
|
if (tag_num == null) return null;
|
||||||
|
|
||||||
|
const tag: ActionTag = switch (tag_num.?) {
|
||||||
|
0 => .pure,
|
||||||
|
1 => .bind,
|
||||||
|
10 => .putStr,
|
||||||
|
11 => .getLine,
|
||||||
|
20 => .readFile,
|
||||||
|
21 => .writeFile,
|
||||||
|
30 => .ask,
|
||||||
|
31 => .local,
|
||||||
|
40 => .get,
|
||||||
|
41 => .put,
|
||||||
|
60 => .fork,
|
||||||
|
61 => .await,
|
||||||
|
62 => .yield,
|
||||||
|
63 => .sleep,
|
||||||
|
else => return null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const payload = node.fork.right;
|
||||||
|
|
||||||
|
return switch (tag) {
|
||||||
|
.pure => Action{ .pure = payload },
|
||||||
|
.bind => blk: {
|
||||||
|
const payload_node = arena.get(payload);
|
||||||
|
if (payload_node.* != .fork) return null;
|
||||||
|
break :blk Action{ .bind = .{ .left = payload_node.fork.left, .k = payload_node.fork.right } };
|
||||||
|
},
|
||||||
|
.putStr => Action{ .putStr = payload },
|
||||||
|
.getLine => Action.getLine,
|
||||||
|
.readFile => Action{ .readFile = payload },
|
||||||
|
.writeFile => blk: {
|
||||||
|
const payload_node = arena.get(payload);
|
||||||
|
if (payload_node.* != .fork) return null;
|
||||||
|
break :blk Action{ .writeFile = .{ .path = payload_node.fork.left, .contents = payload_node.fork.right } };
|
||||||
|
},
|
||||||
|
.ask => Action.ask,
|
||||||
|
.local => blk: {
|
||||||
|
const payload_node = arena.get(payload);
|
||||||
|
if (payload_node.* != .fork) return null;
|
||||||
|
break :blk Action{ .local = .{ .f = payload_node.fork.left, .action = payload_node.fork.right } };
|
||||||
|
},
|
||||||
|
.get => Action.get,
|
||||||
|
.put => Action{ .put = payload },
|
||||||
|
.fork => Action{ .fork = payload },
|
||||||
|
.await => Action{ .await = payload },
|
||||||
|
.yield => Action.yield,
|
||||||
|
.sleep => Action{ .sleep = payload },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Response tree constructors
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub fn makePure(arena: *Arena, val: u32) !u32 {
|
||||||
|
const tag = try codecs.ofNumber(arena, 0);
|
||||||
|
return try arena.alloc(.{ .fork = .{ .left = tag, .right = val } });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn makeOkResult(arena: *Arena, val: u32) !u32 {
|
||||||
|
const ok_tag = try arena.alloc(.{ .stem = .{ .child = try arena.alloc(.leaf) } });
|
||||||
|
const val_pair = try arena.alloc(.{ .fork = .{ .left = val, .right = try arena.alloc(.leaf) } });
|
||||||
|
return try arena.alloc(.{ .fork = .{ .left = ok_tag, .right = val_pair } });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn makeErrResult(arena: *Arena, code: u64) !u32 {
|
||||||
|
const code_tree = try codecs.ofNumber(arena, code);
|
||||||
|
const code_pair = try arena.alloc(.{ .fork = .{ .left = code_tree, .right = try arena.alloc(.leaf) } });
|
||||||
|
return try arena.alloc(.{ .fork = .{ .left = try arena.alloc(.leaf), .right = code_pair } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Frame stack and runtime
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const Frame = union(enum) {
|
||||||
|
bind: u32, // continuation k
|
||||||
|
local: u32, // old env
|
||||||
|
};
|
||||||
|
|
||||||
|
const Runtime = struct {
|
||||||
|
env: u32,
|
||||||
|
state: u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helper: reduce a term in a scratch arena and copy the result back
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn reduceInScratch(gpa: std.mem.Allocator, arena: *Arena, term: u32) !u32 {
|
||||||
|
var scratch = Arena.init(gpa);
|
||||||
|
defer scratch.deinit();
|
||||||
|
const scratch_root = try tree.copyTree(arena.nodes.items, &scratch, term);
|
||||||
|
const scratch_result = try reduce.reduce(scratch_root, &scratch, std.math.maxInt(u64));
|
||||||
|
return try tree.copyTree(scratch.nodes.items, arena, scratch_result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Task
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const Task = struct {
|
||||||
|
id: u64,
|
||||||
|
parent: ?*Task,
|
||||||
|
frames: std.ArrayList(Frame),
|
||||||
|
runtime: Runtime,
|
||||||
|
current: u32,
|
||||||
|
status: enum { runnable, blocked, completed },
|
||||||
|
result: ?u32,
|
||||||
|
waiting_for: ?u64,
|
||||||
|
|
||||||
|
fn init(gpa: std.mem.Allocator, id: u64, parent: ?*Task, env: u32, state: u32, current: u32) !*Task {
|
||||||
|
const task = try gpa.create(Task);
|
||||||
|
task.* = .{
|
||||||
|
.id = id,
|
||||||
|
.parent = parent,
|
||||||
|
.frames = std.ArrayList(Frame).empty,
|
||||||
|
.runtime = .{ .env = env, .state = state },
|
||||||
|
.current = current,
|
||||||
|
.status = .runnable,
|
||||||
|
.result = null,
|
||||||
|
.waiting_for = null,
|
||||||
|
};
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deinit(self: *Task, gpa: std.mem.Allocator) void {
|
||||||
|
self.frames.deinit(gpa);
|
||||||
|
gpa.destroy(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
// finishValue processes a value through the frame stack.
|
||||||
|
// Returns true if the task has completed (no more frames).
|
||||||
|
fn finishValue(self: *Task, arena: *Arena, value: u32) !bool {
|
||||||
|
if (self.frames.pop()) |frame| {
|
||||||
|
switch (frame) {
|
||||||
|
.bind => |k| {
|
||||||
|
self.current = try arena.alloc(.{ .app = .{ .func = k, .arg = value } });
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
.local => |old_env| {
|
||||||
|
self.runtime.env = old_env;
|
||||||
|
self.current = try makePure(arena, value);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.current = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Scheduler
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const Scheduler = struct {
|
||||||
|
gpa: std.mem.Allocator,
|
||||||
|
loop: *c.uv_loop_t,
|
||||||
|
arena: *Arena,
|
||||||
|
tasks: std.ArrayList(*Task),
|
||||||
|
runnable: std.ArrayList(*Task),
|
||||||
|
next_id: u64,
|
||||||
|
perms: IOPerms,
|
||||||
|
|
||||||
|
fn init(gpa: std.mem.Allocator, loop: *c.uv_loop_t, arena: *Arena, perms: IOPerms) !Scheduler {
|
||||||
|
const sched = Scheduler{
|
||||||
|
.gpa = gpa,
|
||||||
|
.loop = loop,
|
||||||
|
.arena = arena,
|
||||||
|
.tasks = std.ArrayList(*Task).empty,
|
||||||
|
.runnable = std.ArrayList(*Task).empty,
|
||||||
|
.next_id = 1,
|
||||||
|
.perms = perms,
|
||||||
|
};
|
||||||
|
return sched;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deinit(self: *Scheduler) void {
|
||||||
|
for (self.tasks.items) |task| {
|
||||||
|
task.deinit(self.gpa);
|
||||||
|
}
|
||||||
|
self.tasks.deinit(self.gpa);
|
||||||
|
self.runnable.deinit(self.gpa);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn createTask(self: *Scheduler, parent: ?*Task, env: u32, state: u32, current: u32) !*Task {
|
||||||
|
const id = self.next_id;
|
||||||
|
self.next_id += 1;
|
||||||
|
const task = try Task.init(self.gpa, id, parent, env, state, current);
|
||||||
|
try self.tasks.append(self.gpa, task);
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(self: *Scheduler) !void {
|
||||||
|
while (true) {
|
||||||
|
if (self.runnable.items.len > 0) {
|
||||||
|
const task = self.runnable.orderedRemove(0);
|
||||||
|
try self.stepTask(task);
|
||||||
|
} else if (self.hasPendingHandles()) {
|
||||||
|
_ = c.uv_run(self.loop, c.UV_RUN_ONCE);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hasPendingHandles(self: *Scheduler) bool {
|
||||||
|
return c.uv_loop_alive(self.loop) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn completeTask(self: *Scheduler, task: *Task) !void {
|
||||||
|
task.status = .completed;
|
||||||
|
task.result = task.current;
|
||||||
|
// Unblock any tasks waiting for this one
|
||||||
|
for (self.tasks.items) |t| {
|
||||||
|
if (t.status == .blocked and t.waiting_for == task.id) {
|
||||||
|
t.status = .runnable;
|
||||||
|
t.waiting_for = null;
|
||||||
|
t.current = try makePure(self.arena, task.result.?);
|
||||||
|
try self.runnable.append(self.gpa, t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stepTask(self: *Scheduler, task: *Task) !void {
|
||||||
|
const reduced = try reduceInScratch(self.gpa, self.arena, task.current);
|
||||||
|
|
||||||
|
const decoded = try decodeAction(self.arena, reduced);
|
||||||
|
if (decoded == null) {
|
||||||
|
// Not a recognized action — if no frames, it's the final result.
|
||||||
|
// Otherwise treat as invalid.
|
||||||
|
if (task.frames.items.len == 0) {
|
||||||
|
task.current = reduced;
|
||||||
|
try self.completeTask(task);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const err = try makeErrResult(self.arena, ERR_INVALID_ACTION);
|
||||||
|
if (try task.finishValue(self.arena, err)) {
|
||||||
|
try self.completeTask(task);
|
||||||
|
} else {
|
||||||
|
try self.runnable.append(self.gpa, task);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (decoded.?) {
|
||||||
|
.pure => |val| {
|
||||||
|
if (try task.finishValue(self.arena, val)) {
|
||||||
|
try self.completeTask(task);
|
||||||
|
} else {
|
||||||
|
try self.runnable.append(self.gpa, task);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
.bind => |b| {
|
||||||
|
try task.frames.append(self.gpa, .{ .bind = b.k });
|
||||||
|
task.current = b.left;
|
||||||
|
try self.runnable.append(self.gpa, task);
|
||||||
|
},
|
||||||
|
|
||||||
|
.putStr => |str_tree| {
|
||||||
|
const str = try codecs.toString(self.arena, str_tree) orelse {
|
||||||
|
const err = try makeErrResult(self.arena, ERR_INVALID_STRING);
|
||||||
|
if (try task.finishValue(self.arena, err)) {
|
||||||
|
try self.completeTask(task);
|
||||||
|
} else {
|
||||||
|
try self.runnable.append(self.gpa, task);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
defer self.gpa.free(str);
|
||||||
|
_ = std.c.write(1, str.ptr, str.len);
|
||||||
|
const leaf = try self.arena.alloc(.leaf);
|
||||||
|
if (try task.finishValue(self.arena, leaf)) {
|
||||||
|
try self.completeTask(task);
|
||||||
|
} else {
|
||||||
|
try self.runnable.append(self.gpa, task);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
.getLine => {
|
||||||
|
var buf: [4096]u8 = undefined;
|
||||||
|
var len: usize = 0;
|
||||||
|
while (len < buf.len) {
|
||||||
|
const n = std.c.read(0, buf[len..].ptr, 1);
|
||||||
|
if (n <= 0) break;
|
||||||
|
if (buf[len] == '\n') break;
|
||||||
|
len += 1;
|
||||||
|
}
|
||||||
|
const line = buf[0..len];
|
||||||
|
const str_tree = try codecs.ofString(self.arena, line);
|
||||||
|
if (try task.finishValue(self.arena, str_tree)) {
|
||||||
|
try self.completeTask(task);
|
||||||
|
} else {
|
||||||
|
try self.runnable.append(self.gpa, task);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
.readFile => |path_tree| {
|
||||||
|
const path = try codecs.toString(self.arena, path_tree) orelse {
|
||||||
|
const err = try makeErrResult(self.arena, ERR_INVALID_STRING);
|
||||||
|
if (try task.finishValue(self.arena, err)) {
|
||||||
|
try self.completeTask(task);
|
||||||
|
} else {
|
||||||
|
try self.runnable.append(self.gpa, task);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!self.perms.allow_read_all) {
|
||||||
|
self.arena.allocator.free(path);
|
||||||
|
const err = try makeErrResult(self.arena, ERR_POLICY_DENY);
|
||||||
|
if (try task.finishValue(self.arena, err)) {
|
||||||
|
try self.completeTask(task);
|
||||||
|
} else {
|
||||||
|
try self.runnable.append(self.gpa, task);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = try self.gpa.create(FileReadCtx);
|
||||||
|
ctx.* = .{
|
||||||
|
.scheduler = self,
|
||||||
|
.task = task,
|
||||||
|
.arena = self.arena,
|
||||||
|
.gpa = self.gpa,
|
||||||
|
.fd = -1,
|
||||||
|
.buf = std.ArrayList(u8).empty,
|
||||||
|
.path = path,
|
||||||
|
.req = undefined,
|
||||||
|
.read_buf = null,
|
||||||
|
};
|
||||||
|
ctx.req.data = ctx;
|
||||||
|
_ = c.uv_fs_open(self.loop, &ctx.req, ctx.path.ptr, c.O_RDONLY, 0, file_open_cb);
|
||||||
|
},
|
||||||
|
|
||||||
|
.writeFile => |wf| {
|
||||||
|
const path = try codecs.toString(self.arena, wf.path) orelse {
|
||||||
|
const err = try makeErrResult(self.arena, ERR_INVALID_STRING);
|
||||||
|
if (try task.finishValue(self.arena, err)) {
|
||||||
|
try self.completeTask(task);
|
||||||
|
} else {
|
||||||
|
try self.runnable.append(self.gpa, task);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
const contents = try codecs.toString(self.arena, wf.contents) orelse {
|
||||||
|
self.arena.allocator.free(path);
|
||||||
|
const err = try makeErrResult(self.arena, ERR_INVALID_STRING);
|
||||||
|
if (try task.finishValue(self.arena, err)) {
|
||||||
|
try self.completeTask(task);
|
||||||
|
} else {
|
||||||
|
try self.runnable.append(self.gpa, task);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!self.perms.allow_write_all) {
|
||||||
|
self.arena.allocator.free(path);
|
||||||
|
self.arena.allocator.free(contents);
|
||||||
|
const err = try makeErrResult(self.arena, ERR_POLICY_DENY);
|
||||||
|
if (try task.finishValue(self.arena, err)) {
|
||||||
|
try self.completeTask(task);
|
||||||
|
} else {
|
||||||
|
try self.runnable.append(self.gpa, task);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = try self.gpa.create(FileWriteCtx);
|
||||||
|
ctx.* = .{
|
||||||
|
.scheduler = self,
|
||||||
|
.task = task,
|
||||||
|
.arena = self.arena,
|
||||||
|
.gpa = self.gpa,
|
||||||
|
.fd = -1,
|
||||||
|
.path = path,
|
||||||
|
.contents = contents,
|
||||||
|
.written = false,
|
||||||
|
.req = undefined,
|
||||||
|
};
|
||||||
|
ctx.req.data = ctx;
|
||||||
|
const flags = c.O_WRONLY | c.O_CREAT | c.O_TRUNC;
|
||||||
|
_ = c.uv_fs_open(self.loop, &ctx.req, ctx.path.ptr, flags, 0o644, file_write_open_cb);
|
||||||
|
},
|
||||||
|
|
||||||
|
.ask => {
|
||||||
|
if (try task.finishValue(self.arena, task.runtime.env)) {
|
||||||
|
try self.completeTask(task);
|
||||||
|
} else {
|
||||||
|
try self.runnable.append(self.gpa, task);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
.local => |loc| {
|
||||||
|
const new_env = try reduceInScratch(self.gpa, self.arena, try self.arena.alloc(.{ .app = .{ .func = loc.f, .arg = task.runtime.env } }));
|
||||||
|
try task.frames.append(self.gpa, .{ .local = task.runtime.env });
|
||||||
|
task.runtime.env = new_env;
|
||||||
|
task.current = loc.action;
|
||||||
|
try self.runnable.append(self.gpa, task);
|
||||||
|
},
|
||||||
|
|
||||||
|
.get => {
|
||||||
|
if (try task.finishValue(self.arena, task.runtime.state)) {
|
||||||
|
try self.completeTask(task);
|
||||||
|
} else {
|
||||||
|
try self.runnable.append(self.gpa, task);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
.put => |new_state| {
|
||||||
|
task.runtime.state = new_state;
|
||||||
|
const leaf = try self.arena.alloc(.leaf);
|
||||||
|
if (try task.finishValue(self.arena, leaf)) {
|
||||||
|
try self.completeTask(task);
|
||||||
|
} else {
|
||||||
|
try self.runnable.append(self.gpa, task);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
.fork => |action| {
|
||||||
|
const child = try self.createTask(task, task.runtime.env, task.runtime.state, action);
|
||||||
|
try self.runnable.append(self.gpa, child);
|
||||||
|
const handle = try codecs.ofNumber(self.arena, child.id);
|
||||||
|
if (try task.finishValue(self.arena, handle)) {
|
||||||
|
try self.completeTask(task);
|
||||||
|
} else {
|
||||||
|
try self.runnable.append(self.gpa, task);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
.await => |handle_tree| {
|
||||||
|
const handle = try codecs.toNumber(self.arena, handle_tree) orelse {
|
||||||
|
const err = try makeErrResult(self.arena, ERR_INVALID_ACTION);
|
||||||
|
if (try task.finishValue(self.arena, err)) {
|
||||||
|
try self.completeTask(task);
|
||||||
|
} else {
|
||||||
|
try self.runnable.append(self.gpa, task);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
var found: ?*Task = null;
|
||||||
|
for (self.tasks.items) |t| {
|
||||||
|
if (t.id == handle) {
|
||||||
|
found = t;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found == null) {
|
||||||
|
const err = try makeErrResult(self.arena, ERR_INVALID_ACTION);
|
||||||
|
if (try task.finishValue(self.arena, err)) {
|
||||||
|
try self.completeTask(task);
|
||||||
|
} else {
|
||||||
|
try self.runnable.append(self.gpa, task);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (found.?.status == .completed) {
|
||||||
|
const result = found.?.result.?;
|
||||||
|
if (try task.finishValue(self.arena, result)) {
|
||||||
|
try self.completeTask(task);
|
||||||
|
} else {
|
||||||
|
try self.runnable.append(self.gpa, task);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
task.status = .blocked;
|
||||||
|
task.waiting_for = handle;
|
||||||
|
// Task remains out of runnable until child completes
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
.yield => {
|
||||||
|
const leaf = try self.arena.alloc(.leaf);
|
||||||
|
if (try task.finishValue(self.arena, leaf)) {
|
||||||
|
try self.completeTask(task);
|
||||||
|
} else {
|
||||||
|
try self.runnable.append(self.gpa, task);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
.sleep => |ms_tree| {
|
||||||
|
const ms = try codecs.toNumber(self.arena, ms_tree) orelse 0;
|
||||||
|
const ctx = try self.gpa.create(SleepCtx);
|
||||||
|
ctx.* = .{
|
||||||
|
.scheduler = self,
|
||||||
|
.task = task,
|
||||||
|
.arena = self.arena,
|
||||||
|
.timer = undefined,
|
||||||
|
};
|
||||||
|
ctx.timer.data = ctx;
|
||||||
|
_ = c.uv_timer_init(self.loop, &ctx.timer);
|
||||||
|
_ = c.uv_timer_start(&ctx.timer, sleep_cb, @intCast(ms), 0);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Async file read
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const FileReadCtx = struct {
|
||||||
|
scheduler: *Scheduler,
|
||||||
|
task: *Task,
|
||||||
|
arena: *Arena,
|
||||||
|
gpa: std.mem.Allocator,
|
||||||
|
fd: c_int,
|
||||||
|
buf: std.ArrayList(u8),
|
||||||
|
path: []const u8,
|
||||||
|
req: c.uv_fs_t,
|
||||||
|
read_buf: ?[]u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn mapUvErr(uv_err: c_int) u64 {
|
||||||
|
return switch (uv_err) {
|
||||||
|
c.UV_ENOENT => ERR_DOES_NOT_EXIST,
|
||||||
|
c.UV_EACCES => ERR_PERMISSION,
|
||||||
|
c.UV_EEXIST => ERR_ALREADY_EXISTS,
|
||||||
|
else => ERR_IO_OTHER,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file_open_cb(req: [*c]c.uv_fs_t) callconv(.c) void {
|
||||||
|
const ctx = @as(*FileReadCtx, @ptrCast(@alignCast(req.*.data)));
|
||||||
|
const result = req.*.result;
|
||||||
|
c.uv_fs_req_cleanup(req);
|
||||||
|
if (result < 0) {
|
||||||
|
const err = makeErrResult(ctx.arena, mapUvErr(@intCast(-result))) catch {
|
||||||
|
ctx.gpa.destroy(ctx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if (ctx.task.finishValue(ctx.arena, err) catch false) {
|
||||||
|
ctx.scheduler.completeTask(ctx.task) catch {};
|
||||||
|
} else {
|
||||||
|
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
|
||||||
|
}
|
||||||
|
ctx.buf.deinit(ctx.gpa);
|
||||||
|
ctx.gpa.free(ctx.path);
|
||||||
|
ctx.gpa.destroy(ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.fd = @intCast(result);
|
||||||
|
const read_buf = ctx.gpa.alloc(u8, 4096) catch unreachable;
|
||||||
|
ctx.read_buf = read_buf;
|
||||||
|
var uv_buf = c.uv_buf_init(@ptrCast(read_buf.ptr), @intCast(read_buf.len));
|
||||||
|
_ = c.uv_fs_read(ctx.scheduler.loop, req, ctx.fd, &uv_buf, 1, -1, file_read_cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file_read_cb(req: [*c]c.uv_fs_t) callconv(.c) void {
|
||||||
|
const ctx = @as(*FileReadCtx, @ptrCast(@alignCast(req.*.data)));
|
||||||
|
const nread = req.*.result;
|
||||||
|
c.uv_fs_req_cleanup(req);
|
||||||
|
if (nread < 0) {
|
||||||
|
_ = c.uv_fs_close(ctx.scheduler.loop, req, ctx.fd, null);
|
||||||
|
const err = makeErrResult(ctx.arena, mapUvErr(@intCast(-nread))) catch {
|
||||||
|
ctx.gpa.destroy(ctx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if (ctx.task.finishValue(ctx.arena, err) catch false) {
|
||||||
|
ctx.scheduler.completeTask(ctx.task) catch {};
|
||||||
|
} else {
|
||||||
|
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
|
||||||
|
}
|
||||||
|
if (ctx.read_buf) |b| ctx.gpa.free(b);
|
||||||
|
ctx.buf.deinit(ctx.gpa);
|
||||||
|
ctx.gpa.free(ctx.path);
|
||||||
|
ctx.gpa.destroy(ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (nread == 0) {
|
||||||
|
// EOF
|
||||||
|
_ = c.uv_fs_close(ctx.scheduler.loop, req, ctx.fd, null);
|
||||||
|
const bytes_tree = codecs.ofBytes(ctx.arena, ctx.buf.items) catch {
|
||||||
|
ctx.gpa.destroy(ctx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
const ok = makeOkResult(ctx.arena, bytes_tree) catch {
|
||||||
|
ctx.gpa.destroy(ctx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if (ctx.task.finishValue(ctx.arena, ok) catch false) {
|
||||||
|
ctx.scheduler.completeTask(ctx.task) catch {};
|
||||||
|
} else {
|
||||||
|
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
|
||||||
|
}
|
||||||
|
if (ctx.read_buf) |b| ctx.gpa.free(b);
|
||||||
|
ctx.buf.deinit(ctx.gpa);
|
||||||
|
ctx.gpa.free(ctx.path);
|
||||||
|
ctx.gpa.destroy(ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = ctx.read_buf.?[0..@intCast(nread)];
|
||||||
|
ctx.buf.appendSlice(ctx.gpa, data) catch unreachable;
|
||||||
|
const read_buf = ctx.gpa.alloc(u8, 4096) catch unreachable;
|
||||||
|
ctx.read_buf = read_buf;
|
||||||
|
var uv_buf = c.uv_buf_init(@ptrCast(read_buf.ptr), @intCast(read_buf.len));
|
||||||
|
_ = c.uv_fs_read(ctx.scheduler.loop, req, ctx.fd, &uv_buf, 1, -1, file_read_cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Async file write
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const FileWriteCtx = struct {
|
||||||
|
scheduler: *Scheduler,
|
||||||
|
task: *Task,
|
||||||
|
arena: *Arena,
|
||||||
|
gpa: std.mem.Allocator,
|
||||||
|
fd: c_int,
|
||||||
|
path: []const u8,
|
||||||
|
contents: []const u8,
|
||||||
|
written: bool,
|
||||||
|
req: c.uv_fs_t,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn file_write_open_cb(req: [*c]c.uv_fs_t) callconv(.c) void {
|
||||||
|
const ctx = @as(*FileWriteCtx, @ptrCast(@alignCast(req.*.data)));
|
||||||
|
const result = req.*.result;
|
||||||
|
c.uv_fs_req_cleanup(req);
|
||||||
|
if (result < 0) {
|
||||||
|
const err = makeErrResult(ctx.arena, mapUvErr(@intCast(-result))) catch {
|
||||||
|
ctx.gpa.destroy(ctx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if (ctx.task.finishValue(ctx.arena, err) catch false) {
|
||||||
|
ctx.scheduler.completeTask(ctx.task) catch {};
|
||||||
|
} else {
|
||||||
|
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
|
||||||
|
}
|
||||||
|
ctx.gpa.free(ctx.path);
|
||||||
|
ctx.gpa.free(ctx.contents);
|
||||||
|
ctx.gpa.destroy(ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.fd = @intCast(result);
|
||||||
|
var uv_buf = c.uv_buf_init(@ptrCast(@constCast(ctx.contents.ptr)), @intCast(ctx.contents.len));
|
||||||
|
_ = c.uv_fs_write(ctx.scheduler.loop, req, ctx.fd, &uv_buf, 1, 0, file_write_cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file_write_cb(req: [*c]c.uv_fs_t) callconv(.c) void {
|
||||||
|
const ctx = @as(*FileWriteCtx, @ptrCast(@alignCast(req.*.data)));
|
||||||
|
const nwrite = req.*.result;
|
||||||
|
c.uv_fs_req_cleanup(req);
|
||||||
|
if (nwrite < 0) {
|
||||||
|
_ = c.uv_fs_close(ctx.scheduler.loop, req, ctx.fd, null);
|
||||||
|
const err = makeErrResult(ctx.arena, mapUvErr(@intCast(-nwrite))) catch {
|
||||||
|
ctx.gpa.destroy(ctx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if (ctx.task.finishValue(ctx.arena, err) catch false) {
|
||||||
|
ctx.scheduler.completeTask(ctx.task) catch {};
|
||||||
|
} else {
|
||||||
|
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
|
||||||
|
}
|
||||||
|
ctx.gpa.free(ctx.path);
|
||||||
|
ctx.gpa.free(ctx.contents);
|
||||||
|
ctx.gpa.destroy(ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ = c.uv_fs_close(ctx.scheduler.loop, req, ctx.fd, file_write_close_cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file_write_close_cb(req: [*c]c.uv_fs_t) callconv(.c) void {
|
||||||
|
const ctx = @as(*FileWriteCtx, @ptrCast(@alignCast(req.*.data)));
|
||||||
|
c.uv_fs_req_cleanup(req);
|
||||||
|
const leaf = ctx.arena.alloc(.leaf) catch {
|
||||||
|
ctx.gpa.destroy(ctx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
const ok = makeOkResult(ctx.arena, leaf) catch {
|
||||||
|
ctx.gpa.destroy(ctx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if (ctx.task.finishValue(ctx.arena, ok) catch false) {
|
||||||
|
ctx.scheduler.completeTask(ctx.task) catch {};
|
||||||
|
} else {
|
||||||
|
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
|
||||||
|
}
|
||||||
|
ctx.gpa.free(ctx.path);
|
||||||
|
ctx.gpa.free(ctx.contents);
|
||||||
|
ctx.gpa.destroy(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Async sleep
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const SleepCtx = struct {
|
||||||
|
scheduler: *Scheduler,
|
||||||
|
task: *Task,
|
||||||
|
arena: *Arena,
|
||||||
|
timer: c.uv_timer_t,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn sleep_cb(handle: [*c]c.uv_timer_t) callconv(.c) void {
|
||||||
|
const ctx = @as(*SleepCtx, @ptrCast(@alignCast(handle.*.data)));
|
||||||
|
defer ctx.scheduler.gpa.destroy(ctx);
|
||||||
|
const leaf = ctx.arena.alloc(.leaf) catch {
|
||||||
|
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if (ctx.task.finishValue(ctx.arena, leaf) catch false) {
|
||||||
|
ctx.scheduler.completeTask(ctx.task) catch {};
|
||||||
|
} else {
|
||||||
|
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Public entry point
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub fn runIO(gpa: std.mem.Allocator, arena: *Arena, program: u32, perms: IOPerms) !u32 {
|
||||||
|
const action_tree = try isIOSentinel(arena, program) orelse {
|
||||||
|
return error.InvalidIOSentinel;
|
||||||
|
};
|
||||||
|
|
||||||
|
var loop: c.uv_loop_t = undefined;
|
||||||
|
const rc = c.uv_loop_init(&loop);
|
||||||
|
if (rc != 0) return error.LoopInitFailed;
|
||||||
|
defer _ = c.uv_loop_close(&loop);
|
||||||
|
|
||||||
|
var scheduler = try Scheduler.init(gpa, &loop, arena, perms);
|
||||||
|
defer scheduler.deinit();
|
||||||
|
|
||||||
|
const main_task = try scheduler.createTask(null, try arena.alloc(.leaf), try arena.alloc(.leaf), action_tree);
|
||||||
|
try scheduler.runnable.append(gpa, main_task);
|
||||||
|
|
||||||
|
try scheduler.run();
|
||||||
|
|
||||||
|
// Return the main task's result
|
||||||
|
return main_task.result orelse program;
|
||||||
|
}
|
||||||
@@ -5,6 +5,47 @@ const reduce = @import("reduce.zig");
|
|||||||
const codecs = @import("codecs.zig");
|
const codecs = @import("codecs.zig");
|
||||||
const kernel = @import("kernel.zig");
|
const kernel = @import("kernel.zig");
|
||||||
const bundle = @import("bundle.zig");
|
const bundle = @import("bundle.zig");
|
||||||
|
const io_driver = @import("io_driver.zig");
|
||||||
|
|
||||||
|
fn printNode(arena: *Arena, tag: u64, node: u32, io: std.Io) !void {
|
||||||
|
var stdout_buf: [4096]u8 = undefined;
|
||||||
|
var stdout = std.Io.File.stdout().writer(io, &stdout_buf);
|
||||||
|
|
||||||
|
switch (tag) {
|
||||||
|
codecs.HOST_STRING_TAG => {
|
||||||
|
const s = try codecs.toString(arena, node) orelse {
|
||||||
|
try stdout.interface.writeAll("Error: failed to decode string result\n");
|
||||||
|
try stdout.flush();
|
||||||
|
return error.DecodeFailed;
|
||||||
|
};
|
||||||
|
defer arena.allocator.free(s);
|
||||||
|
try stdout.interface.writeAll(s);
|
||||||
|
try stdout.interface.writeAll("\n");
|
||||||
|
},
|
||||||
|
codecs.HOST_NUMBER_TAG => {
|
||||||
|
const n = try codecs.toNumber(arena, node) orelse 0;
|
||||||
|
try stdout.interface.print("{d}\n", .{n});
|
||||||
|
},
|
||||||
|
codecs.HOST_BOOL_TAG => {
|
||||||
|
const b = try codecs.toBool(arena, node) orelse {
|
||||||
|
try stdout.interface.writeAll("Error: failed to decode bool result\n");
|
||||||
|
try stdout.flush();
|
||||||
|
return error.DecodeFailed;
|
||||||
|
};
|
||||||
|
try stdout.interface.writeAll(if (b) "true\n" else "false\n");
|
||||||
|
},
|
||||||
|
codecs.HOST_TREE_TAG => {
|
||||||
|
try tree.formatTree(&stdout.interface, arena, node, 0);
|
||||||
|
try stdout.interface.writeAll("\n");
|
||||||
|
},
|
||||||
|
else => {
|
||||||
|
try stdout.interface.print("(tag={d}, payload=", .{tag});
|
||||||
|
try tree.formatTree(&stdout.interface, arena, node, 0);
|
||||||
|
try stdout.interface.writeAll(")\n");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
try stdout.flush();
|
||||||
|
}
|
||||||
|
|
||||||
fn runNative(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []const []const u8, fuel: u64, io: std.Io) !void {
|
fn runNative(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []const []const u8, fuel: u64, io: std.Io) !void {
|
||||||
const term = try bundle.loadBundleDefaultRoot(arena, bundle_bytes);
|
const term = try bundle.loadBundleDefaultRoot(arena, bundle_bytes);
|
||||||
@@ -16,44 +57,29 @@ fn runNative(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []cons
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = try reduce.reduce(current, arena, fuel);
|
const result = try reduce.reduce(current, arena, fuel);
|
||||||
|
try printNode(arena, tag, result, io);
|
||||||
var stdout_buf: [4096]u8 = undefined;
|
|
||||||
var stdout = std.Io.File.stdout().writer(io, &stdout_buf);
|
|
||||||
|
|
||||||
switch (tag) {
|
|
||||||
codecs.HOST_STRING_TAG => {
|
|
||||||
const s = try codecs.toString(arena, result) orelse {
|
|
||||||
try stdout.interface.writeAll("Error: failed to decode string result\n");
|
|
||||||
try stdout.flush();
|
|
||||||
return error.DecodeFailed;
|
|
||||||
};
|
|
||||||
defer arena.allocator.free(s);
|
|
||||||
try stdout.interface.writeAll(s);
|
|
||||||
try stdout.interface.writeAll("\n");
|
|
||||||
},
|
|
||||||
codecs.HOST_NUMBER_TAG => {
|
|
||||||
const n = try codecs.toNumber(arena, result) orelse 0;
|
|
||||||
try stdout.interface.print("{d}\n", .{n});
|
|
||||||
},
|
|
||||||
codecs.HOST_BOOL_TAG => {
|
|
||||||
const b = try codecs.toBool(arena, result) orelse {
|
|
||||||
try stdout.interface.writeAll("Error: failed to decode bool result\n");
|
|
||||||
try stdout.flush();
|
|
||||||
return error.DecodeFailed;
|
|
||||||
};
|
|
||||||
try stdout.interface.writeAll(if (b) "true\n" else "false\n");
|
|
||||||
},
|
|
||||||
codecs.HOST_TREE_TAG => {
|
|
||||||
try tree.formatTree(&stdout.interface, arena, result, 0);
|
|
||||||
try stdout.interface.writeAll("\n");
|
|
||||||
},
|
|
||||||
else => {
|
|
||||||
try stdout.interface.print("(tag={d}, payload=", .{tag});
|
|
||||||
try tree.formatTree(&stdout.interface, arena, result, 0);
|
|
||||||
try stdout.interface.writeAll(")\n");
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
try stdout.flush();
|
|
||||||
|
fn runIO(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []const []const u8, fuel: u64, perms: io_driver.IOPerms, io: std.Io) !void {
|
||||||
|
const term = try bundle.loadBundleDefaultRoot(arena, bundle_bytes);
|
||||||
|
|
||||||
|
var current = term;
|
||||||
|
for (args_raw) |arg| {
|
||||||
|
const arg_tree = try parseArg(arena, io, arg);
|
||||||
|
current = try arena.alloc(.{ .app = .{ .func = current, .arg = arg_tree } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const reduced = try reduce.reduce(current, arena, fuel);
|
||||||
|
|
||||||
|
if (try io_driver.isIOSentinel(arena, reduced) == null) {
|
||||||
|
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
|
||||||
|
try stderr.interface.writeAll("Error: reduced term is not a valid IO program\n");
|
||||||
|
try stderr.flush();
|
||||||
|
std.process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = try io_driver.runIO(arena.allocator, arena, reduced, perms);
|
||||||
|
try printNode(arena, tag, result, io);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn runBundle(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []const []const u8, fuel: u64, io: std.Io) !void {
|
fn runBundle(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []const []const u8, fuel: u64, io: std.Io) !void {
|
||||||
@@ -98,43 +124,7 @@ fn runBundle(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []cons
|
|||||||
return error.InvalidHostValue;
|
return error.InvalidHostValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
var stdout_buf: [4096]u8 = undefined;
|
try printNode(arena, hv.tag, hv.payload, io);
|
||||||
var stdout = std.Io.File.stdout().writer(io, &stdout_buf);
|
|
||||||
|
|
||||||
switch (hv.tag) {
|
|
||||||
codecs.HOST_STRING_TAG => {
|
|
||||||
const s = try codecs.toString(arena, hv.payload) orelse {
|
|
||||||
try stdout.interface.writeAll("Error: failed to decode string payload\n");
|
|
||||||
try stdout.flush();
|
|
||||||
return error.DecodeFailed;
|
|
||||||
};
|
|
||||||
defer arena.allocator.free(s);
|
|
||||||
try stdout.interface.writeAll(s);
|
|
||||||
try stdout.interface.writeAll("\n");
|
|
||||||
},
|
|
||||||
codecs.HOST_NUMBER_TAG => {
|
|
||||||
const n = try codecs.toNumber(arena, hv.payload) orelse 0;
|
|
||||||
try stdout.interface.print("{d}\n", .{n});
|
|
||||||
},
|
|
||||||
codecs.HOST_BOOL_TAG => {
|
|
||||||
const b = try codecs.toBool(arena, hv.payload) orelse {
|
|
||||||
try stdout.interface.writeAll("Error: failed to decode bool payload\n");
|
|
||||||
try stdout.flush();
|
|
||||||
return error.DecodeFailed;
|
|
||||||
};
|
|
||||||
try stdout.interface.writeAll(if (b) "true\n" else "false\n");
|
|
||||||
},
|
|
||||||
codecs.HOST_TREE_TAG => {
|
|
||||||
try tree.formatTree(&stdout.interface, arena, hv.payload, 0);
|
|
||||||
try stdout.interface.writeAll("\n");
|
|
||||||
},
|
|
||||||
else => {
|
|
||||||
try stdout.interface.print("(tag={d}, payload=", .{hv.tag});
|
|
||||||
try tree.formatTree(&stdout.interface, arena, hv.payload, 0);
|
|
||||||
try stdout.interface.writeAll(")\n");
|
|
||||||
},
|
|
||||||
}
|
|
||||||
try stdout.flush();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parseArg(arena: *Arena, io: std.Io, s: []const u8) !u32 {
|
fn parseArg(arena: *Arena, io: std.Io, s: []const u8) !u32 {
|
||||||
@@ -162,7 +152,7 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
const args = try init.minimal.args.toSlice(init.arena.allocator());
|
const args = try init.minimal.args.toSlice(init.arena.allocator());
|
||||||
if (args.len < 2) {
|
if (args.len < 2) {
|
||||||
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
|
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
|
||||||
try stderr.interface.writeAll("Usage: tricu-zig [--type TYPE] [--kernel] [--fuel N] <bundle.arboricx> [arg1 arg2 ...]\n");
|
try stderr.interface.writeAll("Usage: tricu-zig [--type TYPE] [--kernel] [--io] [--unsafe-io] [--fuel N] <bundle.arboricx> [arg1 arg2 ...]\n");
|
||||||
try stderr.flush();
|
try stderr.flush();
|
||||||
std.process.exit(1);
|
std.process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -173,6 +163,8 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
var arg_start: usize = 2;
|
var arg_start: usize = 2;
|
||||||
|
|
||||||
var use_kernel = false;
|
var use_kernel = false;
|
||||||
|
var use_io = false;
|
||||||
|
var io_perms = io_driver.IOPerms{};
|
||||||
var fuel: u64 = std.math.maxInt(u64);
|
var fuel: u64 = std.math.maxInt(u64);
|
||||||
|
|
||||||
var i: usize = 1;
|
var i: usize = 1;
|
||||||
@@ -180,7 +172,7 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
if (std.mem.eql(u8, args[i], "--type")) {
|
if (std.mem.eql(u8, args[i], "--type")) {
|
||||||
if (i + 1 >= args.len) {
|
if (i + 1 >= args.len) {
|
||||||
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
|
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
|
||||||
try stderr.interface.writeAll("Usage: tricu-zig --type <tree|number|bool|string|list|bytes> [--fuel N] <bundle> [args...]\n");
|
try stderr.interface.writeAll("Usage: tricu-zig --type <tree|number|bool|string|list|bytes> [--io] [--unsafe-io] [--fuel N] <bundle> [args...]\n");
|
||||||
try stderr.flush();
|
try stderr.flush();
|
||||||
std.process.exit(1);
|
std.process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -201,10 +193,15 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
i += 1;
|
i += 1;
|
||||||
} else if (std.mem.eql(u8, args[i], "--kernel")) {
|
} else if (std.mem.eql(u8, args[i], "--kernel")) {
|
||||||
use_kernel = true;
|
use_kernel = true;
|
||||||
|
} else if (std.mem.eql(u8, args[i], "--io")) {
|
||||||
|
use_io = true;
|
||||||
|
} else if (std.mem.eql(u8, args[i], "--unsafe-io")) {
|
||||||
|
io_perms.allow_read_all = true;
|
||||||
|
io_perms.allow_write_all = true;
|
||||||
} else if (std.mem.eql(u8, args[i], "--fuel")) {
|
} else if (std.mem.eql(u8, args[i], "--fuel")) {
|
||||||
if (i + 1 >= args.len) {
|
if (i + 1 >= args.len) {
|
||||||
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
|
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
|
||||||
try stderr.interface.writeAll("Usage: tricu-zig --fuel <N> <bundle> [args...]\n");
|
try stderr.interface.writeAll("Usage: tricu-zig --fuel <N> [--io] [--unsafe-io] <bundle> [args...]\n");
|
||||||
try stderr.flush();
|
try stderr.flush();
|
||||||
std.process.exit(1);
|
std.process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -225,7 +222,7 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
|
|
||||||
if (bundle_idx >= args.len) {
|
if (bundle_idx >= args.len) {
|
||||||
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
|
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
|
||||||
try stderr.interface.writeAll("Usage: tricu-zig [--type TYPE] [--kernel] [--fuel N] <bundle.arboricx> [arg1 arg2 ...]\n");
|
try stderr.interface.writeAll("Usage: tricu-zig [--type TYPE] [--kernel] [--io] [--unsafe-io] [--fuel N] <bundle.arboricx> [arg1 arg2 ...]\n");
|
||||||
try stderr.flush();
|
try stderr.flush();
|
||||||
std.process.exit(1);
|
std.process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -239,7 +236,14 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
|
|
||||||
const call_args = if (arg_start < args.len) args[arg_start..] else &[_][]const u8{};
|
const call_args = if (arg_start < args.len) args[arg_start..] else &[_][]const u8{};
|
||||||
|
|
||||||
if (use_kernel) {
|
if (use_io) {
|
||||||
|
runIO(&arena, tag, bundle_bytes, call_args, fuel, io_perms, io) catch |err| {
|
||||||
|
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
|
||||||
|
try stderr.interface.print("Execution failed: {s}\n", .{@errorName(err)});
|
||||||
|
try stderr.flush();
|
||||||
|
std.process.exit(1);
|
||||||
|
};
|
||||||
|
} else if (use_kernel) {
|
||||||
runBundle(&arena, tag, bundle_bytes, call_args, fuel, io) catch |err| {
|
runBundle(&arena, tag, bundle_bytes, call_args, fuel, io) catch |err| {
|
||||||
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
|
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
|
||||||
try stderr.interface.print("Execution failed: {s}\n", .{@errorName(err)});
|
try stderr.interface.print("Execution failed: {s}\n", .{@errorName(err)});
|
||||||
|
|||||||
@@ -51,6 +51,68 @@ int main(void) {
|
|||||||
}
|
}
|
||||||
printf("PASS: kernel loaded (root=%u)\n", kernel_root);
|
printf("PASS: kernel loaded (root=%u)\n", kernel_root);
|
||||||
|
|
||||||
|
/* Test: tree inspection primitives */
|
||||||
|
uint32_t l = arb_leaf(ctx);
|
||||||
|
uint32_t s = arb_stem(ctx, l);
|
||||||
|
uint32_t f = arb_fork(ctx, s, l);
|
||||||
|
uint32_t a = arb_app(ctx, f, s);
|
||||||
|
|
||||||
|
if (!arb_is_leaf(ctx, l)) {
|
||||||
|
fprintf(stderr, "FAIL: is_leaf on leaf\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (arb_is_leaf(ctx, s)) {
|
||||||
|
fprintf(stderr, "FAIL: is_leaf on stem should be false\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (!arb_is_stem(ctx, s)) {
|
||||||
|
fprintf(stderr, "FAIL: is_stem on stem\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (!arb_is_fork(ctx, f)) {
|
||||||
|
fprintf(stderr, "FAIL: is_fork on fork\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (!arb_is_app(ctx, a)) {
|
||||||
|
fprintf(stderr, "FAIL: is_app on app\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t child;
|
||||||
|
if (!arb_get_stem_child(ctx, s, &child) || child != l) {
|
||||||
|
fprintf(stderr, "FAIL: get_stem_child\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t left, right;
|
||||||
|
if (!arb_get_fork_children(ctx, f, &left, &right) || left != s || right != l) {
|
||||||
|
fprintf(stderr, "FAIL: get_fork_children\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t func, arg;
|
||||||
|
if (!arb_get_app_func_arg(ctx, a, &func, &arg) || func != f || arg != s) {
|
||||||
|
fprintf(stderr, "FAIL: get_app_func_arg\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Invalid index should return 0 */
|
||||||
|
if (arb_is_leaf(ctx, 999999)) {
|
||||||
|
fprintf(stderr, "FAIL: is_leaf on invalid index should be false\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("PASS: tree inspection primitives\n");
|
||||||
|
|
||||||
arboricx_free(ctx);
|
arboricx_free(ctx);
|
||||||
printf("\nAll C ABI tests passed.\n");
|
printf("\nAll C ABI tests passed.\n");
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
223
ext/zig/tests/io_protocol_test.c
Normal file
223
ext/zig/tests/io_protocol_test.c
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include "arboricx.h"
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
arb_ctx_t* ctx = arboricx_init();
|
||||||
|
if (!ctx) {
|
||||||
|
fprintf(stderr, "Failed to initialize Arboricx context\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Test: construct and verify pure action = Fork 0 Leaf */
|
||||||
|
uint32_t leaf = arb_leaf(ctx);
|
||||||
|
uint32_t zero = arb_of_number(ctx, 0);
|
||||||
|
uint32_t pure_action = arb_fork(ctx, zero, leaf);
|
||||||
|
|
||||||
|
if (!arb_is_fork(ctx, pure_action)) {
|
||||||
|
fprintf(stderr, "FAIL: pure action should be fork\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t tag, payload;
|
||||||
|
if (!arb_get_fork_children(ctx, pure_action, &tag, &payload) ||
|
||||||
|
tag != zero || payload != leaf) {
|
||||||
|
fprintf(stderr, "FAIL: pure action children mismatch\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t tag_num;
|
||||||
|
if (!arb_to_number(ctx, tag, &tag_num) || tag_num != 0) {
|
||||||
|
fprintf(stderr, "FAIL: pure action tag should be 0\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("PASS: pure action shape\n");
|
||||||
|
|
||||||
|
/* Test: construct and verify bind action = Fork 1 (Fork left k) */
|
||||||
|
uint32_t one = arb_of_number(ctx, 1);
|
||||||
|
uint32_t left = arb_fork(ctx, zero, leaf); /* pure Leaf */
|
||||||
|
uint32_t k = arb_fork(ctx, leaf, leaf); /* identity as Fork Leaf Leaf */
|
||||||
|
uint32_t bind_pair = arb_fork(ctx, left, k);
|
||||||
|
uint32_t bind_action = arb_fork(ctx, one, bind_pair);
|
||||||
|
|
||||||
|
if (!arb_get_fork_children(ctx, bind_action, &tag, &payload) ||
|
||||||
|
!arb_to_number(ctx, tag, &tag_num) || tag_num != 1) {
|
||||||
|
fprintf(stderr, "FAIL: bind action tag should be 1\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t bind_left, bind_k;
|
||||||
|
if (!arb_get_fork_children(ctx, payload, &bind_left, &bind_k) ||
|
||||||
|
bind_left != left || bind_k != k) {
|
||||||
|
fprintf(stderr, "FAIL: bind payload should be Fork left k\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("PASS: bind action shape\n");
|
||||||
|
|
||||||
|
/* Test: construct and verify IO sentinel = Fork "tricuIO" (Fork 1 action) */
|
||||||
|
uint32_t sentinel_str = arb_of_string(ctx, "tricuIO");
|
||||||
|
uint32_t version = arb_of_number(ctx, 1);
|
||||||
|
uint32_t version_action_pair = arb_fork(ctx, version, pure_action);
|
||||||
|
uint32_t io_sentinel = arb_fork(ctx, sentinel_str, version_action_pair);
|
||||||
|
|
||||||
|
if (!arb_is_fork(ctx, io_sentinel)) {
|
||||||
|
fprintf(stderr, "FAIL: IO sentinel should be fork\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t sent_left, sent_right;
|
||||||
|
if (!arb_get_fork_children(ctx, io_sentinel, &sent_left, &sent_right)) {
|
||||||
|
fprintf(stderr, "FAIL: get_fork_children on IO sentinel\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verify sentinel string */
|
||||||
|
uint8_t* decoded_sentinel;
|
||||||
|
size_t decoded_len;
|
||||||
|
if (!arb_to_string(ctx, sent_left, &decoded_sentinel, &decoded_len) ||
|
||||||
|
decoded_len != 7 || memcmp(decoded_sentinel, "tricuIO", 7) != 0) {
|
||||||
|
fprintf(stderr, "FAIL: IO sentinel string mismatch\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
arboricx_free_buf(ctx, decoded_sentinel, decoded_len);
|
||||||
|
|
||||||
|
/* Verify version = 1 and action = pure */
|
||||||
|
uint32_t ver, act;
|
||||||
|
if (!arb_get_fork_children(ctx, sent_right, &ver, &act) ||
|
||||||
|
!arb_to_number(ctx, ver, &tag_num) || tag_num != 1 ||
|
||||||
|
act != pure_action) {
|
||||||
|
fprintf(stderr, "FAIL: IO sentinel version/action mismatch\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("PASS: IO sentinel shape\n");
|
||||||
|
|
||||||
|
/* Test: putStr action = Fork 10 string */
|
||||||
|
uint32_t ten = arb_of_number(ctx, 10);
|
||||||
|
uint32_t msg = arb_of_string(ctx, "hello");
|
||||||
|
uint32_t putStr_action = arb_fork(ctx, ten, msg);
|
||||||
|
|
||||||
|
if (!arb_get_fork_children(ctx, putStr_action, &tag, &payload) ||
|
||||||
|
!arb_to_number(ctx, tag, &tag_num) || tag_num != 10) {
|
||||||
|
fprintf(stderr, "FAIL: putStr tag should be 10\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("PASS: putStr action shape\n");
|
||||||
|
|
||||||
|
/* Test: getLine action = Fork 11 Leaf */
|
||||||
|
uint32_t eleven = arb_of_number(ctx, 11);
|
||||||
|
uint32_t getLine_action = arb_fork(ctx, eleven, leaf);
|
||||||
|
|
||||||
|
if (!arb_get_fork_children(ctx, getLine_action, &tag, &payload) ||
|
||||||
|
!arb_to_number(ctx, tag, &tag_num) || tag_num != 11 ||
|
||||||
|
payload != leaf) {
|
||||||
|
fprintf(stderr, "FAIL: getLine tag should be 11 with Leaf payload\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("PASS: getLine action shape\n");
|
||||||
|
|
||||||
|
/* Test: readFile action = Fork 20 path */
|
||||||
|
uint32_t twenty = arb_of_number(ctx, 20);
|
||||||
|
uint32_t path = arb_of_string(ctx, "/tmp/test.txt");
|
||||||
|
uint32_t readFile_action = arb_fork(ctx, twenty, path);
|
||||||
|
|
||||||
|
if (!arb_get_fork_children(ctx, readFile_action, &tag, &payload) ||
|
||||||
|
!arb_to_number(ctx, tag, &tag_num) || tag_num != 20) {
|
||||||
|
fprintf(stderr, "FAIL: readFile tag should be 20\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("PASS: readFile action shape\n");
|
||||||
|
|
||||||
|
/* Test: writeFile action = Fork 21 (Fork path contents) */
|
||||||
|
uint32_t twenty_one = arb_of_number(ctx, 21);
|
||||||
|
uint32_t contents = arb_of_string(ctx, "data");
|
||||||
|
uint32_t write_pair = arb_fork(ctx, path, contents);
|
||||||
|
uint32_t writeFile_action = arb_fork(ctx, twenty_one, write_pair);
|
||||||
|
|
||||||
|
if (!arb_get_fork_children(ctx, writeFile_action, &tag, &payload) ||
|
||||||
|
!arb_to_number(ctx, tag, &tag_num) || tag_num != 21) {
|
||||||
|
fprintf(stderr, "FAIL: writeFile tag should be 21\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t wf_path, wf_contents;
|
||||||
|
if (!arb_get_fork_children(ctx, payload, &wf_path, &wf_contents) ||
|
||||||
|
wf_path != path || wf_contents != contents) {
|
||||||
|
fprintf(stderr, "FAIL: writeFile payload should be Fork path contents\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("PASS: writeFile action shape\n");
|
||||||
|
|
||||||
|
/* Test: ok result = Fork (Stem Leaf) (Fork val Leaf) */
|
||||||
|
uint32_t stem_leaf = arb_stem(ctx, leaf);
|
||||||
|
uint32_t val_pair = arb_fork(ctx, msg, leaf);
|
||||||
|
uint32_t ok_result = arb_fork(ctx, stem_leaf, val_pair);
|
||||||
|
|
||||||
|
if (!arb_is_fork(ctx, ok_result)) {
|
||||||
|
fprintf(stderr, "FAIL: ok result should be fork\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t ok_tag, ok_rest;
|
||||||
|
if (!arb_get_fork_children(ctx, ok_result, &ok_tag, &ok_rest) ||
|
||||||
|
!arb_is_stem(ctx, ok_tag)) {
|
||||||
|
fprintf(stderr, "FAIL: ok result left should be stem\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t ok_val, ok_leaf;
|
||||||
|
if (!arb_get_fork_children(ctx, ok_rest, &ok_val, &ok_leaf) ||
|
||||||
|
ok_val != msg || ok_leaf != leaf) {
|
||||||
|
fprintf(stderr, "FAIL: ok result right should be Fork val Leaf\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("PASS: ok result shape\n");
|
||||||
|
|
||||||
|
/* Test: err result = Fork Leaf (Fork code Leaf) */
|
||||||
|
uint32_t err_code = arb_of_number(ctx, 42);
|
||||||
|
uint32_t err_pair = arb_fork(ctx, err_code, leaf);
|
||||||
|
uint32_t err_result = arb_fork(ctx, leaf, err_pair);
|
||||||
|
|
||||||
|
if (!arb_is_fork(ctx, err_result)) {
|
||||||
|
fprintf(stderr, "FAIL: err result should be fork\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t err_tag, err_rest;
|
||||||
|
if (!arb_get_fork_children(ctx, err_result, &err_tag, &err_rest) ||
|
||||||
|
!arb_is_leaf(ctx, err_tag)) {
|
||||||
|
fprintf(stderr, "FAIL: err result left should be leaf\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t err_c, err_l;
|
||||||
|
if (!arb_get_fork_children(ctx, err_rest, &err_c, &err_l) ||
|
||||||
|
err_c != err_code || err_l != leaf) {
|
||||||
|
fprintf(stderr, "FAIL: err result right should be Fork code Leaf\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("PASS: err result shape\n");
|
||||||
|
|
||||||
|
arboricx_free(ctx);
|
||||||
|
printf("\nAll IO protocol tests passed.\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
217
ext/zig/tests/io_run_test.c
Normal file
217
ext/zig/tests/io_run_test.c
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include "arboricx.h"
|
||||||
|
|
||||||
|
static uint32_t make_pure(arb_ctx_t* ctx, uint32_t val) {
|
||||||
|
uint32_t zero = arb_of_number(ctx, 0);
|
||||||
|
return arb_fork(ctx, zero, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t make_io_sentinel(arb_ctx_t* ctx, uint32_t action) {
|
||||||
|
uint32_t sentinel = arb_of_string(ctx, "tricuIO");
|
||||||
|
uint32_t version = arb_of_number(ctx, 1);
|
||||||
|
uint32_t version_action = arb_fork(ctx, version, action);
|
||||||
|
return arb_fork(ctx, sentinel, version_action);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
arb_ctx_t* ctx = arboricx_init();
|
||||||
|
if (!ctx) {
|
||||||
|
fprintf(stderr, "Failed to initialize Arboricx context\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
arb_io_perms_t perms = { 0, 0 };
|
||||||
|
|
||||||
|
/* Test 1: pure "hello" wrapped in IO sentinel */
|
||||||
|
{
|
||||||
|
uint32_t hello = arb_of_string(ctx, "hello");
|
||||||
|
uint32_t pure_hello = make_pure(ctx, hello);
|
||||||
|
uint32_t program = make_io_sentinel(ctx, pure_hello);
|
||||||
|
|
||||||
|
uint32_t result = arb_run_io(ctx, program, &perms);
|
||||||
|
if (result == 0) {
|
||||||
|
fprintf(stderr, "FAIL: pure hello returned 0\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t* decoded;
|
||||||
|
size_t decoded_len;
|
||||||
|
if (!arb_to_string(ctx, result, &decoded, &decoded_len) ||
|
||||||
|
decoded_len != 5 || memcmp(decoded, "hello", 5) != 0) {
|
||||||
|
fprintf(stderr, "FAIL: pure hello result mismatch\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
arboricx_free_buf(ctx, decoded, decoded_len);
|
||||||
|
printf("PASS: pure hello\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Test 2: bind (pure "a") (\_ : pure "done") */
|
||||||
|
{
|
||||||
|
uint32_t a = arb_of_string(ctx, "a");
|
||||||
|
uint32_t done = arb_of_string(ctx, "done");
|
||||||
|
uint32_t pure_a = make_pure(ctx, a);
|
||||||
|
uint32_t pure_done = make_pure(ctx, done);
|
||||||
|
|
||||||
|
/* K pure_done = Fork Leaf pure_done */
|
||||||
|
uint32_t k = arb_fork(ctx, arb_leaf(ctx), pure_done);
|
||||||
|
uint32_t bind_pair = arb_fork(ctx, pure_a, k);
|
||||||
|
uint32_t one = arb_of_number(ctx, 1);
|
||||||
|
uint32_t bind_action = arb_fork(ctx, one, bind_pair);
|
||||||
|
uint32_t program = make_io_sentinel(ctx, bind_action);
|
||||||
|
|
||||||
|
uint32_t result = arb_run_io(ctx, program, &perms);
|
||||||
|
if (result == 0) {
|
||||||
|
fprintf(stderr, "FAIL: bind returned 0\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t* decoded;
|
||||||
|
size_t decoded_len;
|
||||||
|
if (!arb_to_string(ctx, result, &decoded, &decoded_len) ||
|
||||||
|
decoded_len != 4 || memcmp(decoded, "done", 4) != 0) {
|
||||||
|
fprintf(stderr, "FAIL: bind result mismatch\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
arboricx_free_buf(ctx, decoded, decoded_len);
|
||||||
|
printf("PASS: bind pure\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Test 3: putStr "test" (no permissions needed) */
|
||||||
|
{
|
||||||
|
uint32_t test = arb_of_string(ctx, "test");
|
||||||
|
uint32_t ten = arb_of_number(ctx, 10);
|
||||||
|
uint32_t putStr_action = arb_fork(ctx, ten, test);
|
||||||
|
uint32_t program = make_io_sentinel(ctx, putStr_action);
|
||||||
|
|
||||||
|
printf("EXPECT: test\n");
|
||||||
|
uint32_t result = arb_run_io(ctx, program, &perms);
|
||||||
|
if (result == 0) {
|
||||||
|
fprintf(stderr, "FAIL: putStr returned 0\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (!arb_is_leaf(ctx, result)) {
|
||||||
|
fprintf(stderr, "FAIL: putStr should return Leaf\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("PASS: putStr\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Test 4: readFile without permission returns err */
|
||||||
|
{
|
||||||
|
uint32_t path = arb_of_string(ctx, "/etc/passwd");
|
||||||
|
uint32_t twenty = arb_of_number(ctx, 20);
|
||||||
|
uint32_t readFile_action = arb_fork(ctx, twenty, path);
|
||||||
|
uint32_t program = make_io_sentinel(ctx, readFile_action);
|
||||||
|
|
||||||
|
uint32_t result = arb_run_io(ctx, program, &perms);
|
||||||
|
if (result == 0) {
|
||||||
|
fprintf(stderr, "FAIL: readFile denied returned 0\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Should be an err result: Fork Leaf (Fork code Leaf) */
|
||||||
|
uint32_t left, right;
|
||||||
|
if (!arb_get_fork_children(ctx, result, &left, &right) ||
|
||||||
|
!arb_is_leaf(ctx, left)) {
|
||||||
|
fprintf(stderr, "FAIL: readFile denied should be err result\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t code, rest;
|
||||||
|
if (!arb_get_fork_children(ctx, right, &code, &rest) ||
|
||||||
|
!arb_is_leaf(ctx, rest)) {
|
||||||
|
fprintf(stderr, "FAIL: readFile denied err shape mismatch\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t code_num;
|
||||||
|
if (!arb_to_number(ctx, code, &code_num) || code_num != 20) {
|
||||||
|
fprintf(stderr, "FAIL: readFile denied code should be 20, got %llu\n",
|
||||||
|
(unsigned long long)code_num);
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("PASS: readFile denied\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Test 5: readFile with permission succeeds */
|
||||||
|
{
|
||||||
|
/* Create a temp file first */
|
||||||
|
const char* tmp = "/tmp/tricu_io_test.txt";
|
||||||
|
FILE* f = fopen(tmp, "w");
|
||||||
|
if (!f) {
|
||||||
|
fprintf(stderr, "FAIL: could not create temp file\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
fprintf(f, "hi");
|
||||||
|
fclose(f);
|
||||||
|
|
||||||
|
arb_io_perms_t unsafe_perms = { 1, 0 };
|
||||||
|
uint32_t path = arb_of_string(ctx, tmp);
|
||||||
|
uint32_t twenty = arb_of_number(ctx, 20);
|
||||||
|
uint32_t readFile_action = arb_fork(ctx, twenty, path);
|
||||||
|
uint32_t program = make_io_sentinel(ctx, readFile_action);
|
||||||
|
|
||||||
|
uint32_t result = arb_run_io(ctx, program, &unsafe_perms);
|
||||||
|
if (result == 0) {
|
||||||
|
fprintf(stderr, "FAIL: readFile allowed returned 0\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Should be ok result: Fork (Stem Leaf) (Fork val Leaf) */
|
||||||
|
uint32_t ok_tag, ok_rest;
|
||||||
|
if (!arb_get_fork_children(ctx, result, &ok_tag, &ok_rest) ||
|
||||||
|
!arb_is_stem(ctx, ok_tag)) {
|
||||||
|
fprintf(stderr, "FAIL: readFile allowed should be ok result\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t val, leaf;
|
||||||
|
if (!arb_get_fork_children(ctx, ok_rest, &val, &leaf) ||
|
||||||
|
!arb_is_leaf(ctx, leaf)) {
|
||||||
|
fprintf(stderr, "FAIL: readFile allowed ok shape mismatch\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t* decoded;
|
||||||
|
size_t decoded_len;
|
||||||
|
if (!arb_to_string(ctx, val, &decoded, &decoded_len) ||
|
||||||
|
decoded_len != 2 || memcmp(decoded, "hi", 2) != 0) {
|
||||||
|
fprintf(stderr, "FAIL: readFile allowed contents mismatch\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
arboricx_free_buf(ctx, decoded, decoded_len);
|
||||||
|
printf("PASS: readFile allowed\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Test 6: invalid sentinel returns 0 */
|
||||||
|
{
|
||||||
|
uint32_t bad = arb_fork(ctx, arb_leaf(ctx), arb_leaf(ctx));
|
||||||
|
uint32_t result = arb_run_io(ctx, bad, &perms);
|
||||||
|
if (result != 0) {
|
||||||
|
fprintf(stderr, "FAIL: invalid sentinel should return 0\n");
|
||||||
|
arboricx_free(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("PASS: invalid sentinel\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
arboricx_free(ctx);
|
||||||
|
printf("\nAll IO run tests passed.\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
29
flake.nix
29
flake.nix
@@ -26,6 +26,18 @@
|
|||||||
haskellPackages.callCabal2nix packageName self {}
|
haskellPackages.callCabal2nix packageName self {}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
tricuBench =
|
||||||
|
hsLib.overrideCabal
|
||||||
|
(hsLib.doBenchmark (
|
||||||
|
haskellPackages.callCabal2nix packageName self {}
|
||||||
|
))
|
||||||
|
(oldAttrs: {
|
||||||
|
postInstall = (oldAttrs.postInstall or "") + ''
|
||||||
|
mkdir -p $out/bin
|
||||||
|
cp dist/build/tricu-bench/tricu-bench $out/bin/
|
||||||
|
'';
|
||||||
|
});
|
||||||
|
|
||||||
customGHC = haskellPackages.ghcWithPackages (hpkgs: with hpkgs; [
|
customGHC = haskellPackages.ghcWithPackages (hpkgs: with hpkgs; [
|
||||||
megaparsec
|
megaparsec
|
||||||
]);
|
]);
|
||||||
@@ -37,7 +49,8 @@
|
|||||||
pname = "tricu-zig";
|
pname = "tricu-zig";
|
||||||
version = "0.1.0";
|
version = "0.1.0";
|
||||||
src = ./ext/zig;
|
src = ./ext/zig;
|
||||||
nativeBuildInputs = [ pkgs.zig ];
|
nativeBuildInputs = [ pkgs.zig pkgs.pkg-config ];
|
||||||
|
buildInputs = [ pkgs.libuv ];
|
||||||
buildPhase = ''
|
buildPhase = ''
|
||||||
export ZIG_GLOBAL_CACHE_DIR=$TMPDIR/zig-cache
|
export ZIG_GLOBAL_CACHE_DIR=$TMPDIR/zig-cache
|
||||||
zig build
|
zig build
|
||||||
@@ -55,6 +68,7 @@
|
|||||||
version = "0.1.0";
|
version = "0.1.0";
|
||||||
src = ./.;
|
src = ./.;
|
||||||
nativeBuildInputs = [ pkgs.gcc pkgs.python3 tricuZig ];
|
nativeBuildInputs = [ pkgs.gcc pkgs.python3 tricuZig ];
|
||||||
|
buildInputs = [ pkgs.libuv ];
|
||||||
buildPhase = "true";
|
buildPhase = "true";
|
||||||
doCheck = true;
|
doCheck = true;
|
||||||
checkPhase = ''
|
checkPhase = ''
|
||||||
@@ -69,6 +83,18 @@
|
|||||||
-Wl,-rpath,${tricuZig}/lib
|
-Wl,-rpath,${tricuZig}/lib
|
||||||
/tmp/c_abi_test
|
/tmp/c_abi_test
|
||||||
|
|
||||||
|
# IO protocol shape test
|
||||||
|
gcc -o /tmp/io_protocol_test tests/io_protocol_test.c \
|
||||||
|
-I ${tricuZig}/include -L ${tricuZig}/lib -larboricx \
|
||||||
|
-Wl,-rpath,${tricuZig}/lib
|
||||||
|
/tmp/io_protocol_test
|
||||||
|
|
||||||
|
# IO run test (synchronous driver)
|
||||||
|
gcc -o /tmp/io_run_test tests/io_run_test.c \
|
||||||
|
-I ${tricuZig}/include -L ${tricuZig}/lib -larboricx \
|
||||||
|
-Wl,-rpath,${tricuZig}/lib
|
||||||
|
/tmp/io_run_test
|
||||||
|
|
||||||
# Kernel path append test
|
# Kernel path append test
|
||||||
gcc -o /tmp/c_abi_append_test tests/c_abi_append_test.c \
|
gcc -o /tmp/c_abi_append_test tests/c_abi_append_test.c \
|
||||||
-I ${tricuZig}/include -L ${tricuZig}/lib -larboricx \
|
-I ${tricuZig}/include -L ${tricuZig}/lib -larboricx \
|
||||||
@@ -195,6 +221,7 @@
|
|||||||
in {
|
in {
|
||||||
packages.${packageName} = tricuPackage;
|
packages.${packageName} = tricuPackage;
|
||||||
packages.default = tricuPackage;
|
packages.default = tricuPackage;
|
||||||
|
packages.tricu-bench = tricuBench;
|
||||||
packages.tricu-zig = tricuZig;
|
packages.tricu-zig = tricuZig;
|
||||||
packages.tricu-zig-tests = tricuZigTests;
|
packages.tricu-zig-tests = tricuZigTests;
|
||||||
packages.tricu-php = tricuPhp;
|
packages.tricu-php = tricuPhp;
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ module IODriver
|
|||||||
, runIOWith
|
, runIOWith
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Research (T(..), apply, toString, toNumber, ofString, ofNumber)
|
import Research (T(..), apply, toString, toNumber, ofString, ofNumber, ofBytes)
|
||||||
|
import qualified Data.ByteString as BS
|
||||||
import System.IO (putStr, getLine)
|
import System.IO (putStr, getLine)
|
||||||
import qualified System.IO as IO
|
import qualified System.IO as IO
|
||||||
import Control.Exception (try, IOException, SomeException)
|
import Control.Exception (try, IOException, SomeException)
|
||||||
@@ -487,9 +488,9 @@ stepMachine machine =
|
|||||||
in path == prefix || prefix' `isPrefixOf` path
|
in path == prefix || prefix' `isPrefixOf` path
|
||||||
|
|
||||||
tryReadFile path = do
|
tryReadFile path = do
|
||||||
result <- try (IO.readFile path) :: IO (Either IOException String)
|
result <- try (BS.readFile path) :: IO (Either IOException BS.ByteString)
|
||||||
case result of
|
case result of
|
||||||
Right content -> return $ okResult (ofString content)
|
Right content -> return $ okResult (ofBytes content)
|
||||||
Left e -> return $ errResult (ioErrorCode e)
|
Left e -> return $ errResult (ioErrorCode e)
|
||||||
|
|
||||||
tryWriteFile path contents = do
|
tryWriteFile path contents = do
|
||||||
|
|||||||
BIN
test/fixtures/greet-io.aboricx
vendored
Normal file
BIN
test/fixtures/greet-io.aboricx
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/runArboricxTyped.arboricx
vendored
Normal file
BIN
test/fixtures/runArboricxTyped.arboricx
vendored
Normal file
Binary file not shown.
43
tricu.cabal
43
tricu.cabal
@@ -78,6 +78,49 @@ executable tricu
|
|||||||
Wire
|
Wire
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
|
|
||||||
|
benchmark tricu-bench
|
||||||
|
type: exitcode-stdio-1.0
|
||||||
|
main-is: Bench.hs
|
||||||
|
hs-source-dirs: bench, src
|
||||||
|
default-extensions:
|
||||||
|
LambdaCase
|
||||||
|
MultiWayIf
|
||||||
|
OverloadedStrings
|
||||||
|
ScopedTypeVariables
|
||||||
|
build-depends:
|
||||||
|
base >=4.7
|
||||||
|
, ansi-terminal
|
||||||
|
, base16-bytestring
|
||||||
|
, base64-bytestring
|
||||||
|
, bytestring
|
||||||
|
, containers
|
||||||
|
, criterion
|
||||||
|
, cryptonite
|
||||||
|
, directory
|
||||||
|
, exceptions
|
||||||
|
, filepath
|
||||||
|
, megaparsec
|
||||||
|
, memory
|
||||||
|
, mtl
|
||||||
|
, sqlite-simple
|
||||||
|
, text
|
||||||
|
, time
|
||||||
|
, transformers
|
||||||
|
, vector
|
||||||
|
, zlib
|
||||||
|
default-language: Haskell2010
|
||||||
|
other-modules:
|
||||||
|
ApplyStats
|
||||||
|
ContentStore
|
||||||
|
Eval
|
||||||
|
FileEval
|
||||||
|
IODriver
|
||||||
|
Lexer
|
||||||
|
Parser
|
||||||
|
Paths_tricu
|
||||||
|
Research
|
||||||
|
Wire
|
||||||
|
|
||||||
test-suite tricu-tests
|
test-suite tricu-tests
|
||||||
type: exitcode-stdio-1.0
|
type: exitcode-stdio-1.0
|
||||||
main-is: Spec.hs
|
main-is: Spec.hs
|
||||||
|
|||||||
Reference in New Issue
Block a user