Perhaps the first webserver in Tree Calculus? Sure, it's married to a Haskell IO runtime... but we're managing all of the actual webserver semantics in tricu! This includes a demo Arboricx application server that is capable of storing and serving bundles.
14 KiB
AGENTS.md - tricu Project Guide
For AI agents and contributors working in this repository.
1. Build & Test
# Haskell tests (default check)
nix flake check
# Zig build
nix build .#tricu-zig
# Zig tests (separate target — not part of nix flake check)
nix build .#tricu-zig-tests
# Full build
nix build .#
⚠️ Never call cabal directly
Rule of thumb: if it builds, links, or tests, it goes through
nix.
2. Project Overview
tricu (pronounced "tree-shoe") is a programming-language experiment written in Haskell. It implements Triage Calculus, an extension of Barry Jay's Tree Calculus, with lambda-abstraction sugar that gets eliminated back to pure tree calculus terms.
Core types (in src/Research.hs)
| Type | Description |
|---|---|
T = Leaf | Stem T | Fork T T |
Tree Calculus term (the runtime value) |
TricuAST |
Parsed AST with SDef, SApp, SLambda, etc. |
LToken |
Lexer tokens |
Node / MerkleHash |
Content-addressed Merkle DAG nodes |
Source modules (Haskell)
| Module | Purpose |
|---|---|
Main.hs |
CLI entry point (cmdargs), three modes: repl, eval, decode |
Eval.hs |
Interpreter: evalTricu, result, evalSingle |
Parser.hs |
Megaparsec parser → TricuAST |
Lexer.hs |
Megaparsec lexer → LToken |
FileEval.hs |
File loading, module imports, !import |
REPL.hs |
Interactive Read-Eval-Print Loop (haskeline) |
Research.hs |
Core types, apply reduction, booleans, marshalling (ofString, ofNumber), output formatters (toAscii, toTernaryString, decodeResult) |
ContentStore.hs |
SQLite-backed term persistence |
Wire.hs |
Arboricx portable wire format — encode/decode/import/export of Merkle-DAG bundle blobs |
Multi-language Arboricx ecosystem
Arboricx is the portable executable-object format used by tricu. The project now includes native parsing and execution in multiple languages:
| Language | Location | Capabilities |
|---|---|---|
| Haskell | src/Wire.hs, src/Research.hs |
Reference implementation — bundle encode/decode, content store, full Tree Calculus reduction |
| tricu (self-hosted) | kernel_run_arboricx_typed.dag |
A self-hosting Arboricx parser/executor written in tricu itself. Used as a kernel inside the Zig host for maximum portability ("cool but useless" — ~3s for append) |
| Zig | ext/zig/ |
Production host — native bundle parser, WHNF reducer, C ABI (libarboricx.so / .a), CLI (tricu-zig), Python FFI support |
| JavaScript (Node) | ext/js/ |
Native bundle parser, manifest decoder, Merkle DAG verifier, Tree Calculus reducer, CLI runner |
| PHP | ext/php/ |
FFI wrapper around libarboricx.so, CLI runner |
All hosts share the same bundle format and Merkle hashing scheme.
File extensions
.hs- Haskell source.tri- tricu language source (used inlib/,test/,demos/).arboricx- Portable executable bundle.dag- Serialized kernel DAG (used bygen_kernel.zigat build time)
3. Test Suite
Haskell tests
Tests live in test/Spec.hs and use Tasty + HUnit.
nix flake check
Test groups
| Group | What it covers |
|---|---|
lexer |
Megaparsec lexer - identifiers, keywords, strings, escapes, invalid tokens |
parser |
Parser - defs, lambda, applications, lists, comments, parentheses |
simpleEvaluation |
Core apply reduction rules, variable substitution, immutability |
lambdas |
Lambda elimination, SKI calculus, higher-order functions, currying, shadowing, free vars |
providedLibraries |
lib/list.tri - triage, booleans, list ops (head, tail, map, emptyList?, append, equal?) |
fileEval |
Loading .tri files, multi-file context, decode |
modules |
!import, cyclic deps, namespacing, multi-level imports, unresolved vars, local namespaces |
demos |
demos/*.tri - structural equality, toSource, size, level-order traversal |
decoding |
decodeResult - Leaf, numbers, strings, lists, mixed |
elimLambdaSingle |
Lambda elimination: eta reduction, SDef binding, semantics preservation |
stressElimLambda |
Lambda elimination stress test: 200 vars, 800-body curried lambda |
Zig tests
Run separately via:
nix build .#tricu-zig-tests
These are not included in nix flake check. The test derivation compiles and runs:
| Test | What it covers |
|---|---|
c_abi_test.c |
Smoke tests — leaf, stem, fork, app, reduce, number/string roundtrip, kernel root |
c_abi_append_test.c |
Kernel path — append.arboricx with string arguments via Tricu kernel |
native_bundle_append_test.c |
Native fast path — append.arboricx loaded natively, applied, reduced |
native_bundle_id_test.c |
Native fast path — id.arboricx |
native_bundle_bools_test.c |
Native fast path — true.arboricx / false.arboricx |
python_ffi_test.py |
Python ctypes FFI — tests both kernel and native paths for id and append |
4. tricu Language Quick Reference
t → Leaf (the base term)
t t → Stem Leaf
t t t → Fork Leaf Leaf
x = t → Define term x = Leaf
id = (a : a) → Lambda identity (eliminates to tree calculus)
head (map f xs) → From lib/list.tri
!import "./path.tri" NS → Import file under namespace
-- line comment
CRITICAL:
When working with recursion in tricu files:
- Put consumed data first in recursive workers.
- Let data shape drive recursion.
- Do not let counters unroll over abstract input.
5. Output Formats
The eval command accepts --form (shorthand -t):
| Format | Value | Description |
|---|---|---|
tree |
TreeCalculus |
Simple t form (default) |
fsl |
FSL |
Full show representation |
ast |
AST |
Parsed AST representation |
ternary |
Ternary |
Ternary string encoding |
ascii |
Ascii |
ASCII-art tree diagram |
decode |
Decode |
Human-readable (strings, numbers, lists) |
6. Content Addressing
Each T term is content-addressed via a Merkle DAG:
NLeaf → 0x00
NStem(h) → 0x01 || h (32 bytes)
NFork(l,r) → 0x02 || l (32 bytes) || r (32 bytes)
hash = SHA256("arboricx.merkle.node.v1" <> 0x00 <> serialized_node)
This is stored in SQLite via ContentStore.hs. Hash suffixes on identifiers (e.g., foo_abc123...) are validated: 16–64 hex characters (SHA256).
7. Arboricx Portable Bundles (.arboricx)
Portable executable bundles are generated via Wire.hs. See docs/arboricx-bundle-format.md for the full binary format spec.
# Export a bundle from the content store
./result/bin/tricu export -o myterm.arboricx myterm
# Run a bundle (requires TRICU_DB_PATH)
./result/bin/tricu import -f lib/list.tri
TRICU_DB_PATH=/tmp/tricu.db ./result/bin/tricu export -o list_ops.arboricx append
8. Zig Arboricx Host (ext/zig/)
The Zig host is a fast implementation for running Arboricx bundles. It provides a native bundle parser and arena-based evaluator.
Modules
| File | Role |
|---|---|
src/main.zig |
CLI entrypoint — default native path, --kernel fallback |
src/bundle.zig |
Native Arboricx bundle parser — verifies digests, hashes, loads DAG into arena |
src/c_abi.zig |
C FFI exports — arboricx_init, tree constructors, codecs, reduction, bundle loading |
src/reduce.zig |
WHNF reducer (Tree Calculus apply rules) |
src/arena.zig |
Node arena (ArrayListUnmanaged) |
src/tree.zig |
Node union + iterative copyTree |
src/codecs.zig |
Number/string/list/bytes encoding + result unwrapping |
src/kernel.zig |
Embeds DAG kernel into arena (fallback path only) |
src/ternary.zig |
Ternary string parser for Tree Calculus terms |
tools/gen_kernel.zig |
Build-time tool: converts .dag → kernel_embed.zig |
include/arboricx.h |
C header for libarboricx |
C ABI
Key functions:
arb_ctx_t* arboricx_init(void);
uint32_t arb_load_bundle(arb_ctx_t*, const uint8_t* bytes, size_t len, const char* name);
uint32_t arb_load_bundle_default(arb_ctx_t*, const uint8_t* bytes, size_t len);
uint32_t arb_reduce(arb_ctx_t*, uint32_t root, uint64_t fuel);
arb_reduce evaluates in a fresh scratch arena so garbage never accumulates.
Stack size requirement
Tree Calculus reduction is deeply recursive. Assume a segfault is a memory limitation until proven otherwise.
ulimit -s 32768 # 32 MB
Performance comparison
| Fixture | Native path | Kernel path (--kernel) |
|---|---|---|
append "hello " "world" |
~0.007 s | ~3.4 s |
id "hello" |
~0.005 s | ~0.38 s |
The kernel path is kept as a "cool but useless" fallback — the DAG is tiny (~30 KB) so the cost is negligible.
9. Nix Flake Outputs
| Output | Description |
|---|---|
packages.default / packages.tricu |
Haskell tricu package |
packages.tricu-zig |
Zig CLI + libarboricx.a + libarboricx.so + arboricx.h |
packages.tricu-zig-tests |
Separate test target — C ABI + native bundle + Python FFI tests |
packages.tricu-php |
PHP source + libarboricx.so + tricu-php wrapper script |
packages.tricu-php-tests |
Separate test target — PHP FFI tests against fixture bundles |
packages.tricu-container |
Docker image |
checks.default / checks.tricu |
Haskell test suite via Tasty/HUnit |
tricu-zig-tests is deliberately not in checks so nix flake check remains fast.
10. Directory Layout
tricu/
├── flake.nix # Nix flake: packages, tests, devShell
├── tricu.cabal # Cabal package (used via callCabal2nix)
├── AGENTS.md # This file
├── src/ # Haskell modules
│ ├── Main.hs
│ ├── Eval.hs
│ ├── Parser.hs
│ ├── Lexer.hs
│ ├── FileEval.hs
│ ├── REPL.hs
│ ├── Research.hs
│ ├── ContentStore.hs
│ └── Wire.hs
├── test/
│ ├── Spec.hs # Tasty + HUnit tests
│ ├── *.tri # tricu test programs
│ ├── *.arboricx # Arboricx bundle fixtures
│ └── local-ns/ # Module namespace test files
├── lib/
│ ├── base.tri
│ ├── list.tri
│ └── patterns.tri
├── demos/
│ ├── equality.tri
│ ├── size.tri
│ ├── toSource.tri
│ ├── levelOrderTraversal.tri
│ └── patternMatching.tri
├── ext/ # Multi-language Arboricx hosts
│ ├── js/ # Node.js bundle parser + reducer
│ │ ├── src/
│ │ │ ├── bundle.js
│ │ │ ├── manifest.js
│ │ │ ├── merkle.js
│ │ │ ├── tree.js
│ │ │ ├── codecs.js
│ │ │ └── cli.js
│ │ └── test/
│ ├── php/ # PHP FFI host for libarboricx.so
│ │ ├── src/
│ │ │ └── ffi.php
│ │ └── run.php
│ └── zig/ # Zig production host
│ ├── build.zig
│ ├── build.zig.zon
│ ├── kernel_run_arboricx_typed.dag
│ ├── include/arboricx.h
│ ├── src/
│ │ ├── main.zig
│ │ ├── bundle.zig
│ │ ├── c_abi.zig
│ │ ├── codecs.zig
│ │ ├── kernel.zig
│ │ ├── reduce.zig
│ │ ├── arena.zig
│ │ ├── tree.zig
│ │ └── ternary.zig
│ ├── tests/
│ │ ├── c_abi_test.c
│ │ ├── c_abi_append_test.c
│ │ ├── native_bundle_append_test.c
│ │ ├── native_bundle_id_test.c
│ │ ├── native_bundle_bools_test.c
│ │ └── python_ffi_test.py
│ └── tools/
│ └── gen_kernel.zig
└── docs/
└── arboricx-bundle-format.md
11. Content Store Workflow (Custom DB)
The content store location is controlled by the TRICU_DB_PATH environment variable. When set, eval mode automatically loads all stored terms into the initial environment, so you can call any previously imported/evaluated term by name.
# Use a local DB
export TRICU_DB_PATH=/tmp/tricu-local.db
# Import terms from the standard library
./result/bin/tricu import -f lib/list.tri
# Now use them in eval mode
echo "not? (t t)" | ./result/bin/tricu eval -t decode
# Output: t
echo "not? (t t t)" | ./result/bin/tricu eval -t decode
# Output: Stem Leaf
echo "equal? (t t) (t t t)" | ./result/bin/tricu eval -t decode
# Output: t
# Check what's in the store
./result/bin/tricu
t> !definitions
Without TRICU_DB_PATH set, eval uses only the terms defined in the input file(s).
12. Development Tips
- REPL:
nix run .#starts the interactive tricu REPL. - Evaluate files:
nix run .# -- eval -f demos/equality.tri - Zig host:
nix build .#tricu-zigthen./result/bin/tricu-zig <bundle> [args...] - Zig tests:
nix build .#tricu-zig-tests - GHC options:
-threaded -rtsopts -with-rtsopts=-Nfor parallel runtime. Use-NRTS flag for multi-core. - Upx is in the devShell for binary compression if needed.
13. Viewing Haskell Dependency Docs from Nix
When you need Haddock documentation for a Haskell dependency available in Nixpkgs, build the package's doc output directly with ^doc.
Example:
Replace megaparsec with the dependency name you need:
nix build "nixpkgs#haskellPackages.${pkg}^doc"
View the available documentation files:
find ./result-doc -type f \( -name '*.html' -o -name '*.haddock' \) | sort