From 8d5e76db1cc439a0264f4f36de3e7fa1a61f5c09 Mon Sep 17 00:00:00 2001 From: James Eversole Date: Fri, 15 May 2026 21:41:19 -0500 Subject: [PATCH] Interaction Trees in Zig and simple benchmarks --- bench/ApplyStats.hs | 240 +++++++ bench/Bench.hs | 125 ++++ demos/runArboricxBundle.tri | 26 + docs/zig-io.md | 193 ++++++ ext/zig/build.zig | 4 + ext/zig/include/arboricx.h | 19 + ext/zig/src/c_abi.zig | 69 ++ ext/zig/src/io_driver.zig | 845 ++++++++++++++++++++++++ ext/zig/src/main.zig | 158 ++--- ext/zig/tests/c_abi_test.c | 62 ++ ext/zig/tests/io_protocol_test.c | 223 +++++++ ext/zig/tests/io_run_test.c | 217 ++++++ flake.nix | 29 +- src/IODriver.hs | 7 +- test/fixtures/greet-io.aboricx | Bin 0 -> 1415 bytes test/fixtures/runArboricxTyped.arboricx | Bin 0 -> 31015 bytes tricu.cabal | 43 ++ 17 files changed, 2179 insertions(+), 81 deletions(-) create mode 100644 bench/ApplyStats.hs create mode 100644 bench/Bench.hs create mode 100644 demos/runArboricxBundle.tri create mode 100644 docs/zig-io.md create mode 100644 ext/zig/src/io_driver.zig create mode 100644 ext/zig/tests/io_protocol_test.c create mode 100644 ext/zig/tests/io_run_test.c create mode 100644 test/fixtures/greet-io.aboricx create mode 100644 test/fixtures/runArboricxTyped.arboricx 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 0000000000000000000000000000000000000000..5509836c72e753e1a8d56e665ccb64a61d4d6c39 GIT binary patch literal 1415 zcmZ{jdsEaf6vb0mTu?+*R1_7&_oMawR&WTc@-RejUG?-@|j; zJ=qj7J8jM{=O#C~>2`YEPrc2xugX)>#;W(|wFK!^ztPyYT6*2#&{)&=e%IsUf28m9 zx?3MNK7Sd;Ea*JU@_w`%gk>5hCK%|nzioDk;OAXLFBZ8mex#GAOv)YK?3$=7beI@F z3%{GFK*($(q@LF9r!it<*z-2Qa4bl(*wkU8AN`1i7j?K0%6WOf{welm92`3mUZqY%ne1askj=*i#Y3xA$ zsrK8s=R%{_4_xOV;VOi*Y5^(LV}g|G2|-Hrlpv*gMvziHCp?09LAVd`l5huNg%Cij vy6}o{6QV=72GJ$RM%M`Ft7ebA4y@oc#0EjW!Z(Dsd!0=>684U88RGq4=Z9~0 literal 0 HcmV?d00001 diff --git a/test/fixtures/runArboricxTyped.arboricx b/test/fixtures/runArboricxTyped.arboricx new file mode 100644 index 0000000000000000000000000000000000000000..79a8092fcced154f42d737629cc799c46f6d2b64 GIT binary patch literal 31015 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 n-fUKpGXlUau%mGk0J}Qxb+Z<7I>KoQ;3%85a=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