Interaction Trees in Zig and simple benchmarks

This commit is contained in:
2026-05-15 21:41:19 -05:00
parent e3dcf5edd7
commit 8d5e76db1c
17 changed files with 2179 additions and 81 deletions

240
bench/ApplyStats.hs Normal file
View File

@@ -0,0 +1,240 @@
{-# LANGUAGE BangPatterns #-}
module ApplyStats
( ApplyStats(..)
, emptyApplyStats
, emptyApplyStatsSampled
, applyCounted
, runApplyCounted
, runApplySampledWithProgress
, runApplyGlobalCounted
, printApplyStats
) where
import Research
import qualified Data.Map.Strict as M
import qualified Data.List as L
import Data.Ord (comparing)
import Data.Text (Text)
import qualified Data.Text as T
import Debug.Trace (trace)
import System.IO.Unsafe (unsafePerformIO, unsafeDupablePerformIO)
import Data.IORef
-- ---------------------------------------------------------------------------
-- Threaded stats (slow but pure)
-- ---------------------------------------------------------------------------
type Hash = Text
type AppKey = (Hash, Hash)
data ApplyStats = ApplyStats
{ totalApplyCalls :: !Int
, uniqueApps :: !(M.Map AppKey Int)
, sampleInterval :: !Int
, sampleCounter :: !Int
, progressEvery :: !Int
}
deriving (Show)
emptyApplyStats :: ApplyStats
emptyApplyStats = emptyApplyStatsSampled 1
emptyApplyStatsSampled :: Int -> ApplyStats
emptyApplyStatsSampled n = ApplyStats
{ totalApplyCalls = 0
, uniqueApps = M.empty
, sampleInterval = max 1 n
, sampleCounter = 0
, progressEvery = 0
}
bump :: T -> T -> ApplyStats -> ApplyStats
bump !f !x !st =
let !counter' = sampleCounter st + 1
!total' = totalApplyCalls st + 1
!stBase = st { totalApplyCalls = total'
, sampleCounter = counter'
}
!st' = if counter' `mod` sampleInterval st /= 0
then stBase
else let !hf = termHash f
!hx = termHash x
!k = (hf, hx)
!m = M.insertWith (+) k 1 (uniqueApps st)
in stBase { uniqueApps = m }
in case progressEvery st of
0 -> st'
n | total' `mod` n == 0 ->
trace ("apply calls so far: " ++ show total') st'
_ -> st'
termHash :: T -> Hash
termHash Leaf =
nodeHash NLeaf
termHash (Stem t) =
nodeHash (NStem (termHash t))
termHash (Fork l r) =
nodeHash (NFork (termHash l) (termHash r))
applyCounted :: T -> T -> ApplyStats -> (T, ApplyStats)
applyCounted !f !x !st0 =
let !st1 = bump f x st0
in applyStepCounted f x st1
applyStepCounted :: T -> T -> ApplyStats -> (T, ApplyStats)
applyStepCounted (Fork Leaf a) _ st =
(a, st)
applyStepCounted (Fork (Stem a) b) c st =
let (!ac, !st1) = applyCounted a c st
(!bc, !st2) = applyCounted b c st1
in applyCounted ac bc st2
applyStepCounted (Fork (Fork a _b) _c) Leaf st =
(a, st)
applyStepCounted (Fork (Fork _a b) _c) (Stem u) st =
applyCounted b u st
applyStepCounted (Fork (Fork _a _b) c) (Fork u v) st =
let (!cu, !st1) = applyCounted c u st
in applyCounted cu v st1
applyStepCounted Leaf b st =
(Stem b, st)
applyStepCounted (Stem a) b st =
(Fork a b, st)
runApplyCounted :: T -> T -> (T, ApplyStats)
runApplyCounted !f !x =
applyCounted f x emptyApplyStats
runApplySampled :: Int -> T -> T -> (T, ApplyStats)
runApplySampled !n !f !x =
applyCounted f x (emptyApplyStatsSampled n)
runApplySampledWithProgress :: Int -> Int -> T -> T -> (T, ApplyStats)
runApplySampledWithProgress !interval !progress !f !x =
let st = (emptyApplyStatsSampled interval) { progressEvery = progress }
in applyCounted f x st
-- ---------------------------------------------------------------------------
-- Global mutable stats (fast, unsafe, single-threaded only)
-- ---------------------------------------------------------------------------
{-# NOINLINE globalTotalCount #-}
globalTotalCount :: IORef Int
globalTotalCount = unsafePerformIO (newIORef 0)
{-# NOINLINE globalInterval #-}
globalInterval :: IORef Int
globalInterval = unsafePerformIO (newIORef 1)
{-# NOINLINE globalMap #-}
globalMap :: IORef (M.Map AppKey Int)
globalMap = unsafePerformIO (newIORef M.empty)
{-# NOINLINE globalProgress #-}
globalProgress :: IORef Int
globalProgress = unsafePerformIO (newIORef 0)
resetGlobalStats :: Int -> Int -> IO ()
resetGlobalStats !interval !progress = do
writeIORef globalTotalCount 0
writeIORef globalInterval (max 1 interval)
writeIORef globalMap M.empty
writeIORef globalProgress progress
readGlobalStats :: IO ApplyStats
readGlobalStats = do
total <- readIORef globalTotalCount
m <- readIORef globalMap
pure ApplyStats
{ totalApplyCalls = total
, uniqueApps = m
, sampleInterval = 0
, sampleCounter = 0
, progressEvery = 0
}
{-# INLINE globalBump #-}
globalBump :: T -> T -> ()
globalBump !f !x = unsafeDupablePerformIO $ do
!total <- readIORef globalTotalCount
let !total' = total + 1
writeIORef globalTotalCount total'
!interval <- readIORef globalInterval
!progress <- readIORef globalProgress
let !_ = if progress > 0 && total' `mod` progress == 0
then trace ("apply calls so far: " ++ show total') ()
else ()
if total' `mod` interval /= 0
then pure ()
else do
let !hf = termHash f
!hx = termHash x
!k = (hf, hx)
!m <- readIORef globalMap
writeIORef globalMap (M.insertWith (+) k 1 m)
pure ()
applyGlobalCounted :: T -> T -> T
applyGlobalCounted !f !x =
let !_ = globalBump f x
in applyGlobalStep f x
applyGlobalStep :: T -> T -> T
applyGlobalStep (Fork Leaf a) _ = a
applyGlobalStep (Fork (Stem a) b) c =
applyGlobalCounted (applyGlobalCounted a c) (applyGlobalCounted b c)
applyGlobalStep (Fork (Fork a _b) _c) Leaf = a
applyGlobalStep (Fork (Fork _a b) _c) (Stem u) = applyGlobalCounted b u
applyGlobalStep (Fork (Fork _a _b) c) (Fork u v) =
applyGlobalCounted (applyGlobalCounted c u) v
applyGlobalStep Leaf b = Stem b
applyGlobalStep (Stem a) b = Fork a b
runApplyGlobalCounted :: Int -> Int -> T -> T -> IO (T, ApplyStats)
runApplyGlobalCounted !interval !progress !f !x = do
resetGlobalStats interval progress
let !result = applyGlobalCounted f x
!stats <- readGlobalStats
pure (result, stats)
-- ---------------------------------------------------------------------------
-- Printing
-- ---------------------------------------------------------------------------
printApplyStats :: ApplyStats -> IO ()
printApplyStats st = do
let !total = totalApplyCalls st
!uniq = M.size (uniqueApps st)
!ratio =
if uniq == 0
then 0 :: Double
else fromIntegral total / fromIntegral uniq
counts =
reverse
. L.sortBy (comparing snd)
. M.toList
$ uniqueApps st
repeated =
filter ((> 1) . snd) counts
top20 = take 20 repeated
putStrLn $ "total apply calls: " ++ show total
putStrLn $ "unique application patterns: " ++ show uniq
putStrLn $ "duplication ratio total/unique: " ++ show ratio
putStrLn $ "repeated application patterns: " ++ show (length repeated)
putStrLn "top repeated application counts:"
mapM_ printTop top20
where
short h = T.unpack (T.take 12 h)
printTop ((hf, hx), n) =
putStrLn $
" " ++ show n
++ "x apply "
++ short hf
++ " "
++ short hx

125
bench/Bench.hs Normal file
View File

@@ -0,0 +1,125 @@
{-# LANGUAGE BangPatterns #-}
module Main where
import Criterion.Main
import qualified Data.ByteString as BS
import qualified Data.Map as Map
import ApplyStats (runApplyCounted, runApplyGlobalCounted, printApplyStats)
import Eval
import FileEval
import Parser
import Research
-- | Pre-process a demo file and return its AST.
loadDemo :: FilePath -> IO [TricuAST]
loadDemo = preprocessFile
-- | Evaluate a pre-processed demo to its result term.
runDemo :: [TricuAST] -> T
runDemo ast = result (evalTricu Map.empty ast)
-- | Build an environment from a library file.
loadLib :: FilePath -> IO Env
loadLib = evaluateFile
main :: IO ()
main = do
!equalityAst <- loadDemo "demos/equality.tri"
!sizeAst <- loadDemo "demos/size.tri"
!toSourceAst <- loadDemo "demos/toSource.tri"
!levelOrderAst <- loadDemo "demos/levelOrderTraversal.tri"
!patternAst <- loadDemo "demos/patternMatching.tri"
!listLib <- loadLib "lib/list.tri"
-- Stress benchmark environment: Arboricx parser + size + toSource
!arboricxLib <- loadLib "lib/arboricx-dispatch.tri"
!sizeEnv <- evaluateFileWithContext arboricxLib "demos/size.tri"
!toSourceEnv <- evaluateFileWithContext sizeEnv "demos/toSource.tri"
-- Print apply stats for toSource not?
let Just toSource = Map.lookup "toSource" toSourceEnv
Just notTerm = Map.lookup "not?" toSourceEnv
(_result, stats) = runApplyCounted toSource notTerm
printApplyStats stats
-- Print apply stats for readArboricxContainer against id.arboricx
!idBundleBytes <- BS.readFile "test/fixtures/id.arboricx"
let Just readContainer = Map.lookup "readArboricxContainer" sizeEnv
bundleTree = ofBytes idBundleBytes
(_result2, stats2) <- runApplyGlobalCounted 100000 1000000 readContainer bundleTree
printApplyStats stats2
defaultMain
[ bgroup "demos"
[ bench "equality" $ whnf runDemo equalityAst
, bench "size" $ whnf runDemo sizeAst
, bench "toSource" $ whnf runDemo toSourceAst
, bench "levelOrderTraversal" $ whnf runDemo levelOrderAst
, bench "patternMatching" $ whnf runDemo patternAst
]
, bgroup "lib/list.tri"
[ bench "append strings" $ whnf
(result . evalTricu listLib . parseTricu)
"append \"Hello, \" \"world!\""
, bench "map over 3 elements" $ whnf
(result . evalTricu listLib . parseTricu)
"head (tail (map (a : (t t t)) [(t) (t) (t)]))"
, bench "equal? same" $ whnf
(result . evalTricu listLib . parseTricu)
"equal? (t t t) (t t t)"
, bench "equal? different" $ whnf
(result . evalTricu listLib . parseTricu)
"equal? (t t) (t t t)"
, bench "triage Leaf" $ whnf
(result . evalTricu listLib . parseTricu)
"test t"
, bench "triage Stem" $ whnf
(result . evalTricu listLib . parseTricu)
"test (t t)"
, bench "triage Fork" $ whnf
(result . evalTricu listLib . parseTricu)
"test (t t t)"
, bench "not? true" $ whnf
(result . evalTricu listLib . parseTricu)
"not? (t t)"
, bench "not? false" $ whnf
(result . evalTricu listLib . parseTricu)
"not? t"
]
, bgroup "stress"
[ bench "size runArboricxTyped" $ whnf
(result . evalTricu sizeEnv . parseTricu)
"size runArboricxTyped"
, bench "equal? runArboricxTyped runArboricxTyped" $ whnf
(result . evalTricu sizeEnv . parseTricu)
"equal? runArboricxTyped runArboricxTyped"
, bench "size readArboricxBundle" $ whnf
(result . evalTricu sizeEnv . parseTricu)
"size readArboricxBundle"
, bench "equal? readArboricxBundle readArboricxBundle" $ whnf
(result . evalTricu sizeEnv . parseTricu)
"equal? readArboricxBundle readArboricxBundle"
]
, bgroup "raw-apply"
[ bench "rule-1 (Fork Leaf a) b" $ whnf
(\n -> apply (Fork Leaf (ofNumber n)) (ofNumber 42))
1000
, bench "rule-2 (Fork (Stem a) b) c" $ whnf
(\n -> apply (Fork (Stem (ofNumber n)) (ofNumber n)) (ofNumber 42))
1000
, bench "rule-3a (Fork (Fork a b) c) Leaf" $ whnf
(\n -> apply (Fork (Fork (ofNumber n) (ofNumber n)) (ofNumber n)) Leaf)
1000
, bench "rule-3b (Fork (Fork a b) c) (Stem u)" $ whnf
(\n -> apply (Fork (Fork (ofNumber n) (ofNumber n)) (ofNumber n)) (Stem Leaf))
1000
, bench "rule-3c (Fork (Fork a b) c) (Fork u v)" $ whnf
(\n -> apply (Fork (Fork (ofNumber n) (ofNumber n)) (ofNumber n)) (Fork Leaf Leaf))
1000
]
]

View File

@@ -0,0 +1,26 @@
!import "../lib/base.tri" !Local
!import "../lib/list.tri" !Local
!import "../lib/io.tri" !Local
!import "../lib/arboricx.tri" !Local
-- Read an Arboricx bundle from disk and execute it.
-- This demo loads test/fixtures/id.arboricx and applies the
-- default export to the string "hi". The id bundle simply
-- returns its argument, so the expected output is:
-- hi
--
-- Run with --allow-read test/fixtures/id.arboricx or --unsafe-io.
runBundle = (path arg :
bind (readFile path)
(result :
matchResult
(err rest : putStrLn "ERROR: Could not read bundle file")
(bundleBytes rest :
matchResult
(err rest : putStrLn "ERROR: Could not execute bundle")
(value rest : putStrLn value)
(runArboricx bundleBytes arg))
result))
main = io (runBundle "test/fixtures/id.arboricx" "hi")

193
docs/zig-io.md Normal file
View File

@@ -0,0 +1,193 @@
# Zig Interaction-Tree IO Runtime Plan
## Goal
Port the Haskell `IODriver` interaction-tree system into the Zig host so that:
1. The Zig CLI (`tricu-zig`) can execute tricu programs with effects (`putStr`, `readFile`, `fork`, etc.).
2. The C FFI (`libarboricx`) exposes a single `arb_run_io` call, giving every language host (C, Python, PHP, Node) turnkey IO without reimplementing the protocol.
3. The fast native reduction path (currently ~0.005s for `id "hello"`) is used for pure computation; IO syscalls happen only at effect boundaries.
## Current State
| Host | Reduction Speed | IO Support |
|------|----------------|------------|
| Haskell interpreter | ~1.7s for `runArboricxTyped` demo | Full `IODriver.hs` with scheduler, async, permissions |
| Zig native | ~0.005s for `append` | None — pure reduction only |
| Zig kernel | ~0.235s for `id.arboricx` | None — runs self-hosted parser, no effects |
| C / Python / PHP FFI | Native Zig speed | None — can construct and reduce, cannot interpret interaction trees |
The Haskell `IODriver` is ~500 lines of stateful code (scheduler, frame stack, permission checks, async lifecycle). Replicating it in every host language is a maintenance hazard. We will implement it **once** in Zig and share it through the C ABI.
## Architecture
### Layer 1 — Tree Inspection Primitives (C FFI)
Minimal functions that let C (or other FFIs) inspect raw tree shape. Used internally by the driver, and exposed for non-POSIX hosts that need custom effect handlers.
```c
int arb_is_leaf(arb_ctx_t* ctx, uint32_t root);
int arb_is_stem(arb_ctx_t* ctx, uint32_t root);
int arb_is_fork(arb_ctx_t* ctx, uint32_t root);
int arb_get_stem_child(arb_ctx_t* ctx, uint32_t root, uint32_t* out);
int arb_get_fork_children(arb_ctx_t* ctx, uint32_t root,
uint32_t* out_left, uint32_t* out_right);
```
### Layer 2 — POSIX IO Driver (C FFI)
A single high-level call that runs the full interaction-tree loop:
```c
typedef struct {
int allow_read_all;
int allow_write_all;
const char** allowed_read_paths;
size_t allowed_read_count;
const char** allowed_write_paths;
size_t allowed_write_count;
} arb_io_perms_t;
// Reduce → decode action → perform syscall → feed result → repeat until pure.
// Returns the final pure tree value.
uint32_t arb_run_io(arb_ctx_t* ctx, uint32_t program,
const arb_io_perms_t* perms);
```
This is the only call 99% of hosts need. It contains the exact same logic as `IODriver.hs`:
- **Frame stack** — `BindFrame` (sequencing) and `LocalFrame` (environment scoping)
- **Runtime** — permissions, environment tree, mutable state tree
- **Action dispatch** — decode the tag (pure, bind, putStr, getLine, readFile, writeFile, ask, local, get, put, fork, await, yield, sleep)
- **Scheduler** — runnable queue, blocked tasks, sleeping tasks, wake-on-completion, deadlock detection
- **Error protocol** — ok/err pairs with numeric codes
### Zig CLI Integration
Add `--io` and `--unsafe-io` flags to `tricu-zig`:
```bash
# Safe mode — no filesystem access (default when --io is used)
tricu-zig --io greet.arboricx
# Unsafe mode — full POSIX access (development / local scripts)
tricu-zig --io --unsafe-io writeThenRead.arboricx
# Specific paths
# (future: --allow-read ./foo --allow-write ./bar)
```
Under `--io`, the CLI loads the bundle, reduces it once to WHNF, then passes the root to `arb_run_io` instead of eagerly decoding the final value.
## Implementation Stages
### Stage 1 — Tree Inspection Primitives
Add the five inspection functions to `ext/zig/src/c_abi.zig` and `ext/zig/include/arboricx.h`. No logic changes to reduction; these just read arena node tags.
**Acceptance:** A C test program can walk an arbitrary tree built with `arb_fork`/`arb_stem`/`arb_leaf` without knowing the arena internals.
### Stage 2 — IO Protocol Decoder
Write `ext/zig/src/io_driver.zig` containing:
- `decodeAction` — inspect a reduced tree and identify the action tag (pure=0, bind=1, putStr=10, …)
- `isIOSentinel` — verify `"tricuIO"` sentinel and version
- `makePure`, `makeOkResult`, `makeErrResult` — construct standard response trees
These are pure Zig functions with no syscalls. They mirror `IODriver.hs` logic but operate on arena indices.
**Acceptance:** Unit tests decode each action type correctly from trees built via codecs.
### Stage 3 — Synchronous IO Loop
Implement the core driver loop with a frame stack:
```zig
while (true) {
current = reduce.reduce(current, scratch_arena, fuel);
if (isIOSentinel(current)) |action| {
switch (decodeAction(action)) {
.pure => { /* pop frame or return */ },
.bind => { /* push BindFrame, recurse into left */ },
.putStr => { /* write stdout, continue with Leaf */ },
.getLine => { /* read stdin, continue with string */ },
// ... etc
}
} else {
return current; // pure result
}
}
```
Support synchronous actions only: `pure`, `bind`, `putStr`, `getLine`, `readFile`, `writeFile`, `ask`, `local`, `get`, `put`.
**Acceptance:** `greet.tri` and `writeThenRead.tri` run correctly through `tricu-zig --io`.
### Stage 4 — Scheduler and Async Actions
Add the task scheduler for `fork`, `await`, `yield`, `sleep`:
- `Runnable` queue (FIFO)
- `BlockedOn` map (task → blocked task ID)
- `Sleeping` map (task → wake time)
- Round-robin scheduling with `yield` and `sleep` support
- Deadlock detection when no runnable tasks remain and no sleepers
This mirrors `IODriver.hs` exactly, including task handle encoding (`Fork("task", n)`).
**Acceptance:** `demos/interactionTrees/forkAwait.tri` and `yield.tri` pass.
### Stage 5 — Permission System
Port path canonicalization and permission checks from Haskell:
- Syntactic normalization (resolve `.`, reject `..`)
- `--unsafe-io` bypass (allow all)
- `--allow-read PATH` / `--allow-write PATH` allowlists
- Error code 20 (`errPolicyDeny`) on violation
**Acceptance:** File operations outside allowed paths return err pairs, not crashes.
### Stage 6 — FFI Integration and Host Rollout
- Expose `arb_run_io` in the C header
- Update Python FFI test to verify IO round-trip
- Update PHP wrapper to support `--io`
- Document the two-layer model for future hosts (use `arb_run_io` for POSIX, Layer 1 primitives for custom runtimes)
**Acceptance:** Every existing FFI test still passes; new IO test passes in Python.
## Design Decisions
### Why baked-in POSIX effects?
- Most hosts (C, Python, PHP, native CLI) want real stdout/stdin/files.
- One canonical implementation avoids divergence.
- The Haskell `IODriver.hs` remains the reference spec; the Zig driver is the production runtime.
### Why not callback-based by default?
Callbacks add complexity for the common case. If a non-POSIX host (e.g., browser JS) needs custom effects, it can use the Layer 1 inspection primitives to build a ~50-line shim without reimplementing the scheduler. We can add `arb_run_io_with_callbacks` later if demand exists.
### Why not implement in every host language?
The Haskell `IODriver` is subtle: frame stack unwinding, async lifecycle, deadlock detection, path canonicalization, error code protocol. Bugs in any reimplementation would fracture the language ecosystem. A shared native driver is the only maintainable answer.
## Risks and Open Questions
1. **Fuel exhaustion during IO loops**`arb_run_io` internally calls `reduce.reduce` with a fuel parameter. Should it accept a total fuel budget, or reset fuel per reduction step? The Haskell side has no fuel limit; we may want `arb_run_io_unlimited` and `arb_run_io_fueled` variants.
2. **State threading** — The Haskell driver threads an environment and mutable state tree through the runtime. These are opaque `T` values manipulated by tricu code. The Zig driver must preserve them exactly across scheduler switches.
3. **Binary vs text I/O**`readFile` currently returns bytes (via `ofBytes` / `toString` in Haskell). The Zig driver must match the encoding exactly so that tricu code sees the same values in both hosts.
4. **Error parity** — Every error code (199) 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`).

View File

@@ -31,6 +31,8 @@ pub fn build(b: *std.Build) void {
.optimize = optimize, .optimize = optimize,
}); });
exe_mod.addImport("kernel_embed", kernel_mod); exe_mod.addImport("kernel_embed", kernel_mod);
exe_mod.link_libc = true;
exe_mod.linkSystemLibrary("uv", .{});
const exe = b.addExecutable(.{ const exe = b.addExecutable(.{
.name = "tricu-zig", .name = "tricu-zig",
.root_module = exe_mod, .root_module = exe_mod,
@@ -50,6 +52,8 @@ pub fn build(b: *std.Build) void {
}); });
lib_mod.pic = true; lib_mod.pic = true;
lib_mod.addImport("kernel_embed", kernel_mod); lib_mod.addImport("kernel_embed", kernel_mod);
lib_mod.link_libc = true;
lib_mod.linkSystemLibrary("uv", .{});
const static_lib = b.addLibrary(.{ const static_lib = b.addLibrary(.{
.name = "arboricx", .name = "arboricx",
.root_module = lib_mod, .root_module = lib_mod,

View File

@@ -40,6 +40,25 @@ int arb_to_bool(arb_ctx_t* ctx, uint32_t root, int* out);
int arb_unwrap_result(arb_ctx_t* ctx, uint32_t root, int* out_ok, uint32_t* out_value, uint32_t* out_rest); int arb_unwrap_result(arb_ctx_t* ctx, uint32_t root, int* out_ok, uint32_t* out_value, uint32_t* out_rest);
int arb_unwrap_host_value(arb_ctx_t* ctx, uint32_t root, uint64_t* out_tag, uint32_t* out_payload); int arb_unwrap_host_value(arb_ctx_t* ctx, uint32_t root, uint64_t* out_tag, uint32_t* out_payload);
/* Tree inspection (Layer 1 — for custom IO drivers and non-POSIX hosts) */
int arb_is_leaf(arb_ctx_t* ctx, uint32_t root);
int arb_is_stem(arb_ctx_t* ctx, uint32_t root);
int arb_is_fork(arb_ctx_t* ctx, uint32_t root);
int arb_is_app(arb_ctx_t* ctx, uint32_t root);
int arb_get_stem_child(arb_ctx_t* ctx, uint32_t root, uint32_t* out);
int arb_get_fork_children(arb_ctx_t* ctx, uint32_t root,
uint32_t* out_left, uint32_t* out_right);
int arb_get_app_func_arg(arb_ctx_t* ctx, uint32_t root,
uint32_t* out_func, uint32_t* out_arg);
/* IO driver (Layer 2 — POSIX interaction-tree runtime) */
typedef struct {
int allow_read_all;
int allow_write_all;
} arb_io_perms_t;
uint32_t arb_run_io(arb_ctx_t* ctx, uint32_t program, const arb_io_perms_t* perms);
/* Kernel entrypoints */ /* Kernel entrypoints */
uint32_t arb_kernel_root(arb_ctx_t* ctx); uint32_t arb_kernel_root(arb_ctx_t* ctx);

View File

@@ -5,6 +5,7 @@ const reduce = @import("reduce.zig");
const codecs = @import("codecs.zig"); const codecs = @import("codecs.zig");
const kernel = @import("kernel.zig"); const kernel = @import("kernel.zig");
const bundle = @import("bundle.zig"); const bundle = @import("bundle.zig");
const io_driver = @import("io_driver.zig");
/// Opaque handle for the C API. Layout is not exposed to C. /// Opaque handle for the C API. Layout is not exposed to C.
/// Holds a persistent arena for user-built terms and the kernel. /// Holds a persistent arena for user-built terms and the kernel.
@@ -59,6 +60,57 @@ export fn arb_app(ctx: *ArbCtx, func: u32, arg: u32) u32 {
return ctx.arena.alloc(.{ .app = .{ .func = func, .arg = arg } }) catch 0; return ctx.arena.alloc(.{ .app = .{ .func = func, .arg = arg } }) catch 0;
} }
// ---------------------------------------------------------------------------
// Tree inspection (Layer 1 — for custom IO drivers and non-POSIX hosts)
// All return 1 on success / true, 0 on failure / false.
// ---------------------------------------------------------------------------
export fn arb_is_leaf(ctx: *ArbCtx, root: u32) c_int {
if (root >= ctx.arena.len()) return 0;
return if (ctx.arena.nodes.items[root] == .leaf) 1 else 0;
}
export fn arb_is_stem(ctx: *ArbCtx, root: u32) c_int {
if (root >= ctx.arena.len()) return 0;
return if (ctx.arena.nodes.items[root] == .stem) 1 else 0;
}
export fn arb_is_fork(ctx: *ArbCtx, root: u32) c_int {
if (root >= ctx.arena.len()) return 0;
return if (ctx.arena.nodes.items[root] == .fork) 1 else 0;
}
export fn arb_is_app(ctx: *ArbCtx, root: u32) c_int {
if (root >= ctx.arena.len()) return 0;
return if (ctx.arena.nodes.items[root] == .app) 1 else 0;
}
export fn arb_get_stem_child(ctx: *ArbCtx, root: u32, out: *u32) c_int {
if (root >= ctx.arena.len()) return 0;
const node = ctx.arena.nodes.items[root];
if (node != .stem) return 0;
out.* = node.stem.child;
return 1;
}
export fn arb_get_fork_children(ctx: *ArbCtx, root: u32, out_left: *u32, out_right: *u32) c_int {
if (root >= ctx.arena.len()) return 0;
const node = ctx.arena.nodes.items[root];
if (node != .fork) return 0;
out_left.* = node.fork.left;
out_right.* = node.fork.right;
return 1;
}
export fn arb_get_app_func_arg(ctx: *ArbCtx, root: u32, out_func: *u32, out_arg: *u32) c_int {
if (root >= ctx.arena.len()) return 0;
const node = ctx.arena.nodes.items[root];
if (node != .app) return 0;
out_func.* = node.app.func;
out_arg.* = node.app.arg;
return 1;
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Reduction // Reduction
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -157,6 +209,23 @@ export fn arb_unwrap_host_value(ctx: *ArbCtx, root: u32, out_tag: *u64, out_payl
return 1; return 1;
} }
// ---------------------------------------------------------------------------
// IO driver (Layer 2 — POSIX interaction-tree runtime)
// ---------------------------------------------------------------------------
pub const arb_io_perms_t = extern struct {
allow_read_all: c_int,
allow_write_all: c_int,
};
export fn arb_run_io(ctx: *ArbCtx, program: u32, perms: ?*const arb_io_perms_t) u32 {
const zig_perms = if (perms) |p| io_driver.IOPerms{
.allow_read_all = p.allow_read_all != 0,
.allow_write_all = p.allow_write_all != 0,
} else io_driver.IOPerms{};
return io_driver.runIO(ctx.gpa, &ctx.arena, program, zig_perms) catch 0;
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Kernel entrypoints // Kernel entrypoints
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

845
ext/zig/src/io_driver.zig Normal file
View File

@@ -0,0 +1,845 @@
const std = @import("std");
const Arena = @import("arena.zig").Arena;
const codecs = @import("codecs.zig");
const reduce = @import("reduce.zig");
const tree = @import("tree.zig");
const c = @cImport({
@cInclude("uv.h");
});
// ---------------------------------------------------------------------------
// Action tag constants (must match lib/io.tri and IODriver.hs)
// ---------------------------------------------------------------------------
pub const ActionTag = enum(u8) {
pure = 0,
bind = 1,
putStr = 10,
getLine = 11,
readFile = 20,
writeFile = 21,
ask = 30,
local = 31,
get = 40,
put = 41,
fork = 60,
await = 61,
yield = 62,
sleep = 63,
};
pub const Action = union(ActionTag) {
pure: u32,
bind: struct { left: u32, k: u32 },
putStr: u32,
getLine,
readFile: u32,
writeFile: struct { path: u32, contents: u32 },
ask,
local: struct { f: u32, action: u32 },
get,
put: u32,
fork: u32,
await: u32,
yield,
sleep: u32,
};
// ---------------------------------------------------------------------------
// Error codes (must match IODriver.hs)
// ---------------------------------------------------------------------------
const ERR_DOES_NOT_EXIST: u64 = 1;
const ERR_PERMISSION: u64 = 2;
const ERR_ALREADY_EXISTS: u64 = 3;
const ERR_IO_OTHER: u64 = 4;
const ERR_POLICY_DENY: u64 = 20;
const ERR_INVALID_ACTION: u64 = 40;
const ERR_INVALID_STRING: u64 = 41;
// ---------------------------------------------------------------------------
// Permissions
// ---------------------------------------------------------------------------
pub const IOPerms = struct {
allow_read_all: bool = false,
allow_write_all: bool = false,
};
// ---------------------------------------------------------------------------
// IO sentinel detection
// ---------------------------------------------------------------------------
pub fn isIOSentinel(arena: *Arena, root: u32) !?u32 {
const node = arena.get(root);
if (node.* != .fork) return null;
const sentinel = node.fork.left;
const rest = node.fork.right;
const sentinel_str = try codecs.toString(arena, sentinel);
defer {
if (sentinel_str) |s| {
arena.allocator.free(s);
}
}
if (sentinel_str == null) return null;
if (!std.mem.eql(u8, sentinel_str.?, "tricuIO")) return null;
const rest_node = arena.get(rest);
if (rest_node.* != .fork) return null;
const version_num = try codecs.toNumber(arena, rest_node.fork.left);
if (version_num == null or version_num.? != 1) return null;
return rest_node.fork.right;
}
// ---------------------------------------------------------------------------
// Action decoding
// ---------------------------------------------------------------------------
pub fn decodeAction(arena: *Arena, root: u32) !?Action {
const node = arena.get(root);
if (node.* != .fork) return null;
const tag_num = try codecs.toNumber(arena, node.fork.left);
if (tag_num == null) return null;
const tag: ActionTag = switch (tag_num.?) {
0 => .pure,
1 => .bind,
10 => .putStr,
11 => .getLine,
20 => .readFile,
21 => .writeFile,
30 => .ask,
31 => .local,
40 => .get,
41 => .put,
60 => .fork,
61 => .await,
62 => .yield,
63 => .sleep,
else => return null,
};
const payload = node.fork.right;
return switch (tag) {
.pure => Action{ .pure = payload },
.bind => blk: {
const payload_node = arena.get(payload);
if (payload_node.* != .fork) return null;
break :blk Action{ .bind = .{ .left = payload_node.fork.left, .k = payload_node.fork.right } };
},
.putStr => Action{ .putStr = payload },
.getLine => Action.getLine,
.readFile => Action{ .readFile = payload },
.writeFile => blk: {
const payload_node = arena.get(payload);
if (payload_node.* != .fork) return null;
break :blk Action{ .writeFile = .{ .path = payload_node.fork.left, .contents = payload_node.fork.right } };
},
.ask => Action.ask,
.local => blk: {
const payload_node = arena.get(payload);
if (payload_node.* != .fork) return null;
break :blk Action{ .local = .{ .f = payload_node.fork.left, .action = payload_node.fork.right } };
},
.get => Action.get,
.put => Action{ .put = payload },
.fork => Action{ .fork = payload },
.await => Action{ .await = payload },
.yield => Action.yield,
.sleep => Action{ .sleep = payload },
};
}
// ---------------------------------------------------------------------------
// Response tree constructors
// ---------------------------------------------------------------------------
pub fn makePure(arena: *Arena, val: u32) !u32 {
const tag = try codecs.ofNumber(arena, 0);
return try arena.alloc(.{ .fork = .{ .left = tag, .right = val } });
}
pub fn makeOkResult(arena: *Arena, val: u32) !u32 {
const ok_tag = try arena.alloc(.{ .stem = .{ .child = try arena.alloc(.leaf) } });
const val_pair = try arena.alloc(.{ .fork = .{ .left = val, .right = try arena.alloc(.leaf) } });
return try arena.alloc(.{ .fork = .{ .left = ok_tag, .right = val_pair } });
}
pub fn makeErrResult(arena: *Arena, code: u64) !u32 {
const code_tree = try codecs.ofNumber(arena, code);
const code_pair = try arena.alloc(.{ .fork = .{ .left = code_tree, .right = try arena.alloc(.leaf) } });
return try arena.alloc(.{ .fork = .{ .left = try arena.alloc(.leaf), .right = code_pair } });
}
// ---------------------------------------------------------------------------
// Frame stack and runtime
// ---------------------------------------------------------------------------
const Frame = union(enum) {
bind: u32, // continuation k
local: u32, // old env
};
const Runtime = struct {
env: u32,
state: u32,
};
// ---------------------------------------------------------------------------
// Helper: reduce a term in a scratch arena and copy the result back
// ---------------------------------------------------------------------------
fn reduceInScratch(gpa: std.mem.Allocator, arena: *Arena, term: u32) !u32 {
var scratch = Arena.init(gpa);
defer scratch.deinit();
const scratch_root = try tree.copyTree(arena.nodes.items, &scratch, term);
const scratch_result = try reduce.reduce(scratch_root, &scratch, std.math.maxInt(u64));
return try tree.copyTree(scratch.nodes.items, arena, scratch_result);
}
// ---------------------------------------------------------------------------
// Task
// ---------------------------------------------------------------------------
const Task = struct {
id: u64,
parent: ?*Task,
frames: std.ArrayList(Frame),
runtime: Runtime,
current: u32,
status: enum { runnable, blocked, completed },
result: ?u32,
waiting_for: ?u64,
fn init(gpa: std.mem.Allocator, id: u64, parent: ?*Task, env: u32, state: u32, current: u32) !*Task {
const task = try gpa.create(Task);
task.* = .{
.id = id,
.parent = parent,
.frames = std.ArrayList(Frame).empty,
.runtime = .{ .env = env, .state = state },
.current = current,
.status = .runnable,
.result = null,
.waiting_for = null,
};
return task;
}
fn deinit(self: *Task, gpa: std.mem.Allocator) void {
self.frames.deinit(gpa);
gpa.destroy(self);
}
// finishValue processes a value through the frame stack.
// Returns true if the task has completed (no more frames).
fn finishValue(self: *Task, arena: *Arena, value: u32) !bool {
if (self.frames.pop()) |frame| {
switch (frame) {
.bind => |k| {
self.current = try arena.alloc(.{ .app = .{ .func = k, .arg = value } });
return false;
},
.local => |old_env| {
self.runtime.env = old_env;
self.current = try makePure(arena, value);
return false;
},
}
} else {
self.current = value;
return true;
}
}
};
// ---------------------------------------------------------------------------
// Scheduler
// ---------------------------------------------------------------------------
const Scheduler = struct {
gpa: std.mem.Allocator,
loop: *c.uv_loop_t,
arena: *Arena,
tasks: std.ArrayList(*Task),
runnable: std.ArrayList(*Task),
next_id: u64,
perms: IOPerms,
fn init(gpa: std.mem.Allocator, loop: *c.uv_loop_t, arena: *Arena, perms: IOPerms) !Scheduler {
const sched = Scheduler{
.gpa = gpa,
.loop = loop,
.arena = arena,
.tasks = std.ArrayList(*Task).empty,
.runnable = std.ArrayList(*Task).empty,
.next_id = 1,
.perms = perms,
};
return sched;
}
fn deinit(self: *Scheduler) void {
for (self.tasks.items) |task| {
task.deinit(self.gpa);
}
self.tasks.deinit(self.gpa);
self.runnable.deinit(self.gpa);
}
fn createTask(self: *Scheduler, parent: ?*Task, env: u32, state: u32, current: u32) !*Task {
const id = self.next_id;
self.next_id += 1;
const task = try Task.init(self.gpa, id, parent, env, state, current);
try self.tasks.append(self.gpa, task);
return task;
}
fn run(self: *Scheduler) !void {
while (true) {
if (self.runnable.items.len > 0) {
const task = self.runnable.orderedRemove(0);
try self.stepTask(task);
} else if (self.hasPendingHandles()) {
_ = c.uv_run(self.loop, c.UV_RUN_ONCE);
} else {
break;
}
}
}
fn hasPendingHandles(self: *Scheduler) bool {
return c.uv_loop_alive(self.loop) != 0;
}
fn completeTask(self: *Scheduler, task: *Task) !void {
task.status = .completed;
task.result = task.current;
// Unblock any tasks waiting for this one
for (self.tasks.items) |t| {
if (t.status == .blocked and t.waiting_for == task.id) {
t.status = .runnable;
t.waiting_for = null;
t.current = try makePure(self.arena, task.result.?);
try self.runnable.append(self.gpa, t);
}
}
}
fn stepTask(self: *Scheduler, task: *Task) !void {
const reduced = try reduceInScratch(self.gpa, self.arena, task.current);
const decoded = try decodeAction(self.arena, reduced);
if (decoded == null) {
// Not a recognized action — if no frames, it's the final result.
// Otherwise treat as invalid.
if (task.frames.items.len == 0) {
task.current = reduced;
try self.completeTask(task);
return;
}
const err = try makeErrResult(self.arena, ERR_INVALID_ACTION);
if (try task.finishValue(self.arena, err)) {
try self.completeTask(task);
} else {
try self.runnable.append(self.gpa, task);
}
return;
}
switch (decoded.?) {
.pure => |val| {
if (try task.finishValue(self.arena, val)) {
try self.completeTask(task);
} else {
try self.runnable.append(self.gpa, task);
}
},
.bind => |b| {
try task.frames.append(self.gpa, .{ .bind = b.k });
task.current = b.left;
try self.runnable.append(self.gpa, task);
},
.putStr => |str_tree| {
const str = try codecs.toString(self.arena, str_tree) orelse {
const err = try makeErrResult(self.arena, ERR_INVALID_STRING);
if (try task.finishValue(self.arena, err)) {
try self.completeTask(task);
} else {
try self.runnable.append(self.gpa, task);
}
return;
};
defer self.gpa.free(str);
_ = std.c.write(1, str.ptr, str.len);
const leaf = try self.arena.alloc(.leaf);
if (try task.finishValue(self.arena, leaf)) {
try self.completeTask(task);
} else {
try self.runnable.append(self.gpa, task);
}
},
.getLine => {
var buf: [4096]u8 = undefined;
var len: usize = 0;
while (len < buf.len) {
const n = std.c.read(0, buf[len..].ptr, 1);
if (n <= 0) break;
if (buf[len] == '\n') break;
len += 1;
}
const line = buf[0..len];
const str_tree = try codecs.ofString(self.arena, line);
if (try task.finishValue(self.arena, str_tree)) {
try self.completeTask(task);
} else {
try self.runnable.append(self.gpa, task);
}
},
.readFile => |path_tree| {
const path = try codecs.toString(self.arena, path_tree) orelse {
const err = try makeErrResult(self.arena, ERR_INVALID_STRING);
if (try task.finishValue(self.arena, err)) {
try self.completeTask(task);
} else {
try self.runnable.append(self.gpa, task);
}
return;
};
if (!self.perms.allow_read_all) {
self.arena.allocator.free(path);
const err = try makeErrResult(self.arena, ERR_POLICY_DENY);
if (try task.finishValue(self.arena, err)) {
try self.completeTask(task);
} else {
try self.runnable.append(self.gpa, task);
}
return;
}
const ctx = try self.gpa.create(FileReadCtx);
ctx.* = .{
.scheduler = self,
.task = task,
.arena = self.arena,
.gpa = self.gpa,
.fd = -1,
.buf = std.ArrayList(u8).empty,
.path = path,
.req = undefined,
.read_buf = null,
};
ctx.req.data = ctx;
_ = c.uv_fs_open(self.loop, &ctx.req, ctx.path.ptr, c.O_RDONLY, 0, file_open_cb);
},
.writeFile => |wf| {
const path = try codecs.toString(self.arena, wf.path) orelse {
const err = try makeErrResult(self.arena, ERR_INVALID_STRING);
if (try task.finishValue(self.arena, err)) {
try self.completeTask(task);
} else {
try self.runnable.append(self.gpa, task);
}
return;
};
const contents = try codecs.toString(self.arena, wf.contents) orelse {
self.arena.allocator.free(path);
const err = try makeErrResult(self.arena, ERR_INVALID_STRING);
if (try task.finishValue(self.arena, err)) {
try self.completeTask(task);
} else {
try self.runnable.append(self.gpa, task);
}
return;
};
if (!self.perms.allow_write_all) {
self.arena.allocator.free(path);
self.arena.allocator.free(contents);
const err = try makeErrResult(self.arena, ERR_POLICY_DENY);
if (try task.finishValue(self.arena, err)) {
try self.completeTask(task);
} else {
try self.runnable.append(self.gpa, task);
}
return;
}
const ctx = try self.gpa.create(FileWriteCtx);
ctx.* = .{
.scheduler = self,
.task = task,
.arena = self.arena,
.gpa = self.gpa,
.fd = -1,
.path = path,
.contents = contents,
.written = false,
.req = undefined,
};
ctx.req.data = ctx;
const flags = c.O_WRONLY | c.O_CREAT | c.O_TRUNC;
_ = c.uv_fs_open(self.loop, &ctx.req, ctx.path.ptr, flags, 0o644, file_write_open_cb);
},
.ask => {
if (try task.finishValue(self.arena, task.runtime.env)) {
try self.completeTask(task);
} else {
try self.runnable.append(self.gpa, task);
}
},
.local => |loc| {
const new_env = try reduceInScratch(self.gpa, self.arena, try self.arena.alloc(.{ .app = .{ .func = loc.f, .arg = task.runtime.env } }));
try task.frames.append(self.gpa, .{ .local = task.runtime.env });
task.runtime.env = new_env;
task.current = loc.action;
try self.runnable.append(self.gpa, task);
},
.get => {
if (try task.finishValue(self.arena, task.runtime.state)) {
try self.completeTask(task);
} else {
try self.runnable.append(self.gpa, task);
}
},
.put => |new_state| {
task.runtime.state = new_state;
const leaf = try self.arena.alloc(.leaf);
if (try task.finishValue(self.arena, leaf)) {
try self.completeTask(task);
} else {
try self.runnable.append(self.gpa, task);
}
},
.fork => |action| {
const child = try self.createTask(task, task.runtime.env, task.runtime.state, action);
try self.runnable.append(self.gpa, child);
const handle = try codecs.ofNumber(self.arena, child.id);
if (try task.finishValue(self.arena, handle)) {
try self.completeTask(task);
} else {
try self.runnable.append(self.gpa, task);
}
},
.await => |handle_tree| {
const handle = try codecs.toNumber(self.arena, handle_tree) orelse {
const err = try makeErrResult(self.arena, ERR_INVALID_ACTION);
if (try task.finishValue(self.arena, err)) {
try self.completeTask(task);
} else {
try self.runnable.append(self.gpa, task);
}
return;
};
var found: ?*Task = null;
for (self.tasks.items) |t| {
if (t.id == handle) {
found = t;
break;
}
}
if (found == null) {
const err = try makeErrResult(self.arena, ERR_INVALID_ACTION);
if (try task.finishValue(self.arena, err)) {
try self.completeTask(task);
} else {
try self.runnable.append(self.gpa, task);
}
return;
}
if (found.?.status == .completed) {
const result = found.?.result.?;
if (try task.finishValue(self.arena, result)) {
try self.completeTask(task);
} else {
try self.runnable.append(self.gpa, task);
}
} else {
task.status = .blocked;
task.waiting_for = handle;
// Task remains out of runnable until child completes
}
},
.yield => {
const leaf = try self.arena.alloc(.leaf);
if (try task.finishValue(self.arena, leaf)) {
try self.completeTask(task);
} else {
try self.runnable.append(self.gpa, task);
}
},
.sleep => |ms_tree| {
const ms = try codecs.toNumber(self.arena, ms_tree) orelse 0;
const ctx = try self.gpa.create(SleepCtx);
ctx.* = .{
.scheduler = self,
.task = task,
.arena = self.arena,
.timer = undefined,
};
ctx.timer.data = ctx;
_ = c.uv_timer_init(self.loop, &ctx.timer);
_ = c.uv_timer_start(&ctx.timer, sleep_cb, @intCast(ms), 0);
},
}
}
};
// ---------------------------------------------------------------------------
// Async file read
// ---------------------------------------------------------------------------
const FileReadCtx = struct {
scheduler: *Scheduler,
task: *Task,
arena: *Arena,
gpa: std.mem.Allocator,
fd: c_int,
buf: std.ArrayList(u8),
path: []const u8,
req: c.uv_fs_t,
read_buf: ?[]u8,
};
fn mapUvErr(uv_err: c_int) u64 {
return switch (uv_err) {
c.UV_ENOENT => ERR_DOES_NOT_EXIST,
c.UV_EACCES => ERR_PERMISSION,
c.UV_EEXIST => ERR_ALREADY_EXISTS,
else => ERR_IO_OTHER,
};
}
fn file_open_cb(req: [*c]c.uv_fs_t) callconv(.c) void {
const ctx = @as(*FileReadCtx, @ptrCast(@alignCast(req.*.data)));
const result = req.*.result;
c.uv_fs_req_cleanup(req);
if (result < 0) {
const err = makeErrResult(ctx.arena, mapUvErr(@intCast(-result))) catch {
ctx.gpa.destroy(ctx);
return;
};
if (ctx.task.finishValue(ctx.arena, err) catch false) {
ctx.scheduler.completeTask(ctx.task) catch {};
} else {
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
}
ctx.buf.deinit(ctx.gpa);
ctx.gpa.free(ctx.path);
ctx.gpa.destroy(ctx);
return;
}
ctx.fd = @intCast(result);
const read_buf = ctx.gpa.alloc(u8, 4096) catch unreachable;
ctx.read_buf = read_buf;
var uv_buf = c.uv_buf_init(@ptrCast(read_buf.ptr), @intCast(read_buf.len));
_ = c.uv_fs_read(ctx.scheduler.loop, req, ctx.fd, &uv_buf, 1, -1, file_read_cb);
}
fn file_read_cb(req: [*c]c.uv_fs_t) callconv(.c) void {
const ctx = @as(*FileReadCtx, @ptrCast(@alignCast(req.*.data)));
const nread = req.*.result;
c.uv_fs_req_cleanup(req);
if (nread < 0) {
_ = c.uv_fs_close(ctx.scheduler.loop, req, ctx.fd, null);
const err = makeErrResult(ctx.arena, mapUvErr(@intCast(-nread))) catch {
ctx.gpa.destroy(ctx);
return;
};
if (ctx.task.finishValue(ctx.arena, err) catch false) {
ctx.scheduler.completeTask(ctx.task) catch {};
} else {
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
}
if (ctx.read_buf) |b| ctx.gpa.free(b);
ctx.buf.deinit(ctx.gpa);
ctx.gpa.free(ctx.path);
ctx.gpa.destroy(ctx);
return;
}
if (nread == 0) {
// EOF
_ = c.uv_fs_close(ctx.scheduler.loop, req, ctx.fd, null);
const bytes_tree = codecs.ofBytes(ctx.arena, ctx.buf.items) catch {
ctx.gpa.destroy(ctx);
return;
};
const ok = makeOkResult(ctx.arena, bytes_tree) catch {
ctx.gpa.destroy(ctx);
return;
};
if (ctx.task.finishValue(ctx.arena, ok) catch false) {
ctx.scheduler.completeTask(ctx.task) catch {};
} else {
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
}
if (ctx.read_buf) |b| ctx.gpa.free(b);
ctx.buf.deinit(ctx.gpa);
ctx.gpa.free(ctx.path);
ctx.gpa.destroy(ctx);
return;
}
const data = ctx.read_buf.?[0..@intCast(nread)];
ctx.buf.appendSlice(ctx.gpa, data) catch unreachable;
const read_buf = ctx.gpa.alloc(u8, 4096) catch unreachable;
ctx.read_buf = read_buf;
var uv_buf = c.uv_buf_init(@ptrCast(read_buf.ptr), @intCast(read_buf.len));
_ = c.uv_fs_read(ctx.scheduler.loop, req, ctx.fd, &uv_buf, 1, -1, file_read_cb);
}
// ---------------------------------------------------------------------------
// Async file write
// ---------------------------------------------------------------------------
const FileWriteCtx = struct {
scheduler: *Scheduler,
task: *Task,
arena: *Arena,
gpa: std.mem.Allocator,
fd: c_int,
path: []const u8,
contents: []const u8,
written: bool,
req: c.uv_fs_t,
};
fn file_write_open_cb(req: [*c]c.uv_fs_t) callconv(.c) void {
const ctx = @as(*FileWriteCtx, @ptrCast(@alignCast(req.*.data)));
const result = req.*.result;
c.uv_fs_req_cleanup(req);
if (result < 0) {
const err = makeErrResult(ctx.arena, mapUvErr(@intCast(-result))) catch {
ctx.gpa.destroy(ctx);
return;
};
if (ctx.task.finishValue(ctx.arena, err) catch false) {
ctx.scheduler.completeTask(ctx.task) catch {};
} else {
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
}
ctx.gpa.free(ctx.path);
ctx.gpa.free(ctx.contents);
ctx.gpa.destroy(ctx);
return;
}
ctx.fd = @intCast(result);
var uv_buf = c.uv_buf_init(@ptrCast(@constCast(ctx.contents.ptr)), @intCast(ctx.contents.len));
_ = c.uv_fs_write(ctx.scheduler.loop, req, ctx.fd, &uv_buf, 1, 0, file_write_cb);
}
fn file_write_cb(req: [*c]c.uv_fs_t) callconv(.c) void {
const ctx = @as(*FileWriteCtx, @ptrCast(@alignCast(req.*.data)));
const nwrite = req.*.result;
c.uv_fs_req_cleanup(req);
if (nwrite < 0) {
_ = c.uv_fs_close(ctx.scheduler.loop, req, ctx.fd, null);
const err = makeErrResult(ctx.arena, mapUvErr(@intCast(-nwrite))) catch {
ctx.gpa.destroy(ctx);
return;
};
if (ctx.task.finishValue(ctx.arena, err) catch false) {
ctx.scheduler.completeTask(ctx.task) catch {};
} else {
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
}
ctx.gpa.free(ctx.path);
ctx.gpa.free(ctx.contents);
ctx.gpa.destroy(ctx);
return;
}
_ = c.uv_fs_close(ctx.scheduler.loop, req, ctx.fd, file_write_close_cb);
}
fn file_write_close_cb(req: [*c]c.uv_fs_t) callconv(.c) void {
const ctx = @as(*FileWriteCtx, @ptrCast(@alignCast(req.*.data)));
c.uv_fs_req_cleanup(req);
const leaf = ctx.arena.alloc(.leaf) catch {
ctx.gpa.destroy(ctx);
return;
};
const ok = makeOkResult(ctx.arena, leaf) catch {
ctx.gpa.destroy(ctx);
return;
};
if (ctx.task.finishValue(ctx.arena, ok) catch false) {
ctx.scheduler.completeTask(ctx.task) catch {};
} else {
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
}
ctx.gpa.free(ctx.path);
ctx.gpa.free(ctx.contents);
ctx.gpa.destroy(ctx);
}
// ---------------------------------------------------------------------------
// Async sleep
// ---------------------------------------------------------------------------
const SleepCtx = struct {
scheduler: *Scheduler,
task: *Task,
arena: *Arena,
timer: c.uv_timer_t,
};
fn sleep_cb(handle: [*c]c.uv_timer_t) callconv(.c) void {
const ctx = @as(*SleepCtx, @ptrCast(@alignCast(handle.*.data)));
defer ctx.scheduler.gpa.destroy(ctx);
const leaf = ctx.arena.alloc(.leaf) catch {
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
return;
};
if (ctx.task.finishValue(ctx.arena, leaf) catch false) {
ctx.scheduler.completeTask(ctx.task) catch {};
} else {
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
}
}
// ---------------------------------------------------------------------------
// Public entry point
// ---------------------------------------------------------------------------
pub fn runIO(gpa: std.mem.Allocator, arena: *Arena, program: u32, perms: IOPerms) !u32 {
const action_tree = try isIOSentinel(arena, program) orelse {
return error.InvalidIOSentinel;
};
var loop: c.uv_loop_t = undefined;
const rc = c.uv_loop_init(&loop);
if (rc != 0) return error.LoopInitFailed;
defer _ = c.uv_loop_close(&loop);
var scheduler = try Scheduler.init(gpa, &loop, arena, perms);
defer scheduler.deinit();
const main_task = try scheduler.createTask(null, try arena.alloc(.leaf), try arena.alloc(.leaf), action_tree);
try scheduler.runnable.append(gpa, main_task);
try scheduler.run();
// Return the main task's result
return main_task.result orelse program;
}

View File

@@ -5,6 +5,47 @@ const reduce = @import("reduce.zig");
const codecs = @import("codecs.zig"); const codecs = @import("codecs.zig");
const kernel = @import("kernel.zig"); const kernel = @import("kernel.zig");
const bundle = @import("bundle.zig"); const bundle = @import("bundle.zig");
const io_driver = @import("io_driver.zig");
fn printNode(arena: *Arena, tag: u64, node: u32, io: std.Io) !void {
var stdout_buf: [4096]u8 = undefined;
var stdout = std.Io.File.stdout().writer(io, &stdout_buf);
switch (tag) {
codecs.HOST_STRING_TAG => {
const s = try codecs.toString(arena, node) orelse {
try stdout.interface.writeAll("Error: failed to decode string result\n");
try stdout.flush();
return error.DecodeFailed;
};
defer arena.allocator.free(s);
try stdout.interface.writeAll(s);
try stdout.interface.writeAll("\n");
},
codecs.HOST_NUMBER_TAG => {
const n = try codecs.toNumber(arena, node) orelse 0;
try stdout.interface.print("{d}\n", .{n});
},
codecs.HOST_BOOL_TAG => {
const b = try codecs.toBool(arena, node) orelse {
try stdout.interface.writeAll("Error: failed to decode bool result\n");
try stdout.flush();
return error.DecodeFailed;
};
try stdout.interface.writeAll(if (b) "true\n" else "false\n");
},
codecs.HOST_TREE_TAG => {
try tree.formatTree(&stdout.interface, arena, node, 0);
try stdout.interface.writeAll("\n");
},
else => {
try stdout.interface.print("(tag={d}, payload=", .{tag});
try tree.formatTree(&stdout.interface, arena, node, 0);
try stdout.interface.writeAll(")\n");
},
}
try stdout.flush();
}
fn runNative(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []const []const u8, fuel: u64, io: std.Io) !void { fn runNative(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []const []const u8, fuel: u64, io: std.Io) !void {
const term = try bundle.loadBundleDefaultRoot(arena, bundle_bytes); const term = try bundle.loadBundleDefaultRoot(arena, bundle_bytes);
@@ -16,44 +57,29 @@ fn runNative(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []cons
} }
const result = try reduce.reduce(current, arena, fuel); const result = try reduce.reduce(current, arena, fuel);
try printNode(arena, tag, result, io);
var stdout_buf: [4096]u8 = undefined;
var stdout = std.Io.File.stdout().writer(io, &stdout_buf);
switch (tag) {
codecs.HOST_STRING_TAG => {
const s = try codecs.toString(arena, result) orelse {
try stdout.interface.writeAll("Error: failed to decode string result\n");
try stdout.flush();
return error.DecodeFailed;
};
defer arena.allocator.free(s);
try stdout.interface.writeAll(s);
try stdout.interface.writeAll("\n");
},
codecs.HOST_NUMBER_TAG => {
const n = try codecs.toNumber(arena, result) orelse 0;
try stdout.interface.print("{d}\n", .{n});
},
codecs.HOST_BOOL_TAG => {
const b = try codecs.toBool(arena, result) orelse {
try stdout.interface.writeAll("Error: failed to decode bool result\n");
try stdout.flush();
return error.DecodeFailed;
};
try stdout.interface.writeAll(if (b) "true\n" else "false\n");
},
codecs.HOST_TREE_TAG => {
try tree.formatTree(&stdout.interface, arena, result, 0);
try stdout.interface.writeAll("\n");
},
else => {
try stdout.interface.print("(tag={d}, payload=", .{tag});
try tree.formatTree(&stdout.interface, arena, result, 0);
try stdout.interface.writeAll(")\n");
},
} }
try stdout.flush();
fn runIO(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []const []const u8, fuel: u64, perms: io_driver.IOPerms, io: std.Io) !void {
const term = try bundle.loadBundleDefaultRoot(arena, bundle_bytes);
var current = term;
for (args_raw) |arg| {
const arg_tree = try parseArg(arena, io, arg);
current = try arena.alloc(.{ .app = .{ .func = current, .arg = arg_tree } });
}
const reduced = try reduce.reduce(current, arena, fuel);
if (try io_driver.isIOSentinel(arena, reduced) == null) {
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
try stderr.interface.writeAll("Error: reduced term is not a valid IO program\n");
try stderr.flush();
std.process.exit(1);
}
const result = try io_driver.runIO(arena.allocator, arena, reduced, perms);
try printNode(arena, tag, result, io);
} }
fn runBundle(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []const []const u8, fuel: u64, io: std.Io) !void { fn runBundle(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []const []const u8, fuel: u64, io: std.Io) !void {
@@ -98,43 +124,7 @@ fn runBundle(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []cons
return error.InvalidHostValue; return error.InvalidHostValue;
}; };
var stdout_buf: [4096]u8 = undefined; try printNode(arena, hv.tag, hv.payload, io);
var stdout = std.Io.File.stdout().writer(io, &stdout_buf);
switch (hv.tag) {
codecs.HOST_STRING_TAG => {
const s = try codecs.toString(arena, hv.payload) orelse {
try stdout.interface.writeAll("Error: failed to decode string payload\n");
try stdout.flush();
return error.DecodeFailed;
};
defer arena.allocator.free(s);
try stdout.interface.writeAll(s);
try stdout.interface.writeAll("\n");
},
codecs.HOST_NUMBER_TAG => {
const n = try codecs.toNumber(arena, hv.payload) orelse 0;
try stdout.interface.print("{d}\n", .{n});
},
codecs.HOST_BOOL_TAG => {
const b = try codecs.toBool(arena, hv.payload) orelse {
try stdout.interface.writeAll("Error: failed to decode bool payload\n");
try stdout.flush();
return error.DecodeFailed;
};
try stdout.interface.writeAll(if (b) "true\n" else "false\n");
},
codecs.HOST_TREE_TAG => {
try tree.formatTree(&stdout.interface, arena, hv.payload, 0);
try stdout.interface.writeAll("\n");
},
else => {
try stdout.interface.print("(tag={d}, payload=", .{hv.tag});
try tree.formatTree(&stdout.interface, arena, hv.payload, 0);
try stdout.interface.writeAll(")\n");
},
}
try stdout.flush();
} }
fn parseArg(arena: *Arena, io: std.Io, s: []const u8) !u32 { fn parseArg(arena: *Arena, io: std.Io, s: []const u8) !u32 {
@@ -162,7 +152,7 @@ pub fn main(init: std.process.Init) !void {
const args = try init.minimal.args.toSlice(init.arena.allocator()); const args = try init.minimal.args.toSlice(init.arena.allocator());
if (args.len < 2) { if (args.len < 2) {
var stderr = std.Io.File.stderr().writer(io, &[_]u8{}); var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
try stderr.interface.writeAll("Usage: tricu-zig [--type TYPE] [--kernel] [--fuel N] <bundle.arboricx> [arg1 arg2 ...]\n"); try stderr.interface.writeAll("Usage: tricu-zig [--type TYPE] [--kernel] [--io] [--unsafe-io] [--fuel N] <bundle.arboricx> [arg1 arg2 ...]\n");
try stderr.flush(); try stderr.flush();
std.process.exit(1); std.process.exit(1);
} }
@@ -173,6 +163,8 @@ pub fn main(init: std.process.Init) !void {
var arg_start: usize = 2; var arg_start: usize = 2;
var use_kernel = false; var use_kernel = false;
var use_io = false;
var io_perms = io_driver.IOPerms{};
var fuel: u64 = std.math.maxInt(u64); var fuel: u64 = std.math.maxInt(u64);
var i: usize = 1; var i: usize = 1;
@@ -180,7 +172,7 @@ pub fn main(init: std.process.Init) !void {
if (std.mem.eql(u8, args[i], "--type")) { if (std.mem.eql(u8, args[i], "--type")) {
if (i + 1 >= args.len) { if (i + 1 >= args.len) {
var stderr = std.Io.File.stderr().writer(io, &[_]u8{}); var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
try stderr.interface.writeAll("Usage: tricu-zig --type <tree|number|bool|string|list|bytes> [--fuel N] <bundle> [args...]\n"); try stderr.interface.writeAll("Usage: tricu-zig --type <tree|number|bool|string|list|bytes> [--io] [--unsafe-io] [--fuel N] <bundle> [args...]\n");
try stderr.flush(); try stderr.flush();
std.process.exit(1); std.process.exit(1);
} }
@@ -201,10 +193,15 @@ pub fn main(init: std.process.Init) !void {
i += 1; i += 1;
} else if (std.mem.eql(u8, args[i], "--kernel")) { } else if (std.mem.eql(u8, args[i], "--kernel")) {
use_kernel = true; use_kernel = true;
} else if (std.mem.eql(u8, args[i], "--io")) {
use_io = true;
} else if (std.mem.eql(u8, args[i], "--unsafe-io")) {
io_perms.allow_read_all = true;
io_perms.allow_write_all = true;
} else if (std.mem.eql(u8, args[i], "--fuel")) { } else if (std.mem.eql(u8, args[i], "--fuel")) {
if (i + 1 >= args.len) { if (i + 1 >= args.len) {
var stderr = std.Io.File.stderr().writer(io, &[_]u8{}); var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
try stderr.interface.writeAll("Usage: tricu-zig --fuel <N> <bundle> [args...]\n"); try stderr.interface.writeAll("Usage: tricu-zig --fuel <N> [--io] [--unsafe-io] <bundle> [args...]\n");
try stderr.flush(); try stderr.flush();
std.process.exit(1); std.process.exit(1);
} }
@@ -225,7 +222,7 @@ pub fn main(init: std.process.Init) !void {
if (bundle_idx >= args.len) { if (bundle_idx >= args.len) {
var stderr = std.Io.File.stderr().writer(io, &[_]u8{}); var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
try stderr.interface.writeAll("Usage: tricu-zig [--type TYPE] [--kernel] [--fuel N] <bundle.arboricx> [arg1 arg2 ...]\n"); try stderr.interface.writeAll("Usage: tricu-zig [--type TYPE] [--kernel] [--io] [--unsafe-io] [--fuel N] <bundle.arboricx> [arg1 arg2 ...]\n");
try stderr.flush(); try stderr.flush();
std.process.exit(1); std.process.exit(1);
} }
@@ -239,7 +236,14 @@ pub fn main(init: std.process.Init) !void {
const call_args = if (arg_start < args.len) args[arg_start..] else &[_][]const u8{}; const call_args = if (arg_start < args.len) args[arg_start..] else &[_][]const u8{};
if (use_kernel) { if (use_io) {
runIO(&arena, tag, bundle_bytes, call_args, fuel, io_perms, io) catch |err| {
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
try stderr.interface.print("Execution failed: {s}\n", .{@errorName(err)});
try stderr.flush();
std.process.exit(1);
};
} else if (use_kernel) {
runBundle(&arena, tag, bundle_bytes, call_args, fuel, io) catch |err| { runBundle(&arena, tag, bundle_bytes, call_args, fuel, io) catch |err| {
var stderr = std.Io.File.stderr().writer(io, &[_]u8{}); var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
try stderr.interface.print("Execution failed: {s}\n", .{@errorName(err)}); try stderr.interface.print("Execution failed: {s}\n", .{@errorName(err)});

View File

@@ -51,6 +51,68 @@ int main(void) {
} }
printf("PASS: kernel loaded (root=%u)\n", kernel_root); printf("PASS: kernel loaded (root=%u)\n", kernel_root);
/* Test: tree inspection primitives */
uint32_t l = arb_leaf(ctx);
uint32_t s = arb_stem(ctx, l);
uint32_t f = arb_fork(ctx, s, l);
uint32_t a = arb_app(ctx, f, s);
if (!arb_is_leaf(ctx, l)) {
fprintf(stderr, "FAIL: is_leaf on leaf\n");
arboricx_free(ctx);
return 1;
}
if (arb_is_leaf(ctx, s)) {
fprintf(stderr, "FAIL: is_leaf on stem should be false\n");
arboricx_free(ctx);
return 1;
}
if (!arb_is_stem(ctx, s)) {
fprintf(stderr, "FAIL: is_stem on stem\n");
arboricx_free(ctx);
return 1;
}
if (!arb_is_fork(ctx, f)) {
fprintf(stderr, "FAIL: is_fork on fork\n");
arboricx_free(ctx);
return 1;
}
if (!arb_is_app(ctx, a)) {
fprintf(stderr, "FAIL: is_app on app\n");
arboricx_free(ctx);
return 1;
}
uint32_t child;
if (!arb_get_stem_child(ctx, s, &child) || child != l) {
fprintf(stderr, "FAIL: get_stem_child\n");
arboricx_free(ctx);
return 1;
}
uint32_t left, right;
if (!arb_get_fork_children(ctx, f, &left, &right) || left != s || right != l) {
fprintf(stderr, "FAIL: get_fork_children\n");
arboricx_free(ctx);
return 1;
}
uint32_t func, arg;
if (!arb_get_app_func_arg(ctx, a, &func, &arg) || func != f || arg != s) {
fprintf(stderr, "FAIL: get_app_func_arg\n");
arboricx_free(ctx);
return 1;
}
/* Invalid index should return 0 */
if (arb_is_leaf(ctx, 999999)) {
fprintf(stderr, "FAIL: is_leaf on invalid index should be false\n");
arboricx_free(ctx);
return 1;
}
printf("PASS: tree inspection primitives\n");
arboricx_free(ctx); arboricx_free(ctx);
printf("\nAll C ABI tests passed.\n"); printf("\nAll C ABI tests passed.\n");
return 0; return 0;

View File

@@ -0,0 +1,223 @@
#include <stdio.h>
#include <string.h>
#include "arboricx.h"
int main(void) {
arb_ctx_t* ctx = arboricx_init();
if (!ctx) {
fprintf(stderr, "Failed to initialize Arboricx context\n");
return 1;
}
/* Test: construct and verify pure action = Fork 0 Leaf */
uint32_t leaf = arb_leaf(ctx);
uint32_t zero = arb_of_number(ctx, 0);
uint32_t pure_action = arb_fork(ctx, zero, leaf);
if (!arb_is_fork(ctx, pure_action)) {
fprintf(stderr, "FAIL: pure action should be fork\n");
arboricx_free(ctx);
return 1;
}
uint32_t tag, payload;
if (!arb_get_fork_children(ctx, pure_action, &tag, &payload) ||
tag != zero || payload != leaf) {
fprintf(stderr, "FAIL: pure action children mismatch\n");
arboricx_free(ctx);
return 1;
}
uint64_t tag_num;
if (!arb_to_number(ctx, tag, &tag_num) || tag_num != 0) {
fprintf(stderr, "FAIL: pure action tag should be 0\n");
arboricx_free(ctx);
return 1;
}
printf("PASS: pure action shape\n");
/* Test: construct and verify bind action = Fork 1 (Fork left k) */
uint32_t one = arb_of_number(ctx, 1);
uint32_t left = arb_fork(ctx, zero, leaf); /* pure Leaf */
uint32_t k = arb_fork(ctx, leaf, leaf); /* identity as Fork Leaf Leaf */
uint32_t bind_pair = arb_fork(ctx, left, k);
uint32_t bind_action = arb_fork(ctx, one, bind_pair);
if (!arb_get_fork_children(ctx, bind_action, &tag, &payload) ||
!arb_to_number(ctx, tag, &tag_num) || tag_num != 1) {
fprintf(stderr, "FAIL: bind action tag should be 1\n");
arboricx_free(ctx);
return 1;
}
uint32_t bind_left, bind_k;
if (!arb_get_fork_children(ctx, payload, &bind_left, &bind_k) ||
bind_left != left || bind_k != k) {
fprintf(stderr, "FAIL: bind payload should be Fork left k\n");
arboricx_free(ctx);
return 1;
}
printf("PASS: bind action shape\n");
/* Test: construct and verify IO sentinel = Fork "tricuIO" (Fork 1 action) */
uint32_t sentinel_str = arb_of_string(ctx, "tricuIO");
uint32_t version = arb_of_number(ctx, 1);
uint32_t version_action_pair = arb_fork(ctx, version, pure_action);
uint32_t io_sentinel = arb_fork(ctx, sentinel_str, version_action_pair);
if (!arb_is_fork(ctx, io_sentinel)) {
fprintf(stderr, "FAIL: IO sentinel should be fork\n");
arboricx_free(ctx);
return 1;
}
uint32_t sent_left, sent_right;
if (!arb_get_fork_children(ctx, io_sentinel, &sent_left, &sent_right)) {
fprintf(stderr, "FAIL: get_fork_children on IO sentinel\n");
arboricx_free(ctx);
return 1;
}
/* Verify sentinel string */
uint8_t* decoded_sentinel;
size_t decoded_len;
if (!arb_to_string(ctx, sent_left, &decoded_sentinel, &decoded_len) ||
decoded_len != 7 || memcmp(decoded_sentinel, "tricuIO", 7) != 0) {
fprintf(stderr, "FAIL: IO sentinel string mismatch\n");
arboricx_free(ctx);
return 1;
}
arboricx_free_buf(ctx, decoded_sentinel, decoded_len);
/* Verify version = 1 and action = pure */
uint32_t ver, act;
if (!arb_get_fork_children(ctx, sent_right, &ver, &act) ||
!arb_to_number(ctx, ver, &tag_num) || tag_num != 1 ||
act != pure_action) {
fprintf(stderr, "FAIL: IO sentinel version/action mismatch\n");
arboricx_free(ctx);
return 1;
}
printf("PASS: IO sentinel shape\n");
/* Test: putStr action = Fork 10 string */
uint32_t ten = arb_of_number(ctx, 10);
uint32_t msg = arb_of_string(ctx, "hello");
uint32_t putStr_action = arb_fork(ctx, ten, msg);
if (!arb_get_fork_children(ctx, putStr_action, &tag, &payload) ||
!arb_to_number(ctx, tag, &tag_num) || tag_num != 10) {
fprintf(stderr, "FAIL: putStr tag should be 10\n");
arboricx_free(ctx);
return 1;
}
printf("PASS: putStr action shape\n");
/* Test: getLine action = Fork 11 Leaf */
uint32_t eleven = arb_of_number(ctx, 11);
uint32_t getLine_action = arb_fork(ctx, eleven, leaf);
if (!arb_get_fork_children(ctx, getLine_action, &tag, &payload) ||
!arb_to_number(ctx, tag, &tag_num) || tag_num != 11 ||
payload != leaf) {
fprintf(stderr, "FAIL: getLine tag should be 11 with Leaf payload\n");
arboricx_free(ctx);
return 1;
}
printf("PASS: getLine action shape\n");
/* Test: readFile action = Fork 20 path */
uint32_t twenty = arb_of_number(ctx, 20);
uint32_t path = arb_of_string(ctx, "/tmp/test.txt");
uint32_t readFile_action = arb_fork(ctx, twenty, path);
if (!arb_get_fork_children(ctx, readFile_action, &tag, &payload) ||
!arb_to_number(ctx, tag, &tag_num) || tag_num != 20) {
fprintf(stderr, "FAIL: readFile tag should be 20\n");
arboricx_free(ctx);
return 1;
}
printf("PASS: readFile action shape\n");
/* Test: writeFile action = Fork 21 (Fork path contents) */
uint32_t twenty_one = arb_of_number(ctx, 21);
uint32_t contents = arb_of_string(ctx, "data");
uint32_t write_pair = arb_fork(ctx, path, contents);
uint32_t writeFile_action = arb_fork(ctx, twenty_one, write_pair);
if (!arb_get_fork_children(ctx, writeFile_action, &tag, &payload) ||
!arb_to_number(ctx, tag, &tag_num) || tag_num != 21) {
fprintf(stderr, "FAIL: writeFile tag should be 21\n");
arboricx_free(ctx);
return 1;
}
uint32_t wf_path, wf_contents;
if (!arb_get_fork_children(ctx, payload, &wf_path, &wf_contents) ||
wf_path != path || wf_contents != contents) {
fprintf(stderr, "FAIL: writeFile payload should be Fork path contents\n");
arboricx_free(ctx);
return 1;
}
printf("PASS: writeFile action shape\n");
/* Test: ok result = Fork (Stem Leaf) (Fork val Leaf) */
uint32_t stem_leaf = arb_stem(ctx, leaf);
uint32_t val_pair = arb_fork(ctx, msg, leaf);
uint32_t ok_result = arb_fork(ctx, stem_leaf, val_pair);
if (!arb_is_fork(ctx, ok_result)) {
fprintf(stderr, "FAIL: ok result should be fork\n");
arboricx_free(ctx);
return 1;
}
uint32_t ok_tag, ok_rest;
if (!arb_get_fork_children(ctx, ok_result, &ok_tag, &ok_rest) ||
!arb_is_stem(ctx, ok_tag)) {
fprintf(stderr, "FAIL: ok result left should be stem\n");
arboricx_free(ctx);
return 1;
}
uint32_t ok_val, ok_leaf;
if (!arb_get_fork_children(ctx, ok_rest, &ok_val, &ok_leaf) ||
ok_val != msg || ok_leaf != leaf) {
fprintf(stderr, "FAIL: ok result right should be Fork val Leaf\n");
arboricx_free(ctx);
return 1;
}
printf("PASS: ok result shape\n");
/* Test: err result = Fork Leaf (Fork code Leaf) */
uint32_t err_code = arb_of_number(ctx, 42);
uint32_t err_pair = arb_fork(ctx, err_code, leaf);
uint32_t err_result = arb_fork(ctx, leaf, err_pair);
if (!arb_is_fork(ctx, err_result)) {
fprintf(stderr, "FAIL: err result should be fork\n");
arboricx_free(ctx);
return 1;
}
uint32_t err_tag, err_rest;
if (!arb_get_fork_children(ctx, err_result, &err_tag, &err_rest) ||
!arb_is_leaf(ctx, err_tag)) {
fprintf(stderr, "FAIL: err result left should be leaf\n");
arboricx_free(ctx);
return 1;
}
uint32_t err_c, err_l;
if (!arb_get_fork_children(ctx, err_rest, &err_c, &err_l) ||
err_c != err_code || err_l != leaf) {
fprintf(stderr, "FAIL: err result right should be Fork code Leaf\n");
arboricx_free(ctx);
return 1;
}
printf("PASS: err result shape\n");
arboricx_free(ctx);
printf("\nAll IO protocol tests passed.\n");
return 0;
}

217
ext/zig/tests/io_run_test.c Normal file
View File

@@ -0,0 +1,217 @@
#include <stdio.h>
#include <string.h>
#include "arboricx.h"
static uint32_t make_pure(arb_ctx_t* ctx, uint32_t val) {
uint32_t zero = arb_of_number(ctx, 0);
return arb_fork(ctx, zero, val);
}
static uint32_t make_io_sentinel(arb_ctx_t* ctx, uint32_t action) {
uint32_t sentinel = arb_of_string(ctx, "tricuIO");
uint32_t version = arb_of_number(ctx, 1);
uint32_t version_action = arb_fork(ctx, version, action);
return arb_fork(ctx, sentinel, version_action);
}
int main(void) {
arb_ctx_t* ctx = arboricx_init();
if (!ctx) {
fprintf(stderr, "Failed to initialize Arboricx context\n");
return 1;
}
arb_io_perms_t perms = { 0, 0 };
/* Test 1: pure "hello" wrapped in IO sentinel */
{
uint32_t hello = arb_of_string(ctx, "hello");
uint32_t pure_hello = make_pure(ctx, hello);
uint32_t program = make_io_sentinel(ctx, pure_hello);
uint32_t result = arb_run_io(ctx, program, &perms);
if (result == 0) {
fprintf(stderr, "FAIL: pure hello returned 0\n");
arboricx_free(ctx);
return 1;
}
uint8_t* decoded;
size_t decoded_len;
if (!arb_to_string(ctx, result, &decoded, &decoded_len) ||
decoded_len != 5 || memcmp(decoded, "hello", 5) != 0) {
fprintf(stderr, "FAIL: pure hello result mismatch\n");
arboricx_free(ctx);
return 1;
}
arboricx_free_buf(ctx, decoded, decoded_len);
printf("PASS: pure hello\n");
}
/* Test 2: bind (pure "a") (\_ : pure "done") */
{
uint32_t a = arb_of_string(ctx, "a");
uint32_t done = arb_of_string(ctx, "done");
uint32_t pure_a = make_pure(ctx, a);
uint32_t pure_done = make_pure(ctx, done);
/* K pure_done = Fork Leaf pure_done */
uint32_t k = arb_fork(ctx, arb_leaf(ctx), pure_done);
uint32_t bind_pair = arb_fork(ctx, pure_a, k);
uint32_t one = arb_of_number(ctx, 1);
uint32_t bind_action = arb_fork(ctx, one, bind_pair);
uint32_t program = make_io_sentinel(ctx, bind_action);
uint32_t result = arb_run_io(ctx, program, &perms);
if (result == 0) {
fprintf(stderr, "FAIL: bind returned 0\n");
arboricx_free(ctx);
return 1;
}
uint8_t* decoded;
size_t decoded_len;
if (!arb_to_string(ctx, result, &decoded, &decoded_len) ||
decoded_len != 4 || memcmp(decoded, "done", 4) != 0) {
fprintf(stderr, "FAIL: bind result mismatch\n");
arboricx_free(ctx);
return 1;
}
arboricx_free_buf(ctx, decoded, decoded_len);
printf("PASS: bind pure\n");
}
/* Test 3: putStr "test" (no permissions needed) */
{
uint32_t test = arb_of_string(ctx, "test");
uint32_t ten = arb_of_number(ctx, 10);
uint32_t putStr_action = arb_fork(ctx, ten, test);
uint32_t program = make_io_sentinel(ctx, putStr_action);
printf("EXPECT: test\n");
uint32_t result = arb_run_io(ctx, program, &perms);
if (result == 0) {
fprintf(stderr, "FAIL: putStr returned 0\n");
arboricx_free(ctx);
return 1;
}
if (!arb_is_leaf(ctx, result)) {
fprintf(stderr, "FAIL: putStr should return Leaf\n");
arboricx_free(ctx);
return 1;
}
printf("PASS: putStr\n");
}
/* Test 4: readFile without permission returns err */
{
uint32_t path = arb_of_string(ctx, "/etc/passwd");
uint32_t twenty = arb_of_number(ctx, 20);
uint32_t readFile_action = arb_fork(ctx, twenty, path);
uint32_t program = make_io_sentinel(ctx, readFile_action);
uint32_t result = arb_run_io(ctx, program, &perms);
if (result == 0) {
fprintf(stderr, "FAIL: readFile denied returned 0\n");
arboricx_free(ctx);
return 1;
}
/* Should be an err result: Fork Leaf (Fork code Leaf) */
uint32_t left, right;
if (!arb_get_fork_children(ctx, result, &left, &right) ||
!arb_is_leaf(ctx, left)) {
fprintf(stderr, "FAIL: readFile denied should be err result\n");
arboricx_free(ctx);
return 1;
}
uint32_t code, rest;
if (!arb_get_fork_children(ctx, right, &code, &rest) ||
!arb_is_leaf(ctx, rest)) {
fprintf(stderr, "FAIL: readFile denied err shape mismatch\n");
arboricx_free(ctx);
return 1;
}
uint64_t code_num;
if (!arb_to_number(ctx, code, &code_num) || code_num != 20) {
fprintf(stderr, "FAIL: readFile denied code should be 20, got %llu\n",
(unsigned long long)code_num);
arboricx_free(ctx);
return 1;
}
printf("PASS: readFile denied\n");
}
/* Test 5: readFile with permission succeeds */
{
/* Create a temp file first */
const char* tmp = "/tmp/tricu_io_test.txt";
FILE* f = fopen(tmp, "w");
if (!f) {
fprintf(stderr, "FAIL: could not create temp file\n");
arboricx_free(ctx);
return 1;
}
fprintf(f, "hi");
fclose(f);
arb_io_perms_t unsafe_perms = { 1, 0 };
uint32_t path = arb_of_string(ctx, tmp);
uint32_t twenty = arb_of_number(ctx, 20);
uint32_t readFile_action = arb_fork(ctx, twenty, path);
uint32_t program = make_io_sentinel(ctx, readFile_action);
uint32_t result = arb_run_io(ctx, program, &unsafe_perms);
if (result == 0) {
fprintf(stderr, "FAIL: readFile allowed returned 0\n");
arboricx_free(ctx);
return 1;
}
/* Should be ok result: Fork (Stem Leaf) (Fork val Leaf) */
uint32_t ok_tag, ok_rest;
if (!arb_get_fork_children(ctx, result, &ok_tag, &ok_rest) ||
!arb_is_stem(ctx, ok_tag)) {
fprintf(stderr, "FAIL: readFile allowed should be ok result\n");
arboricx_free(ctx);
return 1;
}
uint32_t val, leaf;
if (!arb_get_fork_children(ctx, ok_rest, &val, &leaf) ||
!arb_is_leaf(ctx, leaf)) {
fprintf(stderr, "FAIL: readFile allowed ok shape mismatch\n");
arboricx_free(ctx);
return 1;
}
uint8_t* decoded;
size_t decoded_len;
if (!arb_to_string(ctx, val, &decoded, &decoded_len) ||
decoded_len != 2 || memcmp(decoded, "hi", 2) != 0) {
fprintf(stderr, "FAIL: readFile allowed contents mismatch\n");
arboricx_free(ctx);
return 1;
}
arboricx_free_buf(ctx, decoded, decoded_len);
printf("PASS: readFile allowed\n");
}
/* Test 6: invalid sentinel returns 0 */
{
uint32_t bad = arb_fork(ctx, arb_leaf(ctx), arb_leaf(ctx));
uint32_t result = arb_run_io(ctx, bad, &perms);
if (result != 0) {
fprintf(stderr, "FAIL: invalid sentinel should return 0\n");
arboricx_free(ctx);
return 1;
}
printf("PASS: invalid sentinel\n");
}
arboricx_free(ctx);
printf("\nAll IO run tests passed.\n");
return 0;
}

View File

@@ -26,6 +26,18 @@
haskellPackages.callCabal2nix packageName self {} haskellPackages.callCabal2nix packageName self {}
); );
tricuBench =
hsLib.overrideCabal
(hsLib.doBenchmark (
haskellPackages.callCabal2nix packageName self {}
))
(oldAttrs: {
postInstall = (oldAttrs.postInstall or "") + ''
mkdir -p $out/bin
cp dist/build/tricu-bench/tricu-bench $out/bin/
'';
});
customGHC = haskellPackages.ghcWithPackages (hpkgs: with hpkgs; [ customGHC = haskellPackages.ghcWithPackages (hpkgs: with hpkgs; [
megaparsec megaparsec
]); ]);
@@ -37,7 +49,8 @@
pname = "tricu-zig"; pname = "tricu-zig";
version = "0.1.0"; version = "0.1.0";
src = ./ext/zig; src = ./ext/zig;
nativeBuildInputs = [ pkgs.zig ]; nativeBuildInputs = [ pkgs.zig pkgs.pkg-config ];
buildInputs = [ pkgs.libuv ];
buildPhase = '' buildPhase = ''
export ZIG_GLOBAL_CACHE_DIR=$TMPDIR/zig-cache export ZIG_GLOBAL_CACHE_DIR=$TMPDIR/zig-cache
zig build zig build
@@ -55,6 +68,7 @@
version = "0.1.0"; version = "0.1.0";
src = ./.; src = ./.;
nativeBuildInputs = [ pkgs.gcc pkgs.python3 tricuZig ]; nativeBuildInputs = [ pkgs.gcc pkgs.python3 tricuZig ];
buildInputs = [ pkgs.libuv ];
buildPhase = "true"; buildPhase = "true";
doCheck = true; doCheck = true;
checkPhase = '' checkPhase = ''
@@ -69,6 +83,18 @@
-Wl,-rpath,${tricuZig}/lib -Wl,-rpath,${tricuZig}/lib
/tmp/c_abi_test /tmp/c_abi_test
# IO protocol shape test
gcc -o /tmp/io_protocol_test tests/io_protocol_test.c \
-I ${tricuZig}/include -L ${tricuZig}/lib -larboricx \
-Wl,-rpath,${tricuZig}/lib
/tmp/io_protocol_test
# IO run test (synchronous driver)
gcc -o /tmp/io_run_test tests/io_run_test.c \
-I ${tricuZig}/include -L ${tricuZig}/lib -larboricx \
-Wl,-rpath,${tricuZig}/lib
/tmp/io_run_test
# Kernel path append test # Kernel path append test
gcc -o /tmp/c_abi_append_test tests/c_abi_append_test.c \ gcc -o /tmp/c_abi_append_test tests/c_abi_append_test.c \
-I ${tricuZig}/include -L ${tricuZig}/lib -larboricx \ -I ${tricuZig}/include -L ${tricuZig}/lib -larboricx \
@@ -195,6 +221,7 @@
in { in {
packages.${packageName} = tricuPackage; packages.${packageName} = tricuPackage;
packages.default = tricuPackage; packages.default = tricuPackage;
packages.tricu-bench = tricuBench;
packages.tricu-zig = tricuZig; packages.tricu-zig = tricuZig;
packages.tricu-zig-tests = tricuZigTests; packages.tricu-zig-tests = tricuZigTests;
packages.tricu-php = tricuPhp; packages.tricu-php = tricuPhp;

View File

@@ -8,7 +8,8 @@ module IODriver
, runIOWith , runIOWith
) where ) where
import Research (T(..), apply, toString, toNumber, ofString, ofNumber) import Research (T(..), apply, toString, toNumber, ofString, ofNumber, ofBytes)
import qualified Data.ByteString as BS
import System.IO (putStr, getLine) import System.IO (putStr, getLine)
import qualified System.IO as IO import qualified System.IO as IO
import Control.Exception (try, IOException, SomeException) import Control.Exception (try, IOException, SomeException)
@@ -487,9 +488,9 @@ stepMachine machine =
in path == prefix || prefix' `isPrefixOf` path in path == prefix || prefix' `isPrefixOf` path
tryReadFile path = do tryReadFile path = do
result <- try (IO.readFile path) :: IO (Either IOException String) result <- try (BS.readFile path) :: IO (Either IOException BS.ByteString)
case result of case result of
Right content -> return $ okResult (ofString content) Right content -> return $ okResult (ofBytes content)
Left e -> return $ errResult (ioErrorCode e) Left e -> return $ errResult (ioErrorCode e)
tryWriteFile path contents = do tryWriteFile path contents = do

BIN
test/fixtures/greet-io.aboricx vendored Normal file

Binary file not shown.

BIN
test/fixtures/runArboricxTyped.arboricx vendored Normal file

Binary file not shown.

View File

@@ -78,6 +78,49 @@ executable tricu
Wire Wire
default-language: Haskell2010 default-language: Haskell2010
benchmark tricu-bench
type: exitcode-stdio-1.0
main-is: Bench.hs
hs-source-dirs: bench, src
default-extensions:
LambdaCase
MultiWayIf
OverloadedStrings
ScopedTypeVariables
build-depends:
base >=4.7
, ansi-terminal
, base16-bytestring
, base64-bytestring
, bytestring
, containers
, criterion
, cryptonite
, directory
, exceptions
, filepath
, megaparsec
, memory
, mtl
, sqlite-simple
, text
, time
, transformers
, vector
, zlib
default-language: Haskell2010
other-modules:
ApplyStats
ContentStore
Eval
FileEval
IODriver
Lexer
Parser
Paths_tricu
Research
Wire
test-suite tricu-tests test-suite tricu-tests
type: exitcode-stdio-1.0 type: exitcode-stdio-1.0
main-is: Spec.hs main-is: Spec.hs