diff --git a/bench/ApplyStats.hs b/bench/ApplyStats.hs new file mode 100644 index 0000000..155dc4d --- /dev/null +++ b/bench/ApplyStats.hs @@ -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 diff --git a/bench/Bench.hs b/bench/Bench.hs new file mode 100644 index 0000000..a59876f --- /dev/null +++ b/bench/Bench.hs @@ -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 + ] + + ] diff --git a/demos/runArboricxBundle.tri b/demos/runArboricxBundle.tri new file mode 100644 index 0000000..077d56f --- /dev/null +++ b/demos/runArboricxBundle.tri @@ -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") diff --git a/docs/zig-io.md b/docs/zig-io.md new file mode 100644 index 0000000..f0fbe00 --- /dev/null +++ b/docs/zig-io.md @@ -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`). diff --git a/ext/zig/build.zig b/ext/zig/build.zig index bf7b58c..f69913b 100644 --- a/ext/zig/build.zig +++ b/ext/zig/build.zig @@ -31,6 +31,8 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); exe_mod.addImport("kernel_embed", kernel_mod); + exe_mod.link_libc = true; + exe_mod.linkSystemLibrary("uv", .{}); const exe = b.addExecutable(.{ .name = "tricu-zig", .root_module = exe_mod, @@ -50,6 +52,8 @@ pub fn build(b: *std.Build) void { }); lib_mod.pic = true; lib_mod.addImport("kernel_embed", kernel_mod); + lib_mod.link_libc = true; + lib_mod.linkSystemLibrary("uv", .{}); const static_lib = b.addLibrary(.{ .name = "arboricx", .root_module = lib_mod, diff --git a/ext/zig/include/arboricx.h b/ext/zig/include/arboricx.h index 9e0e264..94fc5e4 100644 --- a/ext/zig/include/arboricx.h +++ b/ext/zig/include/arboricx.h @@ -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_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 */ uint32_t arb_kernel_root(arb_ctx_t* ctx); diff --git a/ext/zig/src/c_abi.zig b/ext/zig/src/c_abi.zig index 6e9a5b1..312a543 100644 --- a/ext/zig/src/c_abi.zig +++ b/ext/zig/src/c_abi.zig @@ -5,6 +5,7 @@ const reduce = @import("reduce.zig"); const codecs = @import("codecs.zig"); const kernel = @import("kernel.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. /// 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; } +// --------------------------------------------------------------------------- +// 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 // --------------------------------------------------------------------------- @@ -157,6 +209,23 @@ export fn arb_unwrap_host_value(ctx: *ArbCtx, root: u32, out_tag: *u64, out_payl 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 // --------------------------------------------------------------------------- diff --git a/ext/zig/src/io_driver.zig b/ext/zig/src/io_driver.zig new file mode 100644 index 0000000..256a7cf --- /dev/null +++ b/ext/zig/src/io_driver.zig @@ -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; +} diff --git a/ext/zig/src/main.zig b/ext/zig/src/main.zig index fc4b451..3748a69 100644 --- a/ext/zig/src/main.zig +++ b/ext/zig/src/main.zig @@ -5,6 +5,47 @@ const reduce = @import("reduce.zig"); const codecs = @import("codecs.zig"); const kernel = @import("kernel.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 { 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); + try printNode(arena, tag, result, io); +} - var stdout_buf: [4096]u8 = undefined; - var stdout = std.Io.File.stdout().writer(io, &stdout_buf); +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); - 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"); - }, + 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 } }); } - try stdout.flush(); + + 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 { @@ -98,43 +124,7 @@ fn runBundle(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []cons return error.InvalidHostValue; }; - var stdout_buf: [4096]u8 = undefined; - 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(); + try printNode(arena, hv.tag, hv.payload, io); } 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()); if (args.len < 2) { var stderr = std.Io.File.stderr().writer(io, &[_]u8{}); - try stderr.interface.writeAll("Usage: tricu-zig [--type TYPE] [--kernel] [--fuel N] [arg1 arg2 ...]\n"); + try stderr.interface.writeAll("Usage: tricu-zig [--type TYPE] [--kernel] [--io] [--unsafe-io] [--fuel N] [arg1 arg2 ...]\n"); try stderr.flush(); std.process.exit(1); } @@ -173,6 +163,8 @@ pub fn main(init: std.process.Init) !void { var arg_start: usize = 2; var use_kernel = false; + var use_io = false; + var io_perms = io_driver.IOPerms{}; var fuel: u64 = std.math.maxInt(u64); 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 (i + 1 >= args.len) { var stderr = std.Io.File.stderr().writer(io, &[_]u8{}); - try stderr.interface.writeAll("Usage: tricu-zig --type [--fuel N] [args...]\n"); + try stderr.interface.writeAll("Usage: tricu-zig --type [--io] [--unsafe-io] [--fuel N] [args...]\n"); try stderr.flush(); std.process.exit(1); } @@ -201,10 +193,15 @@ pub fn main(init: std.process.Init) !void { i += 1; } else if (std.mem.eql(u8, args[i], "--kernel")) { 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")) { if (i + 1 >= args.len) { var stderr = std.Io.File.stderr().writer(io, &[_]u8{}); - try stderr.interface.writeAll("Usage: tricu-zig --fuel [args...]\n"); + try stderr.interface.writeAll("Usage: tricu-zig --fuel [--io] [--unsafe-io] [args...]\n"); try stderr.flush(); std.process.exit(1); } @@ -225,7 +222,7 @@ pub fn main(init: std.process.Init) !void { if (bundle_idx >= args.len) { var stderr = std.Io.File.stderr().writer(io, &[_]u8{}); - try stderr.interface.writeAll("Usage: tricu-zig [--type TYPE] [--kernel] [--fuel N] [arg1 arg2 ...]\n"); + try stderr.interface.writeAll("Usage: tricu-zig [--type TYPE] [--kernel] [--io] [--unsafe-io] [--fuel N] [arg1 arg2 ...]\n"); try stderr.flush(); 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{}; - 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| { var stderr = std.Io.File.stderr().writer(io, &[_]u8{}); try stderr.interface.print("Execution failed: {s}\n", .{@errorName(err)}); diff --git a/ext/zig/tests/c_abi_test.c b/ext/zig/tests/c_abi_test.c index 5752af0..188ca4f 100644 --- a/ext/zig/tests/c_abi_test.c +++ b/ext/zig/tests/c_abi_test.c @@ -51,6 +51,68 @@ int main(void) { } 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); printf("\nAll C ABI tests passed.\n"); return 0; diff --git a/ext/zig/tests/io_protocol_test.c b/ext/zig/tests/io_protocol_test.c new file mode 100644 index 0000000..27272d8 --- /dev/null +++ b/ext/zig/tests/io_protocol_test.c @@ -0,0 +1,223 @@ +#include +#include +#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; +} diff --git a/ext/zig/tests/io_run_test.c b/ext/zig/tests/io_run_test.c new file mode 100644 index 0000000..5e52e58 --- /dev/null +++ b/ext/zig/tests/io_run_test.c @@ -0,0 +1,217 @@ +#include +#include +#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; +} diff --git a/flake.nix b/flake.nix index db89d69..8db877d 100644 --- a/flake.nix +++ b/flake.nix @@ -26,6 +26,18 @@ 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; [ megaparsec ]); @@ -37,7 +49,8 @@ pname = "tricu-zig"; version = "0.1.0"; src = ./ext/zig; - nativeBuildInputs = [ pkgs.zig ]; + nativeBuildInputs = [ pkgs.zig pkgs.pkg-config ]; + buildInputs = [ pkgs.libuv ]; buildPhase = '' export ZIG_GLOBAL_CACHE_DIR=$TMPDIR/zig-cache zig build @@ -55,6 +68,7 @@ version = "0.1.0"; src = ./.; nativeBuildInputs = [ pkgs.gcc pkgs.python3 tricuZig ]; + buildInputs = [ pkgs.libuv ]; buildPhase = "true"; doCheck = true; checkPhase = '' @@ -69,6 +83,18 @@ -Wl,-rpath,${tricuZig}/lib /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 gcc -o /tmp/c_abi_append_test tests/c_abi_append_test.c \ -I ${tricuZig}/include -L ${tricuZig}/lib -larboricx \ @@ -195,6 +221,7 @@ in { packages.${packageName} = tricuPackage; packages.default = tricuPackage; + packages.tricu-bench = tricuBench; packages.tricu-zig = tricuZig; packages.tricu-zig-tests = tricuZigTests; packages.tricu-php = tricuPhp; diff --git a/src/IODriver.hs b/src/IODriver.hs index edeb527..73e9404 100644 --- a/src/IODriver.hs +++ b/src/IODriver.hs @@ -8,7 +8,8 @@ module IODriver , runIOWith ) 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 qualified System.IO as IO import Control.Exception (try, IOException, SomeException) @@ -487,9 +488,9 @@ stepMachine machine = in path == prefix || prefix' `isPrefixOf` path 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 - Right content -> return $ okResult (ofString content) + Right content -> return $ okResult (ofBytes content) Left e -> return $ errResult (ioErrorCode e) tryWriteFile path contents = do diff --git a/test/fixtures/greet-io.aboricx b/test/fixtures/greet-io.aboricx new file mode 100644 index 0000000..5509836 Binary files /dev/null and b/test/fixtures/greet-io.aboricx differ diff --git a/test/fixtures/runArboricxTyped.arboricx b/test/fixtures/runArboricxTyped.arboricx new file mode 100644 index 0000000..79a8092 Binary files /dev/null and b/test/fixtures/runArboricxTyped.arboricx differ diff --git a/tricu.cabal b/tricu.cabal index cff4def..e110109 100644 --- a/tricu.cabal +++ b/tricu.cabal @@ -78,6 +78,49 @@ executable tricu Wire 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 type: exitcode-stdio-1.0 main-is: Spec.hs