Compare commits
22 Commits
d37d443021
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a4fcc1cb36 | |||
| fdebb6c13d | |||
| 2e2db07bd6 | |||
| 7cea3d1559 | |||
| ac90d23b46 | |||
| 4bf2ce56dd | |||
| bf30d5945e | |||
| 7ae3fc33f4 | |||
| 1c17d4c94a | |||
| e2a1744508 | |||
| 020fa769a9 | |||
| 2e13583de3 | |||
| 593aa96193 | |||
| e2d035286d | |||
| 8d5e76db1c | |||
| e3dcf5edd7 | |||
| 8f7684a1bb | |||
| 983a0cc5a7 | |||
| d6df01105c | |||
| 31bf7094f4 | |||
| e0b1e95729 | |||
| ea748b2e5e |
@@ -1,65 +0,0 @@
|
||||
name: Test, Build, and Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
container:
|
||||
image: docker.matri.cx/nix-runner:v0.1.0
|
||||
credentials:
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up cache for Cabal
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/cabal
|
||||
~/.config/cabal
|
||||
~/.local/state/cabal
|
||||
key: cabal-${{ hashFiles('tricu.cabal') }}
|
||||
restore-keys: |
|
||||
cabal-
|
||||
|
||||
- name: Initialize Cabal and update package list
|
||||
run: |
|
||||
nix develop --command cabal update
|
||||
|
||||
- name: Run test suite
|
||||
run: |
|
||||
nix develop --command cabal test
|
||||
|
||||
build:
|
||||
needs: test
|
||||
container:
|
||||
image: docker.matri.cx/nix-runner:v0.1.0
|
||||
credentials:
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Build and shrink binary
|
||||
run: |
|
||||
nix build
|
||||
cp -L ./result/bin/tricu ./tricu
|
||||
chmod 755 ./tricu
|
||||
nix develop --command upx ./tricu
|
||||
|
||||
- name: Release binary
|
||||
uses: akkuman/gitea-release-action@v1
|
||||
with:
|
||||
files: |-
|
||||
./tricu
|
||||
token: '${{ secrets.RELEASE_TOKEN }}'
|
||||
body: '${{ gitea.event.head_commit.message }}'
|
||||
prerelease: true
|
||||
338
AGENTS.md
338
AGENTS.md
@@ -2,70 +2,25 @@
|
||||
|
||||
> For AI agents and contributors working in this repository.
|
||||
|
||||
## 0. Test Driven Development
|
||||
|
||||
Write and discuss tests with the user before working on implementation code. Do not modify existing tests without explicit permission.
|
||||
|
||||
## 1. Build & Test
|
||||
## Build & Test
|
||||
|
||||
```bash
|
||||
# Haskell tests (default check)
|
||||
# Tests
|
||||
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
|
||||
# Build tricu executable
|
||||
nix build .#
|
||||
```
|
||||
|
||||
### ⚠️ Never call `cabal` directly
|
||||
### Never call `cabal` directly
|
||||
|
||||
> **Rule of thumb:** if it builds, links, or tests, it goes through `nix`.
|
||||
|
||||
## 2. Project Overview
|
||||
## Project Overview
|
||||
|
||||
**tricu** (pronounced "tree-shoe") is a programming-language experiment written in Haskell. It implements [Triage Calculus](https://olydis.medium.com/a-visual-introduction-to-tree-calculus-2f4a34ceffc2), an extension of Barry Jay's Tree Calculus, with lambda-abstraction sugar that gets eliminated back to pure tree calculus terms.
|
||||
**tricu** (pronounced "tree-shoe") is a programming-language experiment written primarily in Haskell.
|
||||
|
||||
### 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.
|
||||
Core types are in `src/Research.hs`.
|
||||
|
||||
### File extensions
|
||||
|
||||
@@ -74,8 +29,6 @@ All hosts share the same bundle format and Merkle hashing scheme.
|
||||
- `.arboricx` - Portable executable bundle
|
||||
- `.dag` - Serialized kernel DAG (used by `gen_kernel.zig` at build time)
|
||||
|
||||
## 3. Test Suite
|
||||
|
||||
### Haskell tests
|
||||
|
||||
Tests live in `test/Spec.hs` and use **Tasty** + **HUnit**.
|
||||
@@ -84,42 +37,7 @@ 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:
|
||||
|
||||
```bash
|
||||
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
|
||||
## tricu Language Quick Reference
|
||||
|
||||
```
|
||||
t → Leaf (the base term)
|
||||
@@ -136,242 +54,4 @@ head (map f xs) → From lib/list.tri
|
||||
```
|
||||
|
||||
CRITICAL:
|
||||
|
||||
When working with recursion in `tricu` files:
|
||||
|
||||
1. Put consumed data first in recursive workers.
|
||||
2. Let data shape drive recursion.
|
||||
3. 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.
|
||||
|
||||
```bash
|
||||
# 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:
|
||||
|
||||
```c
|
||||
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.
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
```bash
|
||||
# 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-zig` then `./result/bin/tricu-zig <bundle> [args...]`
|
||||
- **Zig tests:** `nix build .#tricu-zig-tests`
|
||||
- **GHC options:** `-threaded -rtsopts -with-rtsopts=-N` for parallel runtime. Use `-N` RTS 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:
|
||||
|
||||
```sh
|
||||
nix build "nixpkgs#haskellPackages.${pkg}^doc"
|
||||
```
|
||||
|
||||
View the available documentation files:
|
||||
|
||||
```sh
|
||||
find ./result-doc -type f \( -name '*.html' -o -name '*.haddock' \) | sort
|
||||
```
|
||||
When working with `tricu` `.tri` files ***YOU MUST REVIEW notes/tricu-normalization-rules.md***
|
||||
|
||||
82
README.md
82
README.md
@@ -2,13 +2,15 @@
|
||||
|
||||
## Introduction
|
||||
|
||||
tricu (pronounced "tree-shoe") is a programming language experiment in Haskell. It is fundamentally based on the application of [Triage Calculus](https://olydis.medium.com/a-visual-introduction-to-tree-calculus-2f4a34ceffc2), an extended form of [Tree Calculus](https://github.com/barry-jay-personal/typed_tree_calculus/blob/main/typed_program_analysis.pdf), but minimal syntax sugar is included.
|
||||
tricu (pronounced "tree-shoe") is an experimental programming language written in Haskell. It is fundamentally based on the application of [Triage Calculus](https://olydis.medium.com/a-visual-introduction-to-tree-calculus-2f4a34ceffc2), an extended form of [Tree Calculus](https://github.com/barry-jay-personal/typed_tree_calculus/blob/main/typed_program_analysis.pdf). I refer to this "family" of calculi as TC below.
|
||||
|
||||
tricu is the word for "tree" in Lojban: `(x1) is a tree of species/cultivar (x2)`.
|
||||
|
||||
In the `ext/` directory there are implementations of TC evaluators and tooling in other languages. Here be dragons; beware.
|
||||
|
||||
I have fully embraced the slopmachine (LLM-assisted development) for this project. Nothing is stable or sacred. We will discover sanity at the end of the journey but we won't strive for it until then.
|
||||
|
||||
This README.md is human written. No other .md file will be until stabilization.
|
||||
This README.md is 100% human written. No other .md file will be until stabilization.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
@@ -35,22 +37,6 @@ tricu > "(t (t (t t) (t t t)) (t t (t t t)))"
|
||||
tricu < -- or calculate its size (/demos/size.tri)
|
||||
tricu < size not?
|
||||
tricu > 12
|
||||
|
||||
tricu < !help
|
||||
tricu version 1.1.0
|
||||
Available commands:
|
||||
!exit - Exit the REPL
|
||||
!clear - Clear the screen
|
||||
!reset - Reset preferences for selected versions
|
||||
!help - Show tricu version and available commands
|
||||
!output - Change output format (tree|fsl|ast|ternary|ascii|decode)
|
||||
!definitions - List all defined terms in the content store
|
||||
!import - Import definitions from file to the content store
|
||||
!watch - Watch a file for changes, evaluate terms, and store them
|
||||
!refresh - Refresh environment from content store (definitions are live)
|
||||
!versions - Show all versions of a term by name
|
||||
!select - Select a specific version of a term for subsequent lookups
|
||||
!tag - Add or update a tag for a term by hash or name
|
||||
```
|
||||
|
||||
## Installation and Use
|
||||
@@ -66,4 +52,62 @@ You can easily build and run this project using [Nix](https://nixos.org/download
|
||||
|
||||
## Usage
|
||||
|
||||
I'll update this once the CLI stabilizes more.
|
||||
### CLI
|
||||
|
||||
Evaluate one or more files:
|
||||
|
||||
```sh
|
||||
tricu eval program.tri
|
||||
tricu eval --format decode program.tri
|
||||
tricu eval --output result.txt program.tri
|
||||
```
|
||||
|
||||
Unchecked eval parses annotation syntax, discards contract metadata, skips
|
||||
producer-side View Contract checks during workspace module auto-builds, and does
|
||||
not publish unchecked View refs.
|
||||
|
||||
```sh
|
||||
tricu eval --unchecked program.tri
|
||||
```
|
||||
|
||||
Check View Contract annotations explicitly:
|
||||
|
||||
```sh
|
||||
tricu check program.tri
|
||||
tricu check --store ./.tricu-store program.tri
|
||||
```
|
||||
|
||||
Compile/import/export Arboricx bundles:
|
||||
|
||||
```sh
|
||||
tricu arboricx compile --file program.tri --output program.arboricx
|
||||
tricu arboricx import --file program.arboricx --module program
|
||||
tricu arboricx export --module prelude --output prelude.arboricx
|
||||
```
|
||||
|
||||
Inspect store aliases:
|
||||
|
||||
```sh
|
||||
tricu store alias list --kind modules
|
||||
tricu store alias get --kind modules prelude
|
||||
```
|
||||
|
||||
### REPL
|
||||
|
||||
Running `tricu` with no subcommand starts the REPL. The REPL uses the same
|
||||
filesystem content store and workspace module loader as the CLI.
|
||||
|
||||
Useful commands:
|
||||
|
||||
```text
|
||||
!load FILE load/evaluate a .tri file without printing a result
|
||||
!check FILE run View Contract checking for a file
|
||||
!store [PATH] show or set the content-addressed store
|
||||
!unchecked on evaluate loaded files without contract checking/publishing refs
|
||||
!unchecked off return to normal producer-checked module loading
|
||||
!format decode set output format by name
|
||||
!env list current in-memory bindings
|
||||
```
|
||||
|
||||
`!load` and `!check` support filename tab completion. Normal REPL input also
|
||||
supports tab completion for names currently in the REPL environment.
|
||||
|
||||
240
bench/ApplyStats.hs
Normal file
240
bench/ApplyStats.hs
Normal file
@@ -0,0 +1,240 @@
|
||||
{-# LANGUAGE BangPatterns #-}
|
||||
|
||||
module ApplyStats
|
||||
( ApplyStats(..)
|
||||
, emptyApplyStats
|
||||
, emptyApplyStatsSampled
|
||||
, applyCounted
|
||||
, runApplyCounted
|
||||
, runApplySampledWithProgress
|
||||
, runApplyGlobalCounted
|
||||
, printApplyStats
|
||||
) where
|
||||
|
||||
import Research
|
||||
import qualified Data.Map.Strict as M
|
||||
import qualified Data.List as L
|
||||
import Data.Ord (comparing)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Debug.Trace (trace)
|
||||
import System.IO.Unsafe (unsafePerformIO, unsafeDupablePerformIO)
|
||||
import Data.IORef
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Threaded stats (slow but pure)
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
type Hash = Text
|
||||
type AppKey = (Hash, Hash)
|
||||
|
||||
data ApplyStats = ApplyStats
|
||||
{ totalApplyCalls :: !Int
|
||||
, uniqueApps :: !(M.Map AppKey Int)
|
||||
, sampleInterval :: !Int
|
||||
, sampleCounter :: !Int
|
||||
, progressEvery :: !Int
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
emptyApplyStats :: ApplyStats
|
||||
emptyApplyStats = emptyApplyStatsSampled 1
|
||||
|
||||
emptyApplyStatsSampled :: Int -> ApplyStats
|
||||
emptyApplyStatsSampled n = ApplyStats
|
||||
{ totalApplyCalls = 0
|
||||
, uniqueApps = M.empty
|
||||
, sampleInterval = max 1 n
|
||||
, sampleCounter = 0
|
||||
, progressEvery = 0
|
||||
}
|
||||
|
||||
bump :: T -> T -> ApplyStats -> ApplyStats
|
||||
bump !f !x !st =
|
||||
let !counter' = sampleCounter st + 1
|
||||
!total' = totalApplyCalls st + 1
|
||||
!stBase = st { totalApplyCalls = total'
|
||||
, sampleCounter = counter'
|
||||
}
|
||||
!st' = if counter' `mod` sampleInterval st /= 0
|
||||
then stBase
|
||||
else let !hf = termHash f
|
||||
!hx = termHash x
|
||||
!k = (hf, hx)
|
||||
!m = M.insertWith (+) k 1 (uniqueApps st)
|
||||
in stBase { uniqueApps = m }
|
||||
in case progressEvery st of
|
||||
0 -> st'
|
||||
n | total' `mod` n == 0 ->
|
||||
trace ("apply calls so far: " ++ show total') st'
|
||||
_ -> st'
|
||||
|
||||
termHash :: T -> Hash
|
||||
termHash Leaf =
|
||||
nodeHash NLeaf
|
||||
termHash (Stem t) =
|
||||
nodeHash (NStem (termHash t))
|
||||
termHash (Fork l r) =
|
||||
nodeHash (NFork (termHash l) (termHash r))
|
||||
|
||||
applyCounted :: T -> T -> ApplyStats -> (T, ApplyStats)
|
||||
applyCounted !f !x !st0 =
|
||||
let !st1 = bump f x st0
|
||||
in applyStepCounted f x st1
|
||||
|
||||
applyStepCounted :: T -> T -> ApplyStats -> (T, ApplyStats)
|
||||
applyStepCounted (Fork Leaf a) _ st =
|
||||
(a, st)
|
||||
applyStepCounted (Fork (Stem a) b) c st =
|
||||
let (!ac, !st1) = applyCounted a c st
|
||||
(!bc, !st2) = applyCounted b c st1
|
||||
in applyCounted ac bc st2
|
||||
applyStepCounted (Fork (Fork a _b) _c) Leaf st =
|
||||
(a, st)
|
||||
applyStepCounted (Fork (Fork _a b) _c) (Stem u) st =
|
||||
applyCounted b u st
|
||||
applyStepCounted (Fork (Fork _a _b) c) (Fork u v) st =
|
||||
let (!cu, !st1) = applyCounted c u st
|
||||
in applyCounted cu v st1
|
||||
applyStepCounted Leaf b st =
|
||||
(Stem b, st)
|
||||
applyStepCounted (Stem a) b st =
|
||||
(Fork a b, st)
|
||||
|
||||
runApplyCounted :: T -> T -> (T, ApplyStats)
|
||||
runApplyCounted !f !x =
|
||||
applyCounted f x emptyApplyStats
|
||||
|
||||
runApplySampled :: Int -> T -> T -> (T, ApplyStats)
|
||||
runApplySampled !n !f !x =
|
||||
applyCounted f x (emptyApplyStatsSampled n)
|
||||
|
||||
runApplySampledWithProgress :: Int -> Int -> T -> T -> (T, ApplyStats)
|
||||
runApplySampledWithProgress !interval !progress !f !x =
|
||||
let st = (emptyApplyStatsSampled interval) { progressEvery = progress }
|
||||
in applyCounted f x st
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Global mutable stats (fast, unsafe, single-threaded only)
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
{-# NOINLINE globalTotalCount #-}
|
||||
globalTotalCount :: IORef Int
|
||||
globalTotalCount = unsafePerformIO (newIORef 0)
|
||||
|
||||
{-# NOINLINE globalInterval #-}
|
||||
globalInterval :: IORef Int
|
||||
globalInterval = unsafePerformIO (newIORef 1)
|
||||
|
||||
{-# NOINLINE globalMap #-}
|
||||
globalMap :: IORef (M.Map AppKey Int)
|
||||
globalMap = unsafePerformIO (newIORef M.empty)
|
||||
|
||||
{-# NOINLINE globalProgress #-}
|
||||
globalProgress :: IORef Int
|
||||
globalProgress = unsafePerformIO (newIORef 0)
|
||||
|
||||
resetGlobalStats :: Int -> Int -> IO ()
|
||||
resetGlobalStats !interval !progress = do
|
||||
writeIORef globalTotalCount 0
|
||||
writeIORef globalInterval (max 1 interval)
|
||||
writeIORef globalMap M.empty
|
||||
writeIORef globalProgress progress
|
||||
|
||||
readGlobalStats :: IO ApplyStats
|
||||
readGlobalStats = do
|
||||
total <- readIORef globalTotalCount
|
||||
m <- readIORef globalMap
|
||||
pure ApplyStats
|
||||
{ totalApplyCalls = total
|
||||
, uniqueApps = m
|
||||
, sampleInterval = 0
|
||||
, sampleCounter = 0
|
||||
, progressEvery = 0
|
||||
}
|
||||
|
||||
{-# INLINE globalBump #-}
|
||||
globalBump :: T -> T -> ()
|
||||
globalBump !f !x = unsafeDupablePerformIO $ do
|
||||
!total <- readIORef globalTotalCount
|
||||
let !total' = total + 1
|
||||
writeIORef globalTotalCount total'
|
||||
!interval <- readIORef globalInterval
|
||||
!progress <- readIORef globalProgress
|
||||
let !_ = if progress > 0 && total' `mod` progress == 0
|
||||
then trace ("apply calls so far: " ++ show total') ()
|
||||
else ()
|
||||
if total' `mod` interval /= 0
|
||||
then pure ()
|
||||
else do
|
||||
let !hf = termHash f
|
||||
!hx = termHash x
|
||||
!k = (hf, hx)
|
||||
!m <- readIORef globalMap
|
||||
writeIORef globalMap (M.insertWith (+) k 1 m)
|
||||
pure ()
|
||||
|
||||
applyGlobalCounted :: T -> T -> T
|
||||
applyGlobalCounted !f !x =
|
||||
let !_ = globalBump f x
|
||||
in applyGlobalStep f x
|
||||
|
||||
applyGlobalStep :: T -> T -> T
|
||||
applyGlobalStep (Fork Leaf a) _ = a
|
||||
applyGlobalStep (Fork (Stem a) b) c =
|
||||
applyGlobalCounted (applyGlobalCounted a c) (applyGlobalCounted b c)
|
||||
applyGlobalStep (Fork (Fork a _b) _c) Leaf = a
|
||||
applyGlobalStep (Fork (Fork _a b) _c) (Stem u) = applyGlobalCounted b u
|
||||
applyGlobalStep (Fork (Fork _a _b) c) (Fork u v) =
|
||||
applyGlobalCounted (applyGlobalCounted c u) v
|
||||
applyGlobalStep Leaf b = Stem b
|
||||
applyGlobalStep (Stem a) b = Fork a b
|
||||
|
||||
runApplyGlobalCounted :: Int -> Int -> T -> T -> IO (T, ApplyStats)
|
||||
runApplyGlobalCounted !interval !progress !f !x = do
|
||||
resetGlobalStats interval progress
|
||||
let !result = applyGlobalCounted f x
|
||||
!stats <- readGlobalStats
|
||||
pure (result, stats)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Printing
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
printApplyStats :: ApplyStats -> IO ()
|
||||
printApplyStats st = do
|
||||
let !total = totalApplyCalls st
|
||||
!uniq = M.size (uniqueApps st)
|
||||
!ratio =
|
||||
if uniq == 0
|
||||
then 0 :: Double
|
||||
else fromIntegral total / fromIntegral uniq
|
||||
|
||||
counts =
|
||||
reverse
|
||||
. L.sortBy (comparing snd)
|
||||
. M.toList
|
||||
$ uniqueApps st
|
||||
|
||||
repeated =
|
||||
filter ((> 1) . snd) counts
|
||||
|
||||
top20 = take 20 repeated
|
||||
|
||||
putStrLn $ "total apply calls: " ++ show total
|
||||
putStrLn $ "unique application patterns: " ++ show uniq
|
||||
putStrLn $ "duplication ratio total/unique: " ++ show ratio
|
||||
putStrLn $ "repeated application patterns: " ++ show (length repeated)
|
||||
|
||||
putStrLn "top repeated application counts:"
|
||||
mapM_ printTop top20
|
||||
where
|
||||
short h = T.unpack (T.take 12 h)
|
||||
|
||||
printTop ((hf, hx), n) =
|
||||
putStrLn $
|
||||
" " ++ show n
|
||||
++ "x apply "
|
||||
++ short hf
|
||||
++ " "
|
||||
++ short hx
|
||||
125
bench/Bench.hs
Normal file
125
bench/Bench.hs
Normal file
@@ -0,0 +1,125 @@
|
||||
{-# LANGUAGE BangPatterns #-}
|
||||
module Main where
|
||||
|
||||
import Criterion.Main
|
||||
import qualified Data.ByteString as BS
|
||||
import qualified Data.Map as Map
|
||||
|
||||
import ApplyStats (runApplyCounted, runApplyGlobalCounted, printApplyStats)
|
||||
import Eval
|
||||
import FileEval
|
||||
import Parser
|
||||
import Research
|
||||
|
||||
-- | Pre-process a demo file and return its AST.
|
||||
loadDemo :: FilePath -> IO [TricuAST]
|
||||
loadDemo = preprocessFile
|
||||
|
||||
-- | Evaluate a pre-processed demo to its result term.
|
||||
runDemo :: [TricuAST] -> T
|
||||
runDemo ast = result (evalTricu Map.empty ast)
|
||||
|
||||
-- | Build an environment from a library file.
|
||||
loadLib :: FilePath -> IO Env
|
||||
loadLib = evaluateFile
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
!equalityAst <- loadDemo "demos/equality.tri"
|
||||
!sizeAst <- loadDemo "demos/size.tri"
|
||||
!toSourceAst <- loadDemo "demos/toSource.tri"
|
||||
!levelOrderAst <- loadDemo "demos/levelOrderTraversal.tri"
|
||||
!patternAst <- loadDemo "demos/patternMatching.tri"
|
||||
!listLib <- loadLib "lib/list.tri"
|
||||
|
||||
-- Stress benchmark environment: Arboricx parser + size + toSource
|
||||
!arboricxLib <- loadLib "lib/arboricx/dispatch.tri"
|
||||
!sizeEnv <- evaluateFileWithContext arboricxLib "demos/size.tri"
|
||||
!toSourceEnv <- evaluateFileWithContext sizeEnv "demos/toSource.tri"
|
||||
|
||||
-- Print apply stats for toSource not?
|
||||
let Just toSource = Map.lookup "toSource" toSourceEnv
|
||||
Just notTerm = Map.lookup "not?" toSourceEnv
|
||||
(_result, stats) = runApplyCounted toSource notTerm
|
||||
printApplyStats stats
|
||||
|
||||
-- Print apply stats for readArboricxContainer against id.arboricx
|
||||
!idBundleBytes <- BS.readFile "test/fixtures/id.arboricx"
|
||||
let Just readContainer = Map.lookup "readArboricxContainer" sizeEnv
|
||||
bundleTree = ofBytes idBundleBytes
|
||||
(_result2, stats2) <- runApplyGlobalCounted 100000 1000000 readContainer bundleTree
|
||||
printApplyStats stats2
|
||||
|
||||
defaultMain
|
||||
[ bgroup "demos"
|
||||
[ bench "equality" $ whnf runDemo equalityAst
|
||||
, bench "size" $ whnf runDemo sizeAst
|
||||
, bench "toSource" $ whnf runDemo toSourceAst
|
||||
, bench "levelOrderTraversal" $ whnf runDemo levelOrderAst
|
||||
, bench "patternMatching" $ whnf runDemo patternAst
|
||||
]
|
||||
|
||||
, bgroup "lib/list.tri"
|
||||
[ bench "append strings" $ whnf
|
||||
(result . evalTricu listLib . parseTricu)
|
||||
"append \"Hello, \" \"world!\""
|
||||
, bench "map over 3 elements" $ whnf
|
||||
(result . evalTricu listLib . parseTricu)
|
||||
"head (tail (map (a : (t t t)) [(t) (t) (t)]))"
|
||||
, bench "equal? same" $ whnf
|
||||
(result . evalTricu listLib . parseTricu)
|
||||
"equal? (t t t) (t t t)"
|
||||
, bench "equal? different" $ whnf
|
||||
(result . evalTricu listLib . parseTricu)
|
||||
"equal? (t t) (t t t)"
|
||||
, bench "triage Leaf" $ whnf
|
||||
(result . evalTricu listLib . parseTricu)
|
||||
"test t"
|
||||
, bench "triage Stem" $ whnf
|
||||
(result . evalTricu listLib . parseTricu)
|
||||
"test (t t)"
|
||||
, bench "triage Fork" $ whnf
|
||||
(result . evalTricu listLib . parseTricu)
|
||||
"test (t t t)"
|
||||
, bench "not? true" $ whnf
|
||||
(result . evalTricu listLib . parseTricu)
|
||||
"not? (t t)"
|
||||
, bench "not? false" $ whnf
|
||||
(result . evalTricu listLib . parseTricu)
|
||||
"not? t"
|
||||
]
|
||||
|
||||
, bgroup "stress"
|
||||
[ bench "size runArboricxTyped" $ whnf
|
||||
(result . evalTricu sizeEnv . parseTricu)
|
||||
"size runArboricxTyped"
|
||||
, bench "equal? runArboricxTyped runArboricxTyped" $ whnf
|
||||
(result . evalTricu sizeEnv . parseTricu)
|
||||
"equal? runArboricxTyped runArboricxTyped"
|
||||
, bench "size readArboricxBundle" $ whnf
|
||||
(result . evalTricu sizeEnv . parseTricu)
|
||||
"size readArboricxBundle"
|
||||
, bench "equal? readArboricxBundle readArboricxBundle" $ whnf
|
||||
(result . evalTricu sizeEnv . parseTricu)
|
||||
"equal? readArboricxBundle readArboricxBundle"
|
||||
]
|
||||
|
||||
, bgroup "raw-apply"
|
||||
[ bench "rule-1 (Fork Leaf a) b" $ whnf
|
||||
(\n -> apply (Fork Leaf (ofNumber n)) (ofNumber 42))
|
||||
1000
|
||||
, bench "rule-2 (Fork (Stem a) b) c" $ whnf
|
||||
(\n -> apply (Fork (Stem (ofNumber n)) (ofNumber n)) (ofNumber 42))
|
||||
1000
|
||||
, bench "rule-3a (Fork (Fork a b) c) Leaf" $ whnf
|
||||
(\n -> apply (Fork (Fork (ofNumber n) (ofNumber n)) (ofNumber n)) Leaf)
|
||||
1000
|
||||
, bench "rule-3b (Fork (Fork a b) c) (Stem u)" $ whnf
|
||||
(\n -> apply (Fork (Fork (ofNumber n) (ofNumber n)) (ofNumber n)) (Stem Leaf))
|
||||
1000
|
||||
, bench "rule-3c (Fork (Fork a b) c) (Fork u v)" $ whnf
|
||||
(\n -> apply (Fork (Fork (ofNumber n) (ofNumber n)) (ofNumber n)) (Fork Leaf Leaf))
|
||||
1000
|
||||
]
|
||||
|
||||
]
|
||||
@@ -1,5 +1,4 @@
|
||||
!import "../lib/base.tri" !Local
|
||||
!import "../lib/list.tri" !Local
|
||||
!import "prelude" !Local
|
||||
|
||||
main = lambdaEqualsTC
|
||||
|
||||
|
||||
57
demos/interactionTrees.tri
Normal file
57
demos/interactionTrees.tri
Normal file
@@ -0,0 +1,57 @@
|
||||
!import "prelude" !Local
|
||||
!import "io" !Local
|
||||
|
||||
-- Interaction Tree Effect Runtime
|
||||
--
|
||||
-- The IO system is an interaction-tree effect runtime interpreted by a
|
||||
-- small-step machine with a cooperative scheduler. Primitive actions
|
||||
-- (putStr, readFile, writeFile, ...) are tagged nodes in an interaction
|
||||
-- tree. Sequencing is performed by the single generic `bind` constructor.
|
||||
--
|
||||
-- pure x -- lift a pure value into IO
|
||||
-- bind action k -- run action, then apply k to its result
|
||||
-- thenIO a b -- run a, discard its result, then run b
|
||||
-- mapIO action f -- run action, then apply f to its result inside pure
|
||||
--
|
||||
-- The runtime supports several effects beyond basic IO:
|
||||
-- ask -- read the current environment
|
||||
-- local f action -- run action with environment transformed by f
|
||||
-- get -- read the current mutable state
|
||||
-- put s -- replace the mutable state
|
||||
-- fork action -- spawn a concurrent task, returning a handle
|
||||
-- await handle -- wait for a forked task to complete
|
||||
-- yield -- yield control to the scheduler
|
||||
-- sleep ms -- suspend current task for N milliseconds
|
||||
--
|
||||
-- File operations return a Result tree (see lib/base.tri):
|
||||
-- ok value -- pair true (pair value t)
|
||||
-- err msg -- pair false (pair msg t)
|
||||
--
|
||||
-- Use onReadFile / onWriteFile for convenient branching.
|
||||
--
|
||||
-- See demos/interactionTrees/ for smaller focused examples.
|
||||
|
||||
-- Cooperative async demo.
|
||||
-- fork runs an action in the background.
|
||||
-- sleep suspends the current task for N milliseconds.
|
||||
-- await waits for a forked task and returns its value.
|
||||
--
|
||||
-- Here the child sleeps for 2 s while the parent prints immediately.
|
||||
-- The parent's message appears first, proving interleaving.
|
||||
|
||||
asyncDemo = (
|
||||
bind (fork
|
||||
(bind (sleep 2000) (_ :
|
||||
bind (putStrLn "2000ms done sleeping!") (_ :
|
||||
pure "child2000 done"))))
|
||||
(handle2000 :
|
||||
bind (fork
|
||||
(bind (sleep 5000) (_ :
|
||||
bind (putStrLn "5000ms done sleeping!") (_ :
|
||||
pure "child5000 done"))))
|
||||
(handle5000 :
|
||||
bind (putStrLn "Parent first!") (_ :
|
||||
bind (await handle5000) (_ :
|
||||
await handle2000)))))
|
||||
|
||||
main = io asyncDemo
|
||||
22
demos/interactionTrees/arboricxServer.tri
Normal file
22
demos/interactionTrees/arboricxServer.tri
Normal file
@@ -0,0 +1,22 @@
|
||||
!import "base" !Local
|
||||
!import "io" !Local
|
||||
!import "arboricx.server" !Local
|
||||
|
||||
-- Arboricx HTTP registry server demo.
|
||||
-- Run with --allow-write ./store --allow-read ./store
|
||||
--
|
||||
-- Endpoints:
|
||||
-- GET /_arboricx/health -> "OK"
|
||||
-- POST /_arboricx/bundle -> upload bundle, returns hash
|
||||
-- GET /_arboricx/bundle/hash/:h -> download bundle by hash
|
||||
--
|
||||
-- Example usage:
|
||||
-- curl http://localhost:9050/_arboricx/health
|
||||
-- curl -X POST --data-binary @mybundle.arboricx http://localhost:9050/_arboricx/bundles
|
||||
-- curl http://localhost:9050/_arboricx/bundle/hash/<hash>
|
||||
|
||||
main = io (thenIO
|
||||
(putStrLn "Starting Arboricx server on 127.0.0.1:9050")
|
||||
(thenIO
|
||||
(void (ensureStore "/tmp/store"))
|
||||
(arboricxServer "/tmp/store" "127.0.0.1" 9050)))
|
||||
28
demos/interactionTrees/echoServer.tri
Normal file
28
demos/interactionTrees/echoServer.tri
Normal file
@@ -0,0 +1,28 @@
|
||||
!import "prelude" !Local
|
||||
!import "io" !Local
|
||||
!import "socket" !Local
|
||||
|
||||
-- Main accept+echo loop. Recursion via y.
|
||||
echoLoop = y (self : server :
|
||||
withAccepted_ server
|
||||
(err :
|
||||
bind (putStrLn (append "accept error: " err)) (_ :
|
||||
self server))
|
||||
(clientSock addr :
|
||||
bind (putStrLn (append "client from " addr)) (_ :
|
||||
onResult_ (recv clientSock 4096)
|
||||
(err :
|
||||
bind (closeSocket clientSock) (_ :
|
||||
self server))
|
||||
(msg :
|
||||
bind (send clientSock msg) (_ :
|
||||
bind (closeSocket clientSock) (_ :
|
||||
self server))))))
|
||||
|
||||
main = io (
|
||||
onOk_ socket (server :
|
||||
onOk_ (bindSocket server "127.0.0.1" 0) (_ :
|
||||
onOk_ (listen server 5) (_ :
|
||||
onOk_ (getSocketName server) (port :
|
||||
bind (putStrLn (append "Echo server listening on port " (showNumber port))) (_ :
|
||||
echoLoop server))))))
|
||||
20
demos/interactionTrees/environment.tri
Normal file
20
demos/interactionTrees/environment.tri
Normal file
@@ -0,0 +1,20 @@
|
||||
!import "base" !Local
|
||||
!import "list" !Local
|
||||
!import "io" !Local
|
||||
|
||||
-- Environment effects: ask and local.
|
||||
-- ask reads the current environment value.
|
||||
-- local f action runs action with the env transformed by f.
|
||||
--
|
||||
-- The CLI starts with an empty (Leaf) environment. This demo uses
|
||||
-- local to inject a real string so that ask returns something readable.
|
||||
|
||||
main = io <|
|
||||
(bind
|
||||
local (_ : "sandbox")
|
||||
(bind ask (env :
|
||||
bind (putStrLn (append "working in env: " env)) (_ :
|
||||
pure "inside-done"))))
|
||||
(outside :
|
||||
bind (putStrLn (append "local returned: " outside)) (_ :
|
||||
pure t))
|
||||
18
demos/interactionTrees/forkAwait.tri
Normal file
18
demos/interactionTrees/forkAwait.tri
Normal file
@@ -0,0 +1,18 @@
|
||||
!import "base" !Local
|
||||
!import "list" !Local
|
||||
!import "io" !Local
|
||||
|
||||
-- Basic fork and await.
|
||||
-- fork spawns a concurrent task and returns a handle.
|
||||
-- await blocks until the task completes and returns its value.
|
||||
|
||||
worker = msg :
|
||||
bind (putStrLn (append "working: " msg)) (_ :
|
||||
pure (append msg "-result"))
|
||||
|
||||
main = io <|
|
||||
(bind (fork (worker "job1")) (h1 :
|
||||
bind (fork (worker "job2")) (h2 :
|
||||
bind (await h1) (r1 :
|
||||
bind (await h2) (r2 :
|
||||
putStrLn (append "Got " (append r1 (append " and " r2))))))))
|
||||
26
demos/interactionTrees/getLineAsync.tri
Normal file
26
demos/interactionTrees/getLineAsync.tri
Normal file
@@ -0,0 +1,26 @@
|
||||
-- Manual test for async getLine
|
||||
--
|
||||
-- Run with:
|
||||
-- nix run .# -- eval -f demos/async-getline-test.tri --io
|
||||
--
|
||||
-- Expected behaviour:
|
||||
-- 1. You immediately see:
|
||||
-- Please enter your first name:
|
||||
-- (this printed before you typed anything)
|
||||
-- (this second line also printed before you typed anything)
|
||||
-- 2. You type your name and press Enter.
|
||||
-- 3. You see:
|
||||
-- Hello, <name>!
|
||||
|
||||
!import "prelude" !Local
|
||||
!import "io" !Local
|
||||
|
||||
main = io <|
|
||||
bind (fork getLine) (h :
|
||||
bind (putStr "Please enter your first name: ") (_ :
|
||||
bind (putStr "\n(this printed before you typed anything)\n") (_ :
|
||||
bind (putStr "\n(this second line also printed before you typed anything)\n") (_ :
|
||||
bind (await h) (name :
|
||||
bind (putStr "Hello, ") (_ :
|
||||
bind (putStr name) (_ :
|
||||
putStr "!\n")))))))
|
||||
10
demos/interactionTrees/greet.tri
Normal file
10
demos/interactionTrees/greet.tri
Normal file
@@ -0,0 +1,10 @@
|
||||
!import "base" !Local
|
||||
!import "list" !Local
|
||||
!import "io" !Local
|
||||
|
||||
-- Greet and return a pure value.
|
||||
-- putStrLn writes to stdout; pure lifts "done" into IO.
|
||||
|
||||
main = io <|
|
||||
bind (putStrLn (append "Hello, " "tricu"))
|
||||
(_ : pure "")
|
||||
16
demos/interactionTrees/httpServer.tri
Normal file
16
demos/interactionTrees/httpServer.tri
Normal file
@@ -0,0 +1,16 @@
|
||||
!import "prelude" !Local
|
||||
!import "io" !Local
|
||||
!import "socket" !Local
|
||||
!import "http" !Local
|
||||
|
||||
myRouter = (method path headers body :
|
||||
matchBool
|
||||
(okResponse (append "Hello from " (append path "\n")))
|
||||
(methodNotAllowedResponse)
|
||||
(strEq? method "GET"))
|
||||
|
||||
main = io (
|
||||
onOk_ socket (server :
|
||||
onOk_ (bindSocket server "127.0.0.1" 9050) (_ :
|
||||
onOk_ (listen server 5) (_ :
|
||||
serveForever server (httpHandler myRouter)))))
|
||||
16
demos/interactionTrees/safeRead.tri
Normal file
16
demos/interactionTrees/safeRead.tri
Normal file
@@ -0,0 +1,16 @@
|
||||
!import "base" !Local
|
||||
!import "list" !Local
|
||||
!import "io" !Local
|
||||
|
||||
-- readFile returns a Result. matchResult branches on ok / err.
|
||||
-- Run with --allow-read PATH or --unsafe-io.
|
||||
|
||||
safeRead = (path :
|
||||
bind (readFile path)
|
||||
(result :
|
||||
matchResult
|
||||
(err rest : pure "ERROR: Unable to read file")
|
||||
(contents rest : pure contents)
|
||||
result))
|
||||
|
||||
main = io (safeRead "demos/interactionTrees/greet.tri")
|
||||
23
demos/interactionTrees/shout.tri
Normal file
23
demos/interactionTrees/shout.tri
Normal file
@@ -0,0 +1,23 @@
|
||||
!import "base" !Local
|
||||
!import "list" !Local
|
||||
!import "io" !Local
|
||||
|
||||
-- Transform an IO result.
|
||||
-- mapIO applies a pure function to the value produced by an action.
|
||||
-- Run with --allow-read PATH or --unsafe-io.
|
||||
|
||||
safeRead = (path :
|
||||
bind (readFile path)
|
||||
(result :
|
||||
matchResult
|
||||
(err rest : pure "missing")
|
||||
(contents rest : pure contents)
|
||||
result))
|
||||
|
||||
shout = (path :
|
||||
mapIO (safeRead path)
|
||||
(text : append text "!!!"))
|
||||
|
||||
main = io (bind
|
||||
(shout "demos/interactionTrees/greet.tri")
|
||||
(text : putStrLn text))
|
||||
22
demos/interactionTrees/state.tri
Normal file
22
demos/interactionTrees/state.tri
Normal file
@@ -0,0 +1,22 @@
|
||||
!import "base" !Local
|
||||
!import "list" !Local
|
||||
!import "io" !Local
|
||||
|
||||
-- Mutable state via get and put.
|
||||
-- get reads the current state.
|
||||
-- put replaces the state.
|
||||
--
|
||||
-- The CLI starts with an empty (Leaf) state. This demo puts
|
||||
-- readable strings and prints them back out.
|
||||
|
||||
main = io <|
|
||||
bind (put "idle") (_ :
|
||||
bind get (s1 :
|
||||
bind (putStrLn (append "state: " s1)) (_ :
|
||||
bind (put "running") (_ :
|
||||
bind get (s2 :
|
||||
bind (putStrLn (append "state: " s2)) (_ :
|
||||
bind (put "done") (_ :
|
||||
bind get (s3 :
|
||||
bind (putStrLn (append "state: " s3)) (_ :
|
||||
pure t)))))))))
|
||||
20
demos/interactionTrees/writeThenRead.tri
Normal file
20
demos/interactionTrees/writeThenRead.tri
Normal file
@@ -0,0 +1,20 @@
|
||||
!import "base" !Local
|
||||
!import "list" !Local
|
||||
!import "io" !Local
|
||||
|
||||
-- Write a file, then read it back.
|
||||
-- thenIO discards the writeFile Result and continues.
|
||||
-- Run with --unsafe-io (needs both read and write permissions).
|
||||
|
||||
writeThenRead = (path text :
|
||||
thenIO
|
||||
(writeFile path text)
|
||||
(readFile path))
|
||||
|
||||
main = io <|
|
||||
(bind (writeThenRead "/tmp/tricu-demo.txt" "hello from tricu")
|
||||
(result :
|
||||
matchResult
|
||||
(err rest : putStrLn "error")
|
||||
(contents rest : putStrLn contents)
|
||||
result))
|
||||
33
demos/interactionTrees/yield.tri
Normal file
33
demos/interactionTrees/yield.tri
Normal file
@@ -0,0 +1,33 @@
|
||||
!import "base" !Local
|
||||
!import "list" !Local
|
||||
!import "io" !Local
|
||||
|
||||
-- Cooperative scheduling with yield.
|
||||
-- yield returns control to the scheduler so other tasks can run.
|
||||
--
|
||||
-- Two tasks print alternately because each yields after every line.
|
||||
|
||||
--chatter = (name n :
|
||||
-- bind (putStrLn (append name " says 1")) (_ :
|
||||
-- bind yield (_ :
|
||||
-- bind (putStrLn (append name " says 2")) (_ :
|
||||
-- bind yield (_ :
|
||||
-- bind (putStrLn (append name " says 3")) (_ :
|
||||
-- pure n))))))
|
||||
|
||||
chatter = name n : bind <|
|
||||
putStrLn (append name " says 1") (_ :
|
||||
bind yield (_ :
|
||||
bind (putStrLn (append name " says 2")) (_ :
|
||||
bind yield (_ :
|
||||
bind (putStrLn (append name " says 3")) (_ :
|
||||
pure n)))))
|
||||
|
||||
|
||||
main = io <|
|
||||
bind (fork (chatter "A" "doneA")) (ha :
|
||||
bind (fork (chatter "B" "doneB")) (hb :
|
||||
bind yield (_ :
|
||||
bind (await ha) (a :
|
||||
bind (await hb) (b :
|
||||
putStrLn (append "Finished: " (append a (append " " b))))))))
|
||||
@@ -1,5 +1,4 @@
|
||||
!import "../lib/base.tri" Lib
|
||||
!import "../lib/list.tri" !Local
|
||||
!import "prelude" !Local
|
||||
|
||||
main = exampleTwo
|
||||
-- Level Order Traversal of a labelled binary tree
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
!import "../lib/patterns.tri" !Local
|
||||
!import "patterns" !Local
|
||||
|
||||
-- We can do conditional pattern matching by providing a list of lists, where
|
||||
-- each sublist contains a boolean expression and a function to return if said
|
||||
|
||||
25
demos/runArboricxBundle.tri
Normal file
25
demos/runArboricxBundle.tri
Normal file
@@ -0,0 +1,25 @@
|
||||
!import "prelude" !Local
|
||||
!import "io" !Local
|
||||
!import "arboricx" !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")
|
||||
@@ -1,5 +1,4 @@
|
||||
!import "../lib/base.tri" !Local
|
||||
!import "../lib/list.tri" !Local
|
||||
!import "prelude" !Local
|
||||
|
||||
main = size size
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
!import "../lib/base.tri" !Local
|
||||
!import "../lib/list.tri" !Local
|
||||
!import "prelude" !Local
|
||||
|
||||
main = toSource not?
|
||||
-- Thanks to intensionality, we can inspect the structure of a given value
|
||||
|
||||
190
demos/viewContracts.tri
Normal file
190
demos/viewContracts.tri
Normal file
@@ -0,0 +1,190 @@
|
||||
!import "prelude" !Local
|
||||
!import "view" !Local
|
||||
|
||||
-- ============================================================================
|
||||
-- View Contracts in tricu
|
||||
-- ============================================================================
|
||||
--
|
||||
-- Verify this guide passes checking with:
|
||||
--
|
||||
-- tricu check demos/viewContracts.tri
|
||||
--
|
||||
-- Expected output:
|
||||
--
|
||||
-- ok
|
||||
--
|
||||
-- This file uses tricu syntax sugar. The lower-level portable View Tree
|
||||
-- form is shown in demos/viewContracts/complete.tri.
|
||||
|
||||
-- ============================================================================
|
||||
-- 1. What's the problem?
|
||||
-- ============================================================================
|
||||
--
|
||||
-- Programs grow by connecting definitions. A common mistake is connecting a
|
||||
-- value with one shape to code that expects another shape:
|
||||
--
|
||||
-- a function expects Bool, but receives String
|
||||
-- a function returns String, but its caller expects Bool
|
||||
-- a list is expected to contain bytes, but contains strings
|
||||
--
|
||||
-- In a large program, those mistakes are often far away from where the bad value
|
||||
-- was first introduced. View Contracts give tricu a portable way to check those
|
||||
-- boundaries.
|
||||
|
||||
-- ============================================================================
|
||||
-- 2. Views: useful built-in shapes
|
||||
-- ============================================================================
|
||||
--
|
||||
-- A View is a description of the shape we expect at a boundary. tricu includes
|
||||
-- built-in Views for common shapes such as:
|
||||
--
|
||||
-- Bool
|
||||
-- String
|
||||
-- Byte
|
||||
-- Unit
|
||||
-- List View
|
||||
-- Maybe View
|
||||
-- Pair View1 View2
|
||||
-- Fn [View1] View2
|
||||
--
|
||||
-- tricu has unconventional but intuitive sugar for annotations:
|
||||
--
|
||||
-- name =@View value
|
||||
-- function argument@View =@ResultView body
|
||||
--
|
||||
-- These examples are ordinary checked source definitions.
|
||||
|
||||
message =@String "hello"
|
||||
|
||||
names =@(List String) [("Ada") ("Grace")]
|
||||
|
||||
chooseFirst left@String right@String =@String left
|
||||
|
||||
stringIdentity =@(Fn [String] String) (x : x)
|
||||
|
||||
-- Uncommenting the below definition demonstrates a plain View mismatch:
|
||||
--
|
||||
-- bad =@Bool "not a Bool"
|
||||
--
|
||||
-- `tricu check` reports that the value is known as String where Bool was
|
||||
-- required.
|
||||
|
||||
-- ============================================================================
|
||||
-- 3. Why don't you just have Types?
|
||||
-- ============================================================================
|
||||
--
|
||||
-- tricu is built on Tree Calculus. A defining feature of Tree Calculus is
|
||||
-- intensionality: programs can inspect and construct program-shaped trees directly.
|
||||
-- That intensional power is useful, but it makes ordinary sound static typing a
|
||||
-- hard fit. A value can be both data and executable structure, and code can make
|
||||
-- decisions based on tree shape in ways a conventional type checker may not be
|
||||
-- able to predict soundly. This is an area of active research, not a settled
|
||||
-- claim that Tree Calculus languages cannot ever have useful typed variants.
|
||||
--
|
||||
-- View Contracts are not advertised as "the type system for tricu". They are
|
||||
-- a practical contract layer: portable metadata plus checker/runtime boundaries
|
||||
-- that catch many real mistakes while leaving the underlying language intact.
|
||||
|
||||
-- For more information about sound typing for Tree Calculus:
|
||||
-- https://github.com/barry-jay-personal/typed_tree_calculus
|
||||
|
||||
-- ============================================================================
|
||||
-- 4. What are the Contracts about, then?
|
||||
-- ============================================================================
|
||||
--
|
||||
-- `List String` tells us that every element is a String. It does not tell us the
|
||||
-- list has at least one element.
|
||||
--
|
||||
-- That matters for functions like `head`. Calling `head` on an empty list is a
|
||||
-- bug. We want to express the stronger requirement:
|
||||
--
|
||||
-- this is a List String, and it is non-empty
|
||||
--
|
||||
-- That is what a guarded View is for.
|
||||
|
||||
-- A guard is ordinary tricu code. It receives the runtime value and returns:
|
||||
--
|
||||
-- guardOk value -- accept the value
|
||||
-- guardFail -- reject the boundary
|
||||
--
|
||||
-- The guard does not write diagnostics. The checked runner reports where the
|
||||
-- failing boundary came from.
|
||||
|
||||
requireNonEmpty = (xs :
|
||||
lazyBool
|
||||
(_ : guardFail)
|
||||
(_ : guardOk xs)
|
||||
(emptyList? xs))
|
||||
|
||||
-- A user-defined View can be parameterized just like an ordinary function.
|
||||
--
|
||||
-- NonEmptyList String
|
||||
--
|
||||
-- means "a List String guarded by requireNonEmpty".
|
||||
|
||||
NonEmptyList elem = viewGuarded (viewList elem) requireNonEmpty
|
||||
|
||||
-- ============================================================================
|
||||
-- 5. Using a custom View in normal annotations
|
||||
-- ============================================================================
|
||||
--
|
||||
-- This value satisfies the custom contract.
|
||||
|
||||
contributors =@(NonEmptyList String) [("Ada") ("Grace")]
|
||||
|
||||
-- This function requires NonEmptyList String before its body can run. In a
|
||||
-- library, this is the kind of contract you would put on an operation like
|
||||
-- `head`: callers must prove the list is non-empty first.
|
||||
|
||||
acceptNames xs@(NonEmptyList String) =@String "accepted non-empty names"
|
||||
|
||||
primaryContributor =@String acceptNames contributors
|
||||
|
||||
-- Uncommenting this definition demonstrates a guarded View failure:
|
||||
--
|
||||
-- nobody =@(NonEmptyList String) []
|
||||
--
|
||||
-- The structure is fine (`[]` is a List String), but the runtime guard rejects
|
||||
-- it because the list is empty.
|
||||
|
||||
-- ============================================================================
|
||||
-- 6. Contracts protect callers too
|
||||
-- ============================================================================
|
||||
--
|
||||
-- Contracts can describe function results as well as arguments. If a function
|
||||
-- promises to return `NonEmptyList String`, checked execution guards that result
|
||||
-- before callers depend on it.
|
||||
|
||||
mkContributors name@String =@(NonEmptyList String) [(name)]
|
||||
|
||||
fromSingleName =@String acceptNames (mkContributors "Evelyn")
|
||||
|
||||
-- Uncommenting this version would fail because the result contract is too
|
||||
-- strong for the implementation:
|
||||
--
|
||||
-- badContributors name@String =@(NonEmptyList String) []
|
||||
|
||||
-- ============================================================================
|
||||
-- 7. Writing your own Views and Contracts
|
||||
-- ============================================================================
|
||||
--
|
||||
-- The pattern is:
|
||||
--
|
||||
-- 1. Start with the closest structural View.
|
||||
-- 2. Write a guard for the runtime fact the structure cannot express.
|
||||
-- 3. Package them with viewGuarded.
|
||||
-- 4. Use the new View in normal annotations.
|
||||
--
|
||||
-- Examples of useful guarded Views:
|
||||
--
|
||||
-- NonEmptyList String
|
||||
-- SortedList Byte
|
||||
-- FixedLengthBytes 32
|
||||
-- ValidUserId
|
||||
-- NonEmptyString
|
||||
--
|
||||
-- Guards are intentionally runtime checks. Use plain Views for ordinary shape
|
||||
-- checking, and guarded Views when a boundary really must enforce a stronger
|
||||
-- invariant.
|
||||
|
||||
main =@String primaryContributor
|
||||
137
demos/viewContracts/README.md
Normal file
137
demos/viewContracts/README.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# View Contract Demos
|
||||
|
||||
These demos exercise the finalized View Contract stack in `lib/view.tri`:
|
||||
portable View Trees/checkable typed-program nodes, structural View flow checks,
|
||||
runtime guarded Views, checked-exec, source annotations, and module-boundary
|
||||
View metadata.
|
||||
|
||||
## End-user guide
|
||||
|
||||
Start here. `demos/viewContracts.tri` is written with normal source annotation
|
||||
sugar and reads as a short guide to View Contracts: motivating structural
|
||||
mismatches, explaining plain Views, noting why this is not a full static type
|
||||
system, and building a custom `NonEmptyList` guarded View.
|
||||
|
||||
```bash
|
||||
tricu check demos/viewContracts.tri
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```text
|
||||
ok
|
||||
```
|
||||
|
||||
## Complete explicit demo
|
||||
|
||||
`demos/viewContracts/complete.tri` shows the same layer from the portable
|
||||
View Tree/checkable-program side. It uses explicit builders such as
|
||||
`typedValue`, `typedRequire`, and `typedApply`, and demonstrates contextual guard
|
||||
diagnostics, observation composition, reachability, and malformed guard output.
|
||||
|
||||
```bash
|
||||
tricu eval demos/viewContracts/complete.tri -f decode
|
||||
```
|
||||
|
||||
## Portable checker self-tests
|
||||
|
||||
Runs the checker self-test suite carried as ordinary `tricu` code.
|
||||
|
||||
```bash
|
||||
tricu eval demos/viewContracts/selfTests.tri -f decode
|
||||
```
|
||||
|
||||
Expected output is a list of `"ok"` strings.
|
||||
|
||||
## Diagnostic rendering
|
||||
|
||||
Shows a strict-mode structural View failure rendered for humans.
|
||||
|
||||
```bash
|
||||
tricu eval demos/viewContracts/diagnostic.tri -f decode
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```text
|
||||
"symbol 162 expected List Bool but got List String"
|
||||
```
|
||||
|
||||
## Stdlib-shaped contracts
|
||||
|
||||
Checks successful higher-order contracts shaped like common stdlib APIs.
|
||||
|
||||
```bash
|
||||
tricu eval demos/viewContracts/stdlibContracts.tri -f decode
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```text
|
||||
["ok", "ok", "ok", "ok", "ok"]
|
||||
```
|
||||
|
||||
These examples are structural View checks, not runtime guarded checks.
|
||||
|
||||
## Frontend emission layer
|
||||
|
||||
`frontendEmission/` documents the portable artifact shape a frontend can emit
|
||||
after parsing/elaboration. The `*.source.txt` files are pseudo-source; the
|
||||
matching `*.emitted.tri` files are explicit typed-program builder output.
|
||||
|
||||
This layer is still instructive because it shows the exact bridge between source
|
||||
syntax and portable View Tree/checkable metadata.
|
||||
|
||||
## Source syntax sugar
|
||||
|
||||
The `sourceSyntax/` demos use ergonomic annotations and the `tricu check`
|
||||
frontend. The frontend lowers annotations to the same typed-program nodes used by
|
||||
the explicit demos above, then executes checked-exec so guarded annotations fail
|
||||
through the portable runner.
|
||||
|
||||
Successful check:
|
||||
|
||||
```bash
|
||||
tricu check demos/viewContracts/sourceSyntax/success.tri
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```text
|
||||
ok
|
||||
```
|
||||
|
||||
Labeled diagnostic check:
|
||||
|
||||
```bash
|
||||
tricu check demos/viewContracts/sourceSyntax/failure.tri
|
||||
```
|
||||
|
||||
Expected first failing diagnostic:
|
||||
|
||||
```text
|
||||
symbol 4 (x) expected Bool but got String
|
||||
```
|
||||
|
||||
If the first definition is fixed or removed, the later application-result
|
||||
failure demonstrates callee-aware labels:
|
||||
|
||||
```text
|
||||
symbol 3 (g application result) expected String but got Bool
|
||||
```
|
||||
|
||||
## Module boundary layer
|
||||
|
||||
`modules/` shows producer-checked module export Views flowing into a consumer
|
||||
check as module-boundary evidence. During auto-build, annotated exports are
|
||||
checked before the module manifest alias is published. Consumers then use the
|
||||
manifest's View Contract metadata as assumptions, while compatibility is still
|
||||
judged by `lib/view.tri`.
|
||||
|
||||
```bash
|
||||
tricu check demos/viewContracts/modules/success.tri
|
||||
# ok
|
||||
|
||||
tricu check demos/viewContracts/modules/failure.tri
|
||||
# symbol 3 (Util.toString application result) expected Bool but got String
|
||||
```
|
||||
119
demos/viewContracts/complete.tri
Normal file
119
demos/viewContracts/complete.tri
Normal file
@@ -0,0 +1,119 @@
|
||||
!import "prelude" !Local
|
||||
!import "view" !Local
|
||||
|
||||
-- Complete explicit View Contract demo.
|
||||
-- Run with: tricu eval demos/viewContracts/complete.tri -f decode
|
||||
--
|
||||
-- This file uses the low-level portable typed-program builders directly. It is
|
||||
-- useful for understanding what source annotations lower to. For the end-user
|
||||
-- guide, see demos/viewContracts.tri.
|
||||
|
||||
requireNonEmpty = (xs :
|
||||
lazyBool
|
||||
(_ : guardFail)
|
||||
(_ : guardOk xs)
|
||||
(emptyList? xs))
|
||||
|
||||
NonEmptyList = (elemView :
|
||||
viewGuarded (viewList elemView) requireNonEmpty)
|
||||
|
||||
checkedResult = (result :
|
||||
matchResult
|
||||
(diag env : renderDiagnostic diag)
|
||||
(exec env :
|
||||
matchResult
|
||||
(runtimeDiag runtimeEnv : renderDiagnostic runtimeDiag)
|
||||
(value runtimeEnv : value)
|
||||
(runChecked exec))
|
||||
result)
|
||||
|
||||
checkedContract = (program :
|
||||
checkedResult (checkTypedProgramWith policyStrict program))
|
||||
|
||||
plainViewFailure =
|
||||
matchResult
|
||||
(diag env : renderDiagnostic diag)
|
||||
(exec env : "unexpected-ok")
|
||||
(checkTypedProgramWith
|
||||
policyStrict
|
||||
(typedProgram
|
||||
0
|
||||
[(typedValue 0 (viewList viewString) [("Ada")])
|
||||
(typedRequire 0 (viewList viewBool) t)]))
|
||||
|
||||
nonEmptyRootSuccess =
|
||||
matchBool
|
||||
"ok"
|
||||
"unexpected-value"
|
||||
(equal?
|
||||
(checkedContract
|
||||
(typedProgram
|
||||
0
|
||||
[(typedValue 0 (NonEmptyList viewString) [("Ada") ("Grace")])]))
|
||||
[("Ada") ("Grace")])
|
||||
|
||||
nonEmptyRootFailure =
|
||||
checkedContract
|
||||
(typedProgram
|
||||
0
|
||||
[(typedValue 0 (viewList viewString) [])
|
||||
(typedRequire 0 (NonEmptyList viewString) [])])
|
||||
|
||||
firstNameSuccess =
|
||||
checkedContract
|
||||
(typedProgram
|
||||
2
|
||||
[(typedValue 0 (viewFn [(NonEmptyList viewString)] viewString) (xs : head xs))
|
||||
(typedValue 1 (viewList viewString) [("Ada") ("Grace")])
|
||||
(typedApply 2 0 1 "Ada")
|
||||
(typedRequire 2 viewString "Ada")])
|
||||
|
||||
firstNameFailure =
|
||||
checkedContract
|
||||
(typedProgram
|
||||
2
|
||||
[(typedValue 0 (viewFn [(NonEmptyList viewString)] viewString) (xs : head xs))
|
||||
(typedValue 1 (viewList viewString) [])
|
||||
(typedApply 2 0 1 t)
|
||||
(typedRequire 2 viewString t)])
|
||||
|
||||
resultGuardFailure =
|
||||
checkedContract
|
||||
(typedProgram
|
||||
2
|
||||
[(typedValue 0 (viewFn [(viewString)] (NonEmptyList viewString)) (name : []))
|
||||
(typedValue 1 viewString "Ada")
|
||||
(typedApply 2 0 1 [])])
|
||||
|
||||
observationComposition =
|
||||
checkedContract
|
||||
(typedProgram
|
||||
0
|
||||
[(typedValue 0 viewString "Ada")
|
||||
(typedRequire 0 (viewGuarded viewString (x : guardOk (append x " Lovelace"))) "Ada")
|
||||
(typedRequire 0 (viewGuarded viewString (x : guardOk (append x "!"))) "Ada")])
|
||||
|
||||
unreachableGuard =
|
||||
checkedContract
|
||||
(typedProgram
|
||||
0
|
||||
[(typedValue 0 viewString "only the root is checked")
|
||||
(typedValue 1 (viewList viewString) [])
|
||||
(typedRequire 1 (NonEmptyList viewString) [])])
|
||||
|
||||
malformedGuard =
|
||||
checkedContract
|
||||
(typedProgram
|
||||
0
|
||||
[(typedValue 0 (viewGuarded viewString (x : record 99 t)) "bad guard")])
|
||||
|
||||
main = [
|
||||
(append "plain View structural failure: " plainViewFailure)
|
||||
(append "NonEmptyList root success: " nonEmptyRootSuccess)
|
||||
(append "NonEmptyList root failure: " nonEmptyRootFailure)
|
||||
(append "NonEmptyList function argument success: " firstNameSuccess)
|
||||
(append "NonEmptyList function argument failure: " firstNameFailure)
|
||||
(append "NonEmptyList function result failure: " resultGuardFailure)
|
||||
(append "guard observations compose: " observationComposition)
|
||||
(append "unreachable guard does not run: " unreachableGuard)
|
||||
(append "malformed guard result: " malformedGuard)]
|
||||
9
demos/viewContracts/diagnostic.tri
Normal file
9
demos/viewContracts/diagnostic.tri
Normal file
@@ -0,0 +1,9 @@
|
||||
!import "prelude" !Local
|
||||
!import "view" !Local
|
||||
!import "views.catalog" !Local
|
||||
|
||||
main =
|
||||
matchResult
|
||||
(diag env : renderDiagnostic diag)
|
||||
(env rest : "ok")
|
||||
(checkTypedProgramWith policyStrict listMapWrongListArgContract)
|
||||
116
demos/viewContracts/frontendEmission/README.md
Normal file
116
demos/viewContracts/frontendEmission/README.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# Frontend Emission Demos
|
||||
|
||||
These examples show the layer between source-level View annotations and the
|
||||
portable View Contract checker.
|
||||
|
||||
Each `*.source.txt` file is pseudo-source: it is not parsed by `tricu`. It shows
|
||||
the information a frontend has after parsing/elaboration.
|
||||
|
||||
Each matching `*.emitted.tri` file shows the lowered typed-program metadata that
|
||||
a frontend can emit today. A successful check returns checked-exec; these demos
|
||||
focus on structural Views, so they report `"ok"` as soon as metadata checking
|
||||
succeeds. Guarded programs should run the returned checked-exec with
|
||||
`runChecked`, as shown in `demos/viewContracts.tri` and by `tricu check`.
|
||||
|
||||
## Successful map use
|
||||
|
||||
Pseudo-source:
|
||||
|
||||
```text
|
||||
map : Fn [Fn [Bool] String, List Bool] (List String)
|
||||
f : Fn [Bool] String
|
||||
xs : List Bool
|
||||
|
||||
partial = map f
|
||||
out = partial xs
|
||||
|
||||
require out : List String
|
||||
```
|
||||
|
||||
Run the emitted artifact:
|
||||
|
||||
```bash
|
||||
tricu eval demos/viewContracts/frontendEmission/map-success.emitted.tri -f decode
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```text
|
||||
"ok"
|
||||
```
|
||||
|
||||
## Wrong list argument
|
||||
|
||||
Pseudo-source:
|
||||
|
||||
```text
|
||||
map : Fn [Fn [Bool] String, List Bool] (List String)
|
||||
f : Fn [Bool] String
|
||||
xs : List String
|
||||
|
||||
partial = map f
|
||||
out = partial xs
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
tricu eval demos/viewContracts/frontendEmission/map-wrong-list.emitted.tri -f decode
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```text
|
||||
"symbol 162 expected List Bool but got List String"
|
||||
```
|
||||
|
||||
## Wrong filter predicate
|
||||
|
||||
Pseudo-source:
|
||||
|
||||
```text
|
||||
filter : Fn [Fn [Bool] Bool, List Bool] (List Bool)
|
||||
pred : Fn [Bool] String
|
||||
xs : List Bool
|
||||
|
||||
partial = filter pred
|
||||
out = partial xs
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
tricu eval demos/viewContracts/frontendEmission/filter-wrong-predicate.emitted.tri -f decode
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```text
|
||||
"symbol 181 expected Fn [Bool] Bool but got Fn [Bool] String"
|
||||
```
|
||||
|
||||
## Lowering shape
|
||||
|
||||
A frontend does not need to expose `tricu` syntax internally. It only needs to
|
||||
emit portable typed-program nodes:
|
||||
|
||||
```text
|
||||
typedValue symbol view term
|
||||
typedApply out callee arg term
|
||||
typedRequire symbol view term
|
||||
```
|
||||
|
||||
The source-level flow:
|
||||
|
||||
```text
|
||||
out = map f xs
|
||||
```
|
||||
|
||||
lowers to curried Tree Calculus application nodes:
|
||||
|
||||
```text
|
||||
typedApply partial map f partialTerm
|
||||
typedApply out partial xs outTerm
|
||||
```
|
||||
|
||||
Function Views drive argument checking and result inference.
|
||||
@@ -0,0 +1,17 @@
|
||||
!import "prelude" !Local
|
||||
!import "view" !Local
|
||||
!import "views.catalog" !Local
|
||||
|
||||
-- Lowering of filter-wrong-predicate.source.txt to portable typed-program metadata.
|
||||
-- Symbols:
|
||||
-- 180 filter
|
||||
-- 181 pred
|
||||
-- 182 partial
|
||||
|
||||
program = listFilterWrongPredicateContract
|
||||
|
||||
main =
|
||||
matchResult
|
||||
(diag env : renderDiagnostic diag)
|
||||
(env rest : "unexpected-ok")
|
||||
(checkTypedProgramWith policyStrict program)
|
||||
20
demos/viewContracts/frontendEmission/map-success.emitted.tri
Normal file
20
demos/viewContracts/frontendEmission/map-success.emitted.tri
Normal file
@@ -0,0 +1,20 @@
|
||||
!import "prelude" !Local
|
||||
!import "view" !Local
|
||||
!import "views.catalog" !Local
|
||||
|
||||
-- Lowering of map-success.source.txt to portable typed-program metadata.
|
||||
-- Symbols:
|
||||
-- 100 map
|
||||
-- 101 f
|
||||
-- 102 xs
|
||||
-- 103 partial
|
||||
-- 104 out
|
||||
|
||||
program =
|
||||
listMapUseContract viewBool viewString 100 101 102 103 104
|
||||
|
||||
main =
|
||||
matchResult
|
||||
(diag env : renderDiagnostic diag)
|
||||
(env rest : "ok")
|
||||
(checkTypedProgramWith policyStrict program)
|
||||
@@ -0,0 +1,19 @@
|
||||
!import "prelude" !Local
|
||||
!import "view" !Local
|
||||
!import "views.catalog" !Local
|
||||
|
||||
-- Lowering of map-wrong-list.source.txt to portable typed-program metadata.
|
||||
-- Symbols:
|
||||
-- 160 map
|
||||
-- 161 f
|
||||
-- 162 xs
|
||||
-- 163 partial
|
||||
-- 164 out
|
||||
|
||||
program = listMapWrongListArgContract
|
||||
|
||||
main =
|
||||
matchResult
|
||||
(diag env : renderDiagnostic diag)
|
||||
(env rest : "unexpected-ok")
|
||||
(checkTypedProgramWith policyStrict program)
|
||||
30
demos/viewContracts/io-continuation.tri
Normal file
30
demos/viewContracts/io-continuation.tri
Normal file
@@ -0,0 +1,30 @@
|
||||
!import "prelude" !Local
|
||||
!import "io" !Local
|
||||
!import "view" !Local
|
||||
|
||||
-- View Contracts inside IO continuations
|
||||
-- Run with:
|
||||
--
|
||||
-- tricu eval demos/viewContracts/io-continuation.tri --io -f decode
|
||||
--
|
||||
-- Checked IO evaluation instruments continuation bodies once from source
|
||||
-- annotations. The IO runtime still executes ordinary interaction-tree actions;
|
||||
-- the returned continuations already contain the checked-exec guard boundaries.
|
||||
|
||||
requireNonEmpty = (xs :
|
||||
lazyBool
|
||||
(_ : guardFail)
|
||||
(_ : guardOk xs)
|
||||
(emptyList? xs))
|
||||
|
||||
NonEmptyList elem = viewGuarded (viewList elem) requireNonEmpty
|
||||
|
||||
acceptNames xs@(NonEmptyList String) =@String "accepted"
|
||||
|
||||
useHandler handler@(Fn [(NonEmptyList String)] String) xs@(List String) =@String
|
||||
handler xs
|
||||
|
||||
-- The IO action yields an empty list. The higher-order boundary requires a
|
||||
-- handler that accepts NonEmptyList String, so the continuation-internal pure
|
||||
-- call fails before returning the next IO value.
|
||||
main = io (bind (pure []) (xs : pure (useHandler acceptNames xs)))
|
||||
51
demos/viewContracts/io.tri
Normal file
51
demos/viewContracts/io.tri
Normal file
@@ -0,0 +1,51 @@
|
||||
!import "prelude" !Local
|
||||
!import "io" !Local
|
||||
!import "view" !Local
|
||||
|
||||
-- View Contracts + IO interaction trees
|
||||
-- Run with:
|
||||
--
|
||||
-- tricu eval demos/viewContracts/io.tri --io -f decode
|
||||
--
|
||||
-- The IO runtime expects the top-level value to be an interaction tree wrapped
|
||||
-- by the `io` sentinel:
|
||||
--
|
||||
-- pair "tricuIO" (pair version action)
|
||||
--
|
||||
-- View Contracts can validate that boundary before the IO driver starts. The IO
|
||||
-- value is still just an interaction tree; this demo only checks how it was
|
||||
-- exposed.
|
||||
|
||||
ioSentinel? = (value :
|
||||
and?
|
||||
(equal? (fst value) "tricuIO")
|
||||
(equal? (fst (snd value)) 1))
|
||||
|
||||
requireIO = (value :
|
||||
lazyBool
|
||||
(_ : guardOk value)
|
||||
(_ : guardFail)
|
||||
(ioSentinel? value))
|
||||
|
||||
-- A first useful IO View is intentionally shallow:
|
||||
--
|
||||
-- viewAny -- accept any payload structurally
|
||||
-- requireIO sentinel -- require the top-level IO wrapper at runtime
|
||||
--
|
||||
-- This does not prove every future continuation step is well-formed. It proves
|
||||
-- the checked program exposes an IO interaction tree to the host driver.
|
||||
viewIO = viewGuarded viewAny requireIO
|
||||
|
||||
checkedIO = (action :
|
||||
matchResult
|
||||
(diag env : io (pure (renderDiagnostic diag)))
|
||||
(exec env :
|
||||
matchResult
|
||||
(runtimeDiag runtimeEnv : io (pure (renderDiagnostic runtimeDiag)))
|
||||
(value runtimeEnv : value)
|
||||
(runChecked exec))
|
||||
(checkTypedProgramWith
|
||||
policyStrict
|
||||
(typedProgram 0 [(typedValue 0 viewIO action)])))
|
||||
|
||||
main = checkedIO (io (pure "checked interaction tree"))
|
||||
17
demos/viewContracts/modules/README.md
Normal file
17
demos/viewContracts/modules/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Module View Contract demo
|
||||
|
||||
This demo shows producer-checked module export Views flowing into a consumer
|
||||
check as trusted View Contract evidence.
|
||||
|
||||
```sh
|
||||
tricu check demos/viewContracts/modules/success.tri
|
||||
# ok
|
||||
|
||||
tricu check demos/viewContracts/modules/failure.tri
|
||||
# symbol 3 (Util.toString application result) expected Bool but got String
|
||||
```
|
||||
|
||||
`util.tri` is a local workspace module. During auto-build, its annotated exports
|
||||
are checked before the module manifest alias is published. The consumer then
|
||||
uses the manifest's View Contract metadata and View Tree export artifacts as
|
||||
module-boundary assumptions; compatibility is still judged by `lib/view.tri`.
|
||||
3
demos/viewContracts/modules/failure.tri
Normal file
3
demos/viewContracts/modules/failure.tri
Normal file
@@ -0,0 +1,3 @@
|
||||
!import "vc.demo.util" Util
|
||||
|
||||
foo x@Bool =@Bool Util.toString x
|
||||
3
demos/viewContracts/modules/success.tri
Normal file
3
demos/viewContracts/modules/success.tri
Normal file
@@ -0,0 +1,3 @@
|
||||
!import "vc.demo.util" Util
|
||||
|
||||
foo x@Bool =@Bool Util.id x
|
||||
1
demos/viewContracts/modules/tricu.workspace
Normal file
1
demos/viewContracts/modules/tricu.workspace
Normal file
@@ -0,0 +1 @@
|
||||
module vc.demo.util = util.tri
|
||||
2
demos/viewContracts/modules/util.tri
Normal file
2
demos/viewContracts/modules/util.tri
Normal file
@@ -0,0 +1,2 @@
|
||||
id x@Bool =@Bool x
|
||||
toString x@Bool =@String "ok"
|
||||
3
demos/viewContracts/selfTests.tri
Normal file
3
demos/viewContracts/selfTests.tri
Normal file
@@ -0,0 +1,3 @@
|
||||
!import "views.catalog" !Local
|
||||
|
||||
main = viewCatalogSelfTests
|
||||
9
demos/viewContracts/sourceSyntax/failure.tri
Normal file
9
demos/viewContracts/sourceSyntax/failure.tri
Normal file
@@ -0,0 +1,9 @@
|
||||
-- Source-level View Contract diagnostic demo.
|
||||
-- Run with: tricu check demos/viewContracts/sourceSyntax/failure.tri
|
||||
|
||||
makeBool x@String =@Bool x
|
||||
|
||||
xs =@(List String) [(g "hi")]
|
||||
g y@String =@Bool y
|
||||
|
||||
main = "if you're seeing this instead of an error, you ran the file unchecked"
|
||||
10
demos/viewContracts/sourceSyntax/success.tri
Normal file
10
demos/viewContracts/sourceSyntax/success.tri
Normal file
@@ -0,0 +1,10 @@
|
||||
-- Source-level View Contract syntax sugar demo.
|
||||
-- Run with: tricu check demos/viewContracts/sourceSyntax/success.tri
|
||||
|
||||
message =@String "hello"
|
||||
|
||||
boxedMessages =@(Maybe (List String)) just [(message) ("world")]
|
||||
|
||||
chooseFirst x@String y@Byte =@String x
|
||||
|
||||
fromLambda =@(Fn [String] String) (x : x)
|
||||
10
demos/viewContracts/stdlibContracts.tri
Normal file
10
demos/viewContracts/stdlibContracts.tri
Normal file
@@ -0,0 +1,10 @@
|
||||
!import "prelude" !Local
|
||||
!import "view" !Local
|
||||
!import "views.catalog" !Local
|
||||
|
||||
main = [
|
||||
(typedContractCheck listMapBoolStringContract)
|
||||
(typedContractCheck headMaybeBoolContract)
|
||||
(typedContractCheck listFilterBoolContract)
|
||||
(typedContractCheck listFoldStringBoolContract)
|
||||
(typedContractCheck listMapMaybeBoolStringContract)]
|
||||
@@ -1,117 +1,119 @@
|
||||
# Arboricx Portable Bundle Format Specification
|
||||
|
||||
**Version:** 0.1
|
||||
**Status:** Exploratory
|
||||
**Author:** A range of slopmachines guided by James Eversole
|
||||
**Human Review Status:** 5 minute scan-through - this is an evolving and malleable document
|
||||
**Version:** 1.1 (Indexed)
|
||||
|
||||
The Arboricx Portable Bundle is a self-contained, content-addressed binary format for distributing Tree Calculus programs and their associated Merkle DAGs. It provides:
|
||||
**Status:** Stable
|
||||
|
||||
- A fixed binary container with header, section directory, and typed sections
|
||||
- A language-neutral Merkle node layer for content-addressed tree values
|
||||
- A fixed-order binary manifest for semantic metadata, exports, and optional extensions
|
||||
**Author:** Slopmachines guided by James Eversole
|
||||
|
||||
The Arboricx Portable Bundle is a self-contained binary format for distributing Tree Calculus programs. It uses topological indexing instead of cryptographic hashing for node identity, making it writable from pure Tree Calculus and verifiable via structural inspection.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Top-Level Container Layout](#1-top-level-container-layout)
|
||||
2. [Header](#2-header)
|
||||
3. [Section Directory](#3-section-directory)
|
||||
4. [Section: Manifest (type 1)](#4-section-manifest-type-1)
|
||||
5. [Section: Nodes (type 2)](#5-section-nodes-type-2)
|
||||
6. [Merkle Node Payload Format](#6-merkle-node-payload-format)
|
||||
7. [Merkle Hash Computation](#7-merkle-hash-computation)
|
||||
1. [Design Principles](#1-design-principles)
|
||||
2. [Top-Level Container Layout](#2-top-level-container-layout)
|
||||
3. [Header](#3-header)
|
||||
4. [Section Directory](#4-section-directory)
|
||||
5. [Section: Manifest (type 1)](#5-section-manifest-type-1)
|
||||
6. [Section: Nodes (type 2)](#6-section-nodes-type-2)
|
||||
7. [Node Payload Format](#7-node-payload-format)
|
||||
8. [Tree Calculus Reduction Semantics](#8-tree-calculus-reduction-semantics)
|
||||
9. [Binary Primitives](#9-binary-primitives)
|
||||
10. [Bundle Verification](#10-bundle-verification)
|
||||
11. [Known Section Types](#11-known-section-types)
|
||||
11. [Canonicalization](#11-canonicalization)
|
||||
12. [Known Section Types](#12-known-section-types)
|
||||
|
||||
---
|
||||
|
||||
## 1. Top-Level Container Layout
|
||||
## 1. Design Principles
|
||||
|
||||
An Arboricx bundle is a flat binary blob with the following layout:
|
||||
- **No cryptographic primitives required.** Node identity is topological (array index), not a SHA-256 hash.
|
||||
- **Self-contained.** A bundle includes all nodes reachable from its exports. No external references.
|
||||
- **Deterministic.** Canonical bundles produce byte-identical output for identical input terms.
|
||||
- **Small.** ~5 bytes per node entry (length + payload) versus ~36 bytes in hash-based formats.
|
||||
- **Verifiable via structure.** Bounds checking and acyclicity verification replace hash recomputation.
|
||||
|
||||
Global artifact identity (for registries, lockfiles, or content-addressed caches) is achieved by hashing the complete canonical bundle file externally. The bundle format itself knows nothing about this hash.
|
||||
|
||||
---
|
||||
|
||||
## 2. Top-Level Container Layout
|
||||
|
||||
```
|
||||
+------------------+------------------+------------------+------------------+
|
||||
| Header | Section Directory| Manifest Section | Nodes Section |
|
||||
| (32 bytes) | (N × 60 bytes) | (variable) | (variable) |
|
||||
| (32 bytes) | (N × 32 bytes) | (variable) | (variable) |
|
||||
+------------------+------------------+------------------+------------------+
|
||||
```
|
||||
|
||||
The container uses **big-endian** byte order for all multi-byte integers.
|
||||
Total bundle size = 32 + (sectionCount × 32) + manifestSize + nodesSize
|
||||
|
||||
Total bundle size = 32 + (sectionCount × 60) + manifestSize + nodesSize
|
||||
All multi-byte integers use **big-endian** byte order.
|
||||
|
||||
---
|
||||
|
||||
## 2. Header
|
||||
## 3. Header
|
||||
|
||||
| Offset | Size | Field | Description |
|
||||
|--------|------|-------|-------------|
|
||||
| 0 | 8 bytes | Magic | ASCII `"ARBORICX"` (`0x41 0x52 0x42 0x4F 0x52 0x49 0x43 0x58`) |
|
||||
| 0 | 8 bytes | Magic | ASCII `"ARBORICX"` |
|
||||
| 8 | 2 bytes | Major version | `u16` BE. Currently `1` |
|
||||
| 10 | 2 bytes | Minor version | `u16` BE. Currently `0` |
|
||||
| 12 | 4 bytes | Section count | `u32` BE. Number of entries in the section directory |
|
||||
| 16 | 8 bytes | Flags | `u64` BE. Reserved; currently all zeros |
|
||||
| 24 | 8 bytes | Directory offset | `u64` BE. Byte offset from the start of the bundle to the section directory |
|
||||
|
||||
**Constraints:**
|
||||
- Major version must be `1`. Bundles with unsupported major versions are rejected.
|
||||
- The directory offset must point to a valid location within the bundle.
|
||||
- The directory offset is always `32` for bundles with the current layout (header immediately followed by the directory).
|
||||
| 24 | 8 bytes | Directory offset | `u64` BE. Byte offset to the section directory (always `32`) |
|
||||
|
||||
---
|
||||
|
||||
## 3. Section Directory
|
||||
## 4. Section Directory
|
||||
|
||||
The section directory is an array of `N` entries, where `N` is the section count from the header. Each entry is exactly **60 bytes**.
|
||||
Array of `N` entries, each exactly **32 bytes**.
|
||||
|
||||
| Offset (within entry) | Size | Field | Description |
|
||||
|----------------------|------|-------|-------------|
|
||||
| 0 | 4 bytes | Type | `u32` BE. Section type identifier (see [Known Section Types](#11-known-section-types)) |
|
||||
| 0 | 4 bytes | Type | `u32` BE. Section type identifier |
|
||||
| 4 | 2 bytes | Version | `u16` BE. Section-specific version |
|
||||
| 6 | 2 bytes | Flags | `u16` BE. Bit flags: bit 0 (`0x0001`) = critical section |
|
||||
| 8 | 2 bytes | Compression | `u16` BE. Compression codec (currently only `0` = none) |
|
||||
| 10 | 2 bytes | Digest algorithm | `u16` BE. Hash algorithm (currently only `1` = SHA-256) |
|
||||
| 12 | 8 bytes | Offset | `u64` BE. Byte offset from the start of the bundle to the section data |
|
||||
| 20 | 8 bytes | Length | `u64` BE. Length of the section data in bytes |
|
||||
| 28 | 32 bytes | SHA-256 digest | Raw digest of the section data |
|
||||
| 6 | 2 bytes | Flags | `u16` BE. Bit 0 (`0x0001`) = critical section |
|
||||
| 8 | 2 bytes | Compression | `u16` BE. `0` = none (currently the only value) |
|
||||
| 10 | 2 bytes | Reserved | `u16` BE. Padding; must be zero |
|
||||
| 12 | 8 bytes | Offset | `u64` BE. Byte offset from bundle start to section data |
|
||||
| 20 | 8 bytes | Length | `u64` BE. Length of section data in bytes |
|
||||
| 28 | 4 bytes | Reserved | Padding; must be zero |
|
||||
|
||||
**Verification:**
|
||||
- Unknown critical sections (flags & `0x0001`) are rejected.
|
||||
- Unknown critical sections are rejected.
|
||||
- Compression must be `0` (none).
|
||||
- Digest algorithm must be `1` (SHA-256).
|
||||
- The SHA-256 digest in the directory entry must match `SHA256(section_data)`.
|
||||
- Reserved fields must be zero.
|
||||
|
||||
**Note:** No per-section digest is stored. Integrity is verified at the distribution layer (e.g. SHA-256 of the complete bundle file) rather than inside the container.
|
||||
|
||||
---
|
||||
|
||||
## 4. Section: Manifest (type 1)
|
||||
## 5. Section: Manifest (type 1)
|
||||
|
||||
The manifest is a binary encoding of bundle metadata. It uses a **fixed-order core** layout followed by an optional **TLV tail** for extensibility.
|
||||
|
||||
### 4.1 Format
|
||||
Binary encoding of bundle metadata. Fixed-order core layout followed by optional TLV tail.
|
||||
|
||||
```
|
||||
Manifest =
|
||||
magic 8 bytes "ARBMNFST"
|
||||
major u16 BE Manifest major version (1)
|
||||
minor u16 BE Manifest minor version (0)
|
||||
minor u16 BE Manifest minor version (1)
|
||||
|
||||
schema string Length-prefixed UTF-8 text
|
||||
bundleType string Length-prefixed UTF-8 text
|
||||
schema string "arboricx.bundle.manifest.v1"
|
||||
bundleType string "tree-calculus-executable-object"
|
||||
|
||||
treeCalculus string Length-prefixed UTF-8 text
|
||||
treeHashAlgorithm string Length-prefixed UTF-8 text
|
||||
treeHashDomain string Length-prefixed UTF-8 text
|
||||
treeNodePayload string Length-prefixed UTF-8 text
|
||||
treeCalculus string "tree-calculus.v1"
|
||||
treeHashAlgorithm string "indexed"
|
||||
treeHashDomain string "arboricx.indexed.node.v1"
|
||||
treeNodePayload string "arboricx.indexed.payload.v1"
|
||||
|
||||
runtimeSemantics string Length-prefixed UTF-8 text
|
||||
runtimeEvaluation string Length-prefixed UTF-8 text
|
||||
runtimeAbi string Length-prefixed UTF-8 text
|
||||
capabilityCount u32 BE Number of capability strings
|
||||
capabilities string[] Array of length-prefixed UTF-8 capability strings
|
||||
runtimeSemantics string "tree-calculus.v1"
|
||||
runtimeEvaluation string "normal-order"
|
||||
runtimeAbi string "arboricx.abi.tree.v1"
|
||||
capabilityCount u32 BE Number of capability strings (currently 0)
|
||||
capabilities string[] Array of length-prefixed UTF-8 strings
|
||||
|
||||
closure u8 0 = complete, 1 = partial
|
||||
closure u8 0 = complete
|
||||
rootCount u32 BE Number of root entries
|
||||
roots Root[] Array of root entries
|
||||
exportCount u32 BE Number of export entries
|
||||
@@ -119,93 +121,76 @@ Manifest =
|
||||
|
||||
metadataFieldCount u32 BE Number of metadata TLV entries
|
||||
metadataFields TLV[] Metadata tag-value entries
|
||||
extensionFieldCount u32 BE Number of extension TLV entries
|
||||
extensionFields TLV[] Extension tag-value entries (skipped by parsers)
|
||||
extensionFieldCount u32 BE Number of extension TLV entries (currently 0)
|
||||
extensionFields TLV[] Extension entries (skipped by parsers)
|
||||
```
|
||||
|
||||
**Trailing bytes after the manifest must be zero** (no leftover data).
|
||||
|
||||
### 4.2 String Format
|
||||
|
||||
Every `string` field uses the same encoding:
|
||||
### String Format
|
||||
|
||||
```
|
||||
string =
|
||||
length u32 BE Number of UTF-8 bytes in the string (not the number of characters)
|
||||
bytes byte[length] UTF-8 encoded string content
|
||||
length u32 BE Number of UTF-8 bytes
|
||||
bytes byte[length] UTF-8 content
|
||||
```
|
||||
|
||||
The length field carries the byte count, so parsers can skip strings without decoding UTF-8.
|
||||
|
||||
### 4.3 Root Entry
|
||||
### Root Entry
|
||||
|
||||
```
|
||||
Root =
|
||||
hash 32 bytes Raw SHA-256 hash of the Merkle node
|
||||
role string Length-prefixed UTF-8 text ("default" for the first root, "root" for others)
|
||||
index u32 BE Node index into the nodes section
|
||||
role string Length-prefixed UTF-8 ("default" for first root, "root" for others)
|
||||
```
|
||||
|
||||
The hash is stored as **raw bytes** (not hex-encoded). It corresponds to the Merkle hash of the node.
|
||||
|
||||
### 4.4 Export Entry
|
||||
### Export Entry
|
||||
|
||||
```
|
||||
Export =
|
||||
name string Length-prefixed UTF-8 text (export identifier)
|
||||
root 32 bytes Raw SHA-256 hash of the Merkle node
|
||||
kind string Length-prefixed UTF-8 text (currently "term")
|
||||
abi string Length-prefixed UTF-8 text (ABI string)
|
||||
name string Length-prefixed UTF-8 export identifier
|
||||
root u32 BE Node index into the nodes section
|
||||
kind string Length-prefixed UTF-8 (currently "term")
|
||||
abi string Length-prefixed UTF-8 ABI string
|
||||
```
|
||||
|
||||
### 4.5 TLV Entry
|
||||
### TLV Entry
|
||||
|
||||
```
|
||||
TLV =
|
||||
tag u16 BE Tag identifier (type)
|
||||
length u32 BE Number of bytes in the value
|
||||
value byte[length] Raw bytes
|
||||
tag u16 BE Tag identifier
|
||||
length u32 BE Value length in bytes
|
||||
value byte[length]
|
||||
```
|
||||
|
||||
TLV entries support variable-length values and are skippable by parsers that do not recognize a tag: read the `u32` length and advance by `2 + 4 + length` bytes.
|
||||
|
||||
### 4.6 Metadata Tags
|
||||
### Metadata Tags
|
||||
|
||||
| Tag | Name | Value |
|
||||
|-----|------|-------|
|
||||
| 1 | package | UTF-8 text: package name |
|
||||
| 2 | version | UTF-8 text: version string |
|
||||
| 3 | description | UTF-8 text: description |
|
||||
| 4 | license | UTF-8 text: license identifier or text |
|
||||
| 5 | createdBy | UTF-8 text: creator identifier |
|
||||
| 1 | package | UTF-8 text |
|
||||
| 2 | version | UTF-8 text |
|
||||
| 3 | description | UTF-8 text |
|
||||
| 4 | license | UTF-8 text |
|
||||
| 5 | createdBy | UTF-8 text |
|
||||
|
||||
Unknown metadata tags are ignored. Unknown extension tags are skipped by length.
|
||||
|
||||
### 4.7 Semantic Constraints
|
||||
|
||||
A valid bundle manifest must satisfy:
|
||||
### Semantic Constraints
|
||||
|
||||
| Constraint | Value |
|
||||
|-----------|-------|
|
||||
| `schema` | `"arboricx.bundle.manifest.v1"` |
|
||||
| `bundleType` | `"tree-calculus-executable-object"` |
|
||||
| `treeCalculus` | `"tree-calculus.v1"` |
|
||||
| `treeHashAlgorithm` | `"sha256"` |
|
||||
| `treeHashDomain` | `"arboricx.merkle.node.v1"` |
|
||||
| `treeNodePayload` | `"arboricx.merkle.payload.v1"` |
|
||||
| `treeHashAlgorithm` | `"indexed"` |
|
||||
| `treeHashDomain` | `"arboricx.indexed.node.v1"` |
|
||||
| `treeNodePayload` | `"arboricx.indexed.payload.v1"` |
|
||||
| `runtimeSemantics` | `"tree-calculus.v1"` |
|
||||
| `runtimeAbi` | `"arboricx.abi.tree.v1"` |
|
||||
| `runtimeCapabilities` | Empty array |
|
||||
| `closure` | `0` (complete) |
|
||||
| `rootCount` | At least 1 |
|
||||
| `exportCount` | At least 1 |
|
||||
| Export names | Non-empty |
|
||||
| Export roots | Non-empty (32 bytes each) |
|
||||
|
||||
---
|
||||
|
||||
## 5. Section: Nodes (type 2)
|
||||
|
||||
The nodes section contains all Merkle DAG nodes referenced by the manifest. It is a sequence of node entries preceded by a count.
|
||||
## 6. Section: Nodes (type 2)
|
||||
|
||||
```
|
||||
NodesSection =
|
||||
@@ -213,22 +198,21 @@ NodesSection =
|
||||
entries NodeEntry[]
|
||||
```
|
||||
|
||||
Each node entry:
|
||||
### Node Entry
|
||||
|
||||
```
|
||||
NodeEntry =
|
||||
hash 32 bytes Raw SHA-256 hash of this node
|
||||
payloadLen u32 BE Length of the payload in bytes
|
||||
payload byte[payloadLen] Node payload (see Section 6)
|
||||
payloadLen u32 BE Length of payload in bytes
|
||||
payload byte[payloadLen]
|
||||
```
|
||||
|
||||
The node count is `u64` to support large bundles. Entries are stored in the order produced by the exporter (typically sorted by hash for determinism).
|
||||
There is **no hash field**. The node is identified solely by its position in the array.
|
||||
|
||||
---
|
||||
|
||||
## 6. Merkle Node Payload Format
|
||||
## 7. Node Payload Format
|
||||
|
||||
Each node in the Merkle DAG is one of three types. The payload is a single byte type tag followed by hash references:
|
||||
Child references are `u32` big-endian indices into the node array. The array **must** be topologically sorted: every child index must be strictly less than the entry's own position.
|
||||
|
||||
### Leaf
|
||||
|
||||
@@ -236,152 +220,116 @@ Each node in the Merkle DAG is one of three types. The payload is a single byte
|
||||
Payload = 0x00
|
||||
```
|
||||
|
||||
A leaf has no children. The payload is exactly 1 byte.
|
||||
Exactly 1 byte.
|
||||
|
||||
### Stem
|
||||
|
||||
```
|
||||
Payload = 0x01 || child_hash (32 bytes raw)
|
||||
Payload = 0x01 || child_index (u32 BE)
|
||||
```
|
||||
|
||||
A stem has exactly one child. The payload is 33 bytes.
|
||||
Exactly 5 bytes.
|
||||
|
||||
### Fork
|
||||
|
||||
```
|
||||
Payload = 0x02 || left_hash (32 bytes raw) || right_hash (32 bytes raw)
|
||||
Payload = 0x02 || left_index (u32 BE) || right_index (u32 BE)
|
||||
```
|
||||
|
||||
A fork has exactly two children. The payload is 65 bytes.
|
||||
|
||||
**Validation:**
|
||||
- Leaf payloads must be exactly 1 byte (`0x00`).
|
||||
- Stem payloads must be exactly 33 bytes.
|
||||
- Fork payloads must be exactly 65 bytes.
|
||||
- Unknown type bytes are rejected.
|
||||
|
||||
---
|
||||
|
||||
## 7. Merkle Hash Computation
|
||||
|
||||
Each node is identified by a SHA-256 hash of its canonical payload:
|
||||
|
||||
```
|
||||
hash = SHA256( domain_tag || 0x00 || payload )
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
| Component | Value |
|
||||
|-----------|-------|
|
||||
| `domain_tag` | `"arboricx.merkle.node.v1"` as UTF-8 bytes |
|
||||
| Separator | `0x00` (one zero byte) |
|
||||
| `payload` | The node's canonical serialization from Section 6 |
|
||||
|
||||
**Examples:**
|
||||
|
||||
- **Leaf:** `SHA256("arboricx.merkle.node.v1" || 0x00 || 0x00)`
|
||||
- **Stem:** `SHA256("arboricx.merkle.node.v1" || 0x00 || 0x01 || child_hash_bytes)`
|
||||
- **Fork:** `SHA256("arboricx.merkle.node.v1" || 0x00 || 0x02 || left_hash_bytes || right_hash_bytes)`
|
||||
|
||||
The resulting SHA-256 hash is stored as a hex-encoded string in the manifest (64 hex characters). Within the nodes section, it is stored as raw bytes.
|
||||
Exactly 9 bytes.
|
||||
|
||||
---
|
||||
|
||||
## 8. Tree Calculus Reduction Semantics
|
||||
|
||||
The bundle represents a **Tree Calculus** term as a Merkle DAG. The reduction rules are:
|
||||
|
||||
### Apply Rules
|
||||
The bundle represents a **Tree Calculus** term. The reduction rules are:
|
||||
|
||||
```
|
||||
apply(Fork(Leaf, a), _) = a
|
||||
apply(Fork(Stem(a), b), c) = apply(apply(a, c), apply(b, c))
|
||||
apply(Fork(Fork, _, _), Leaf) = left of inner Fork
|
||||
apply(Fork(Fork, _, _), Stem) = right of inner Fork
|
||||
apply(Fork(Fork, _, _), Fork) = apply(apply(c, u), v) where c = Fork(u, v)
|
||||
apply(Leaf, b) = Stem(b)
|
||||
apply(Stem(a), b) = Fork(a, b)
|
||||
The t operator is left associative.
|
||||
1. t t a b -> a
|
||||
2. t (t a) b c -> a c (b c)
|
||||
3a. t (t a b) c t -> a
|
||||
3b. t (t a b) c (t u) -> b u
|
||||
3c. t (t a b) c (t u v) -> c u v
|
||||
```
|
||||
|
||||
### Internal Representation
|
||||
|
||||
In the reduction engine, Fork nodes use a `[right, left]` (stack) ordering:
|
||||
- `Fork = [right_child, left_child]`
|
||||
- `Stem = [child]`
|
||||
- `Leaf = []`
|
||||
|
||||
This ordering supports stack-based reduction: pop two terms, apply, push results back.
|
||||
|
||||
### Closure
|
||||
|
||||
The bundle declares `closure = "complete"`, meaning all nodes reachable from export roots are present in the nodes section. No external references exist.
|
||||
**Closure:** The bundle declares `closure = "complete"`, meaning all nodes reachable from export roots are present in the nodes section. No external references exist.
|
||||
|
||||
---
|
||||
|
||||
## 9. Binary Primitives
|
||||
|
||||
All multi-byte integers use **big-endian** byte order.
|
||||
### u8
|
||||
|
||||
Single byte, value `0-255`.
|
||||
|
||||
### u16 (2 bytes)
|
||||
|
||||
```
|
||||
byte[0] | byte[1]
|
||||
value = (byte[0] << 8) | byte[1]
|
||||
```
|
||||
|
||||
### u32 (4 bytes)
|
||||
|
||||
```
|
||||
byte[0] | byte[1] | byte[2] | byte[3]
|
||||
value = (byte[0] << 24) | (byte[1] << 16) | (byte[2] << 8) | byte[3]
|
||||
```
|
||||
|
||||
### u64 (8 bytes)
|
||||
|
||||
```
|
||||
byte[0] ... byte[7]
|
||||
value = (byte[0] << 56) | ... | byte[7]
|
||||
```
|
||||
|
||||
### u8 (1 byte)
|
||||
|
||||
A single byte, value `0-255`.
|
||||
|
||||
---
|
||||
|
||||
## 10. Bundle Verification
|
||||
|
||||
A complete bundle verification proceeds in this order:
|
||||
|
||||
1. **Magic check:** First 8 bytes must be `"ARBORICX"`.
|
||||
2. **Version check:** Major version must be `1`.
|
||||
3. **Section directory:** Parse all entries; reject unknown critical sections.
|
||||
4. **Digest verification:** For each section, compute `SHA256(section_data)` and compare with the digest in the directory entry.
|
||||
5. **Manifest parsing:** Decode the fixed-order manifest; validate semantic constraints.
|
||||
6. **Node section:** Parse all node entries; reject duplicates.
|
||||
7. **Root verification:** All root hashes from the manifest must exist in the node map.
|
||||
8. **Export verification:** All export root hashes must exist in the node map.
|
||||
9. **Node hash verification:** For each node, compute `SHA256(domain || 0x00 || payload)` and compare with the stored hash.
|
||||
10. **Children verification:** For each Stem/Fork node, both child hashes must exist in the node map.
|
||||
11. **Closure verification:** Starting from each root hash, traverse the DAG and confirm all reachable nodes are present.
|
||||
3. **Section directory:** Parse all entries; reject unknown critical sections. Verify reserved fields are zero.
|
||||
4. **Manifest parsing:** Decode fixed-order manifest; validate semantic constraints.
|
||||
5. **Nodes section:** Parse all entries.
|
||||
6. **Bounds checking:**
|
||||
- Every root index `< nodeCount`
|
||||
- Every export index `< nodeCount`
|
||||
- In every Stem payload, `child_index < entry_position` and `child_index < nodeCount`
|
||||
- In every Fork payload, both indices `< entry_position` and `< nodeCount`
|
||||
7. **Acyclicity:** Guaranteed by the `child < parent` rule above.
|
||||
8. **Closure:** Traverse from all root/export indices; confirm every reached index is valid.
|
||||
|
||||
No hash computation is required.
|
||||
|
||||
---
|
||||
|
||||
## 11. Known Section Types
|
||||
## 11. Canonicalization
|
||||
|
||||
A bundle is **canonical** iff:
|
||||
|
||||
1. **Maximal deduplication.** No two entries represent structurally identical subtrees.
|
||||
2. **Topological order.** Children precede parents.
|
||||
3. **Deterministic post-order traversal.** Nodes are emitted in the order discovered by a left-to-right recursive post-order walk.
|
||||
4. **No trailing bytes** in any section.
|
||||
5. **Reserved fields are zero.**
|
||||
|
||||
Canonical bundles produce deterministic bytes and can be file-level hashed for global identity.
|
||||
|
||||
---
|
||||
|
||||
## 12. Known Section Types
|
||||
|
||||
| Type | Name | Required | Version | Description |
|
||||
|------|------|----------|---------|-------------|
|
||||
| 1 | Manifest | Yes | 1 | Bundle metadata in fixed-order binary format |
|
||||
| 2 | Nodes | Yes | 1 | Merkle DAG node entries |
|
||||
| 1 | Manifest | Yes | 1 | Bundle metadata |
|
||||
| 2 | Nodes | Yes | 1 | Topological DAG node entries |
|
||||
|
||||
Unknown section types are permitted if not marked as critical (flags bit 0 is not set).
|
||||
Unknown section types are permitted if not marked critical.
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: Complete Example Layout (id.arboricx)
|
||||
## Appendix A: Complete Example Layout
|
||||
|
||||
A minimal `id.arboricx` bundle has:
|
||||
A minimal bundle for `Stem(Leaf)` (the Tree Calculus encoding of `t t`):
|
||||
|
||||
```
|
||||
+---------------------------------------------------+
|
||||
@@ -392,28 +340,25 @@ A minimal `id.arboricx` bundle has:
|
||||
| Flags: 0 |
|
||||
| Dir offset: 32 |
|
||||
+---------------------------------------------------+
|
||||
| Section Directory (120 bytes = 2 × 60) |
|
||||
| Entry 0: type=1 (manifest), offset=152, len=375 |
|
||||
| Entry 1: type=2 (nodes), offset=527, len=284 |
|
||||
| Section Directory (64 bytes = 2 × 32) |
|
||||
| Entry 0: type=1 (manifest), offset=96, len=~200 |
|
||||
| Entry 1: type=2 (nodes), offset=~296, len=10 |
|
||||
+---------------------------------------------------+
|
||||
| Manifest Section (375 bytes) |
|
||||
| Magic: "ARBMNFST" |
|
||||
| Version: 1.0 |
|
||||
| Core strings (schema, bundleType, tree spec, |
|
||||
| runtime spec, capabilities, closure, roots, |
|
||||
| exports, metadata TLVs, extension fields) |
|
||||
| Manifest Section (~200 bytes) |
|
||||
| Magic: "ARBMNFST", Version: 1.1 |
|
||||
| Schema, bundleType, tree spec, runtime spec |
|
||||
| Closure: 0, Roots: [1], Exports: ["main" -> 1] |
|
||||
| Metadata TLVs, zero extension fields |
|
||||
+---------------------------------------------------+
|
||||
| Nodes Section (284 bytes) |
|
||||
| Nodes Section (10 bytes) |
|
||||
| Node count: 2 |
|
||||
| Node entry 1: hash + payload (Leaf) |
|
||||
| Node entry 2: hash + payload (Fork) |
|
||||
| Entry 0: payloadLen=1, payload=[0x00] |
|
||||
| Entry 1: payloadLen=5, payload=[0x01, 0,0,0,0] |
|
||||
+---------------------------------------------------+
|
||||
```
|
||||
|
||||
The manifest section starts at byte 152 (0x98) and the nodes section at byte 527 (0x20F).
|
||||
|
||||
---
|
||||
|
||||
## Appendix B: File Extension
|
||||
|
||||
Bundles produced by the `tricu` tool use the `.arboricx` file extension. The `.tri` extension is used for plain source files; the `.arboricx` extension identifies the portable binary format.
|
||||
Bundles use the `.arboricx` file extension. Plain source files use `.tri`.
|
||||
|
||||
596
docs/content-store-and-module-format.md
Normal file
596
docs/content-store-and-module-format.md
Normal file
@@ -0,0 +1,596 @@
|
||||
# Content Store and Module Format Design
|
||||
|
||||
Status: concrete design draft.
|
||||
|
||||
This document narrows the higher-level module-system direction into concrete
|
||||
format and storage decisions. It intentionally avoids source/provenance details:
|
||||
modules export usable portable artifacts, not edit history.
|
||||
|
||||
Related design overview: `docs/module-system-design.md`.
|
||||
|
||||
## 1. Scope
|
||||
|
||||
This document specifies the first target shape for:
|
||||
|
||||
- a neutral filesystem-backed content-addressed store;
|
||||
- Arboricx Merkle node persistence;
|
||||
- indexed Arboricx bundle import/export as transport;
|
||||
- module manifests as immutable export maps;
|
||||
- workspace aliases as mutable human-facing references;
|
||||
- View Contract artifact attachment to module exports.
|
||||
|
||||
It does not specify:
|
||||
|
||||
- package manager semantics;
|
||||
- dependency solving;
|
||||
- source-level rebuild/provenance metadata;
|
||||
- final import syntax;
|
||||
- garbage collection;
|
||||
- registry/sync protocol.
|
||||
|
||||
## 2. Non-Negotiable Boundaries
|
||||
|
||||
The content store is not `tricu`-specific and is not Haskell-specific.
|
||||
|
||||
The store may contain objects produced by `tricu`, Haskell, Tree Calculus tools,
|
||||
Arboricx tooling, or future frontends. The store core only knows object bytes,
|
||||
object kinds, hashes, aliases, and optionally structural references for known
|
||||
portable formats.
|
||||
|
||||
View Contracts may be first-class artifact references because they are portable
|
||||
Tree Calculus data checked by pure Tree Calculus code. They are not
|
||||
Haskell-private semantics.
|
||||
|
||||
Source and build provenance are intentionally excluded from the first module
|
||||
manifest format. A module manifest answers:
|
||||
|
||||
```text
|
||||
What portable artifacts does this module export, and what portable contracts are
|
||||
paired with them?
|
||||
```
|
||||
|
||||
It does not answer:
|
||||
|
||||
```text
|
||||
Which source file, parser, frontend, or build command produced these artifacts?
|
||||
```
|
||||
|
||||
## 3. Hashing Convention
|
||||
|
||||
Objects are content-addressed by SHA-256 over domain-separated canonical bytes.
|
||||
|
||||
General rule:
|
||||
|
||||
```text
|
||||
hash = SHA256(domainUtf8 || 0x00 || canonicalPayloadBytes)
|
||||
```
|
||||
|
||||
This matches the existing Merkle node convention in `Research.nodeHash`:
|
||||
|
||||
```text
|
||||
SHA256("arboricx.merkle.node.v1" || 0x00 || nodePayload)
|
||||
```
|
||||
|
||||
The domain string is part of the object format. It prevents identical payload
|
||||
bytes in different formats from accidentally sharing identity.
|
||||
|
||||
Hashes are represented externally as 64 lowercase hexadecimal characters.
|
||||
|
||||
## 4. Filesystem Store Layout
|
||||
|
||||
The canonical filesystem store layout is:
|
||||
|
||||
```text
|
||||
store/
|
||||
objects/
|
||||
abc/
|
||||
abc123... -- object bytes, sharded by first 3 hex chars
|
||||
aliases/
|
||||
names/
|
||||
modules/
|
||||
packages/
|
||||
manifests/
|
||||
tmp/
|
||||
```
|
||||
|
||||
The three-character shard follows the existing `lib/arboricx/server.tri`
|
||||
convention.
|
||||
|
||||
### 4.1 Object paths
|
||||
|
||||
For object hash:
|
||||
|
||||
```text
|
||||
abc123...
|
||||
```
|
||||
|
||||
object bytes live at:
|
||||
|
||||
```text
|
||||
store/objects/abc/abc123...
|
||||
```
|
||||
|
||||
The object filename is the full hash. The shard directory is the first three hex
|
||||
characters.
|
||||
|
||||
### 4.2 Atomic writes
|
||||
|
||||
Writers should use:
|
||||
|
||||
```text
|
||||
store/tmp/<hash>.<nonce>.tmp
|
||||
```
|
||||
|
||||
then atomically rename into:
|
||||
|
||||
```text
|
||||
store/objects/<shard>/<hash>
|
||||
```
|
||||
|
||||
Writing an existing object is idempotent if the existing bytes match the hash.
|
||||
|
||||
### 4.3 Store core metadata
|
||||
|
||||
The minimal filesystem store does not require sidecar metadata for every object.
|
||||
Object kind can be known by context or by manifest references.
|
||||
|
||||
A later index may cache:
|
||||
|
||||
```text
|
||||
hash -> kind
|
||||
hash -> size
|
||||
hash -> references
|
||||
hash -> createdAt
|
||||
```
|
||||
|
||||
but this index is not semantic identity.
|
||||
|
||||
## 5. Arboricx Merkle Node Object Format
|
||||
|
||||
The persistent Tree Calculus representation is a Merkle DAG of node objects.
|
||||
|
||||
Domain:
|
||||
|
||||
```text
|
||||
arboricx.merkle.node.v1
|
||||
```
|
||||
|
||||
Canonical payloads:
|
||||
|
||||
```text
|
||||
Leaf = 0x00
|
||||
Stem child = 0x01 || childHashRaw32
|
||||
Fork left right
|
||||
= 0x02 || leftHashRaw32 || rightHashRaw32
|
||||
```
|
||||
|
||||
Where `childHashRaw32`, `leftHashRaw32`, and `rightHashRaw32` are the raw 32-byte
|
||||
SHA-256 digests corresponding to child node hashes.
|
||||
|
||||
This is already implemented conceptually by:
|
||||
|
||||
```text
|
||||
Research.Node
|
||||
Research.serializeNode
|
||||
Research.deserializeNode
|
||||
Research.nodeHash
|
||||
```
|
||||
|
||||
The filesystem CAS should use this payload/hash convention directly.
|
||||
|
||||
## 6. Tree Roots
|
||||
|
||||
A Tree Calculus value stored in the CAS is identified by the hash of its root
|
||||
Merkle node.
|
||||
|
||||
```text
|
||||
treeRootHash = hash(rootNodePayload)
|
||||
```
|
||||
|
||||
The complete tree is reconstructed by recursively loading node objects reachable
|
||||
from the root.
|
||||
|
||||
Hydration is an interpretation step, not part of object identity. A client may
|
||||
hydrate a root as a plain tree, a graph with explicit sharing, or another runtime
|
||||
representation as long as the observable Tree Calculus value is the same. The
|
||||
filesystem CAS provides structural dedupe and portable identity; it does not by
|
||||
itself guarantee that a hydrated runtime value is the cheapest representation for
|
||||
all workloads.
|
||||
|
||||
Merkle nodes are useful for explicit DAG-oriented tooling, audit, and bundle
|
||||
packing. They are not the default representation for module executable exports:
|
||||
storing every subtree as a separate filesystem object is pathologically slow for
|
||||
large normal forms.
|
||||
|
||||
For module-backed evaluation and imports, a complete normalized named term is
|
||||
stored as one canonical object:
|
||||
|
||||
```text
|
||||
kind: arboricx.tree-term.v1
|
||||
hash: <whole-term object hash>
|
||||
abi: arboricx.abi.tree.v1
|
||||
```
|
||||
|
||||
The `arboricx.tree-term.v1` payload is a prefix encoding:
|
||||
|
||||
```text
|
||||
Leaf = 0x00
|
||||
Stem t = 0x01 Tree
|
||||
Fork l r = 0x02 Tree Tree
|
||||
```
|
||||
|
||||
## 7. Arboricx Indexed Bundles
|
||||
|
||||
Indexed `.arboricx` bundles remain the transport/execution format.
|
||||
|
||||
They are:
|
||||
|
||||
- compact;
|
||||
- self-contained;
|
||||
- deterministic;
|
||||
- suitable for restricted runtimes;
|
||||
- suitable for HTTP serving and deployment.
|
||||
|
||||
They are not the canonical long-lived deduplicated store representation.
|
||||
|
||||
### 7.1 Pack
|
||||
|
||||
Packing converts one or more CAS tree roots into an indexed bundle:
|
||||
|
||||
```text
|
||||
CAS tree roots -> indexed Arboricx bundle
|
||||
```
|
||||
|
||||
The packer traverses reachable Merkle nodes, emits a compact indexed node table,
|
||||
and writes a bundle manifest with export names and root indices.
|
||||
|
||||
### 7.3 Unpack
|
||||
|
||||
Unpacking converts a bundle into CAS nodes:
|
||||
|
||||
```text
|
||||
indexed Arboricx bundle -> CAS tree roots
|
||||
```
|
||||
|
||||
The unpacker verifies the bundle structure, reconstructs each exported tree, and
|
||||
stores the corresponding Merkle nodes. It returns the tree root hash for each
|
||||
bundle export.
|
||||
|
||||
## 8. Module Manifest v1
|
||||
|
||||
A module is an immutable manifest object. The module identity is the hash of its
|
||||
canonical manifest bytes.
|
||||
|
||||
A module name is not identity. It is a workspace alias to a module manifest hash.
|
||||
|
||||
### 8.1 Domain
|
||||
|
||||
Proposed domain:
|
||||
|
||||
```text
|
||||
arboricx.module-manifest.v1
|
||||
```
|
||||
|
||||
### 8.2 Purpose
|
||||
|
||||
A module manifest pairs human-facing export names with portable content objects
|
||||
and optional portable contracts.
|
||||
|
||||
It exists to support:
|
||||
|
||||
- reproducible import resolution;
|
||||
- executable export discovery;
|
||||
- View Contract lookup for imported symbols;
|
||||
- module-to-module reference tracking;
|
||||
- transport/store interop.
|
||||
|
||||
It does not describe source provenance.
|
||||
|
||||
### 8.3 Conceptual shape
|
||||
|
||||
```text
|
||||
moduleManifestV1:
|
||||
imports:
|
||||
- alias: <text>
|
||||
kind: <object kind>
|
||||
hash: <object hash>
|
||||
|
||||
exports:
|
||||
- name: <text>
|
||||
object:
|
||||
kind: <object kind>
|
||||
hash: <object hash>
|
||||
abi: <abi identifier>
|
||||
view: optional
|
||||
kind: <view artifact kind>
|
||||
hash: <view artifact hash>
|
||||
catalog: optional
|
||||
kind: <view catalog kind>
|
||||
hash: <view catalog hash>
|
||||
|
||||
metadata: optional human-facing fields
|
||||
```
|
||||
|
||||
### 8.4 Imports/references
|
||||
|
||||
The `imports` section is a manifest reference graph, not a store-level language
|
||||
dependency graph.
|
||||
|
||||
Each entry records direct content-addressed references used by the module:
|
||||
|
||||
```text
|
||||
alias: Prelude
|
||||
kind: arboricx.module-manifest.v1
|
||||
hash: <module hash>
|
||||
```
|
||||
|
||||
This supports reproducibility, partial fetch, and audit. The content store core
|
||||
stores this object but does not need to understand `Prelude` or import
|
||||
semantics.
|
||||
|
||||
### 8.5 Exports
|
||||
|
||||
Each export is a record, not a single hash. This is required so executable
|
||||
objects and advertised contracts cannot drift apart.
|
||||
|
||||
Minimal executable export:
|
||||
|
||||
```text
|
||||
name: "id"
|
||||
object:
|
||||
kind: arboricx.tree-term.v1
|
||||
hash: <whole-term hash>
|
||||
abi: arboricx.abi.tree.v1
|
||||
```
|
||||
|
||||
Export with View Contract:
|
||||
|
||||
```text
|
||||
name: "map"
|
||||
object:
|
||||
kind: arboricx.tree-term.v1
|
||||
hash: <whole-term hash>
|
||||
abi: arboricx.abi.tree.v1
|
||||
view:
|
||||
kind: arboricx.view-contract.type.v1
|
||||
hash: <view type hash>
|
||||
```
|
||||
|
||||
The manifest preserves the pairing between exported executable and exported
|
||||
contract. For workspace modules built from local source, annotated exports are
|
||||
checked before the manifest is published; only exports that pass producer-side
|
||||
View Contract checking receive direct `arboricx.view-contract.type.v1` refs.
|
||||
|
||||
### 8.6 Metadata
|
||||
|
||||
Metadata is optional and human-facing. Initial fields may include:
|
||||
|
||||
```text
|
||||
package
|
||||
version
|
||||
description
|
||||
license
|
||||
createdBy
|
||||
```
|
||||
|
||||
Metadata is not source provenance and is not required for execution or checking.
|
||||
|
||||
## 9. View Contract Artifacts
|
||||
|
||||
View Contract artifacts are portable Arboricx-layer data. They may be stored
|
||||
as content objects and referenced by module exports. `tricu` may emit these
|
||||
objects, but the object kind is not tricu-specific.
|
||||
|
||||
Current artifact kind:
|
||||
|
||||
```text
|
||||
arboricx.view-contract.type.v1
|
||||
```
|
||||
|
||||
`arboricx.view-contract.type.v1` is the direct export-view artifact. Its
|
||||
payload is a canonical prefix binary encoding of the syntactic ViewType:
|
||||
|
||||
```text
|
||||
Name = 0x00 u32be(byte-length) utf8-name
|
||||
Ref = 0x01 u32be(byte-length) utf8-ref
|
||||
List = 0x02 ViewType
|
||||
Maybe = 0x03 ViewType
|
||||
Pair = 0x04 ViewType ViewType
|
||||
Result = 0x05 ViewType ViewType
|
||||
Fn = 0x06 u32be(argument-count) ViewType* ViewType
|
||||
```
|
||||
|
||||
`utf8-ref` is tagged text:
|
||||
|
||||
```text
|
||||
i:<decimal-integer> numeric/legacy ref
|
||||
s:<text> symbolic user ref
|
||||
```
|
||||
|
||||
Symbolic refs are the preferred user-authored form; numeric refs remain useful
|
||||
for generated code, fixtures, and old low-level examples.
|
||||
|
||||
The object hash domain is the object kind:
|
||||
|
||||
```text
|
||||
arboricx.view-contract.type.v1 \0 <payload>
|
||||
```
|
||||
|
||||
### 9.1 Export-level pairing
|
||||
|
||||
The module manifest is the canonical pairing of an executable export and its
|
||||
advertised contract:
|
||||
|
||||
```text
|
||||
export name -> tree-term hash + optional view artifact hash
|
||||
```
|
||||
|
||||
This avoids drift such as:
|
||||
|
||||
```text
|
||||
map -> tree A
|
||||
map.view -> contract B
|
||||
```
|
||||
|
||||
where aliases might be retargeted independently.
|
||||
|
||||
### 9.2 Import checking
|
||||
|
||||
When a source file imports a module, a frontend can resolve an imported export,
|
||||
decode its direct `arboricx.view-contract.type.v1` ref, and emit typed program
|
||||
evidence locally:
|
||||
|
||||
```text
|
||||
imported List.map has view Fn [...]
|
||||
```
|
||||
|
||||
For locally built workspace modules this is backed by producer-side checking
|
||||
before the module manifest alias is published, including imported view facts from
|
||||
dependencies used by the producer source. External or prebuilt manifests are
|
||||
trusted boundary declarations for now; they are not accompanied by proof objects.
|
||||
The checker still consumes only local numeric symbols and typed-program evidence.
|
||||
Global content hashes do not become checker symbols.
|
||||
|
||||
Correct split:
|
||||
|
||||
```text
|
||||
local checker symbol: 3
|
||||
presentation label: "List.map"
|
||||
resolved object: sha256:...
|
||||
exported view: Fn [...]
|
||||
```
|
||||
|
||||
### 9.3 Execution hydration versus contract evidence
|
||||
|
||||
Execution imports should use a narrow, demand-driven path:
|
||||
|
||||
```text
|
||||
module import -> selected executable exports -> hydrate selected tree-term objects
|
||||
```
|
||||
|
||||
This path should not compute a dependency closure over other module exports.
|
||||
Each selected executable export is already a complete Tree Calculus value.
|
||||
|
||||
Contract-aware checking may use a broader path:
|
||||
|
||||
```text
|
||||
module import -> selected exports -> exported view type refs -> typed-program evidence
|
||||
```
|
||||
|
||||
That path emits portable evidence and leaves compatibility policy decisions to
|
||||
the Tree Calculus checker. typed programs and reusable catalogs do not need their
|
||||
own binary object kinds today: they are ordinary Tree Calculus data and can be
|
||||
stored as `arboricx.tree-term.v1` when persistence is useful.
|
||||
|
||||
## 10. Workspace Aliases
|
||||
|
||||
A workspace is mutable human-facing state over immutable content.
|
||||
|
||||
Examples:
|
||||
|
||||
```text
|
||||
List -> module manifest hash
|
||||
Prelude -> module manifest hash
|
||||
map -> tree-term hash
|
||||
httpServer -> bundle hash
|
||||
```
|
||||
|
||||
Aliases should live under:
|
||||
|
||||
```text
|
||||
store/aliases/
|
||||
```
|
||||
|
||||
Initial categories:
|
||||
|
||||
```text
|
||||
store/aliases/modules/<name>
|
||||
store/aliases/names/<name>
|
||||
store/aliases/packages/<name>
|
||||
```
|
||||
|
||||
Alias file contents should be simple and explicit, for example:
|
||||
|
||||
```text
|
||||
kind: arboricx.module-manifest.v1
|
||||
hash: abc123...
|
||||
```
|
||||
|
||||
Exact encoding can be decided with the first implementation. The important rule
|
||||
is that aliases are mutable pointers, not content identity.
|
||||
|
||||
## 11. Existing Convention Alignment
|
||||
|
||||
This design intentionally preserves existing conventions where they already fit:
|
||||
|
||||
- SHA-256 domain-separated Merkle node hashing;
|
||||
- `Leaf` / `Stem` / `Fork` node payload tags `0x00`, `0x01`, `0x02`;
|
||||
- three-character object sharding from `lib/arboricx/server.tri`;
|
||||
- indexed Arboricx bundles as compact transport objects;
|
||||
- optional human-facing export names in manifests;
|
||||
- View Contract checker evidence as portable Tree Calculus data.
|
||||
|
||||
It replaces or demotes conventions that do not fit:
|
||||
|
||||
- SQLite `terms.names` comma-separated aliases become workspace aliases/indexes;
|
||||
- SQLite `terms.tags` comma-separated tags become optional metadata/indexes;
|
||||
- file imports as AST flattening become transitional behavior;
|
||||
- names cease to be semantic identity.
|
||||
|
||||
## 12. Implementation Sketch
|
||||
|
||||
A staged implementation can proceed as follows:
|
||||
|
||||
1. Add filesystem CAS helpers alongside the existing SQLite store.
|
||||
2. Store/load Arboricx Merkle nodes using the filesystem layout.
|
||||
3. Implement tree-term storage and reconstruction from filesystem CAS.
|
||||
4. Implement pack from CAS tree terms/Merkle roots to indexed Arboricx bundle.
|
||||
5. Implement unpack from indexed Arboricx bundle to CAS tree terms/Merkle roots.
|
||||
6. Define a concrete module manifest encoding.
|
||||
7. Store/load module manifests as content-addressed objects.
|
||||
8. Add workspace alias read/write helpers.
|
||||
9. Teach import resolution to target module manifests/exports.
|
||||
10. Attach exported View Contract artifacts to module exports.
|
||||
11. Gradually migrate existing `!import` users.
|
||||
|
||||
## 13. Deferred Decisions
|
||||
|
||||
These are intentionally left out of the first concrete format:
|
||||
|
||||
- package version solving;
|
||||
- registry/remotes protocol;
|
||||
- garbage collection/reachability;
|
||||
- source/provenance/build-record objects;
|
||||
- editor/update workflows;
|
||||
- rich visibility/export rules;
|
||||
- final import syntax;
|
||||
- whether module manifests also need a tree-native encoding.
|
||||
|
||||
## 14. Summary
|
||||
|
||||
The concrete v1 direction is:
|
||||
|
||||
```text
|
||||
Store:
|
||||
filesystem-backed content-addressed objects
|
||||
|
||||
Hashing:
|
||||
SHA256(domain || 0x00 || canonical payload)
|
||||
|
||||
Tree persistence:
|
||||
Arboricx Merkle nodes
|
||||
|
||||
Transport:
|
||||
indexed .arboricx bundles, packable from and unpackable to CAS roots
|
||||
|
||||
Modules:
|
||||
immutable manifests pairing export names with object refs and optional View
|
||||
Contract refs
|
||||
|
||||
Workspace:
|
||||
mutable aliases from human names to immutable content hashes
|
||||
```
|
||||
|
||||
This keeps the store portable, preserves Arboricx's compact transport role,
|
||||
restores Merkle DAGs as the persistence model, and gives View Contracts a stable
|
||||
module/export attachment point without making the store `tricu`-specific.
|
||||
371
docs/guard-injection.md
Normal file
371
docs/guard-injection.md
Normal file
@@ -0,0 +1,371 @@
|
||||
# Guard Injection Semantics
|
||||
|
||||
This document describes the runtime guard model for View Contracts.
|
||||
|
||||
Views describe portable structural contracts. Guarded views refine those
|
||||
contracts with executable predicates while keeping ordinary value-level code free
|
||||
of `Maybe`, `Result`, sentinel, or host-language abort handling.
|
||||
|
||||
```tri
|
||||
viewGuarded baseView guard
|
||||
```
|
||||
|
||||
A guarded view means: when this guarded view is observed along the reachable
|
||||
checked-execution path, run `guard` against the runtime value.
|
||||
|
||||
## Goals
|
||||
|
||||
- Preserve ordinary value-level program shapes.
|
||||
- Keep guard failure out of user code.
|
||||
- Avoid Haskell-specific checker/runtime semantics.
|
||||
- Represent guard boundaries explicitly in portable tree data.
|
||||
- Make successful guarded execution transparent: guarded values are unwrapped
|
||||
before ordinary code receives them.
|
||||
- Prefer correctness-by-default over avoiding repeated predicate cost.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Preventing user-written guards from diverging.
|
||||
- Letting guards author their own diagnostics.
|
||||
- Solving IO interaction-tree composition.
|
||||
- Finalizing long-term artifact identity policy.
|
||||
- Deduplicating or hoisting repeated guard checks.
|
||||
|
||||
## Plain Views vs Guards
|
||||
|
||||
Plain Views still provide concrete benefits without guards:
|
||||
|
||||
- structural flow checking;
|
||||
- portable API metadata;
|
||||
- module/export contract metadata;
|
||||
- content-store view-tree metadata;
|
||||
- cross-frontend agreement on contract structure;
|
||||
- diagnostics for wrong-view flows.
|
||||
|
||||
Guards are for invariants that require runtime value inspection, such as:
|
||||
|
||||
- non-empty list;
|
||||
- sorted list;
|
||||
- byte string of exactly 32 bytes;
|
||||
- protocol payload with a valid checksum;
|
||||
- domain-specific runtime predicate.
|
||||
|
||||
Guards are deliberately more expensive than ordinary Views. Use them when the
|
||||
runtime contract must be enforced.
|
||||
|
||||
## Guard Result Protocol
|
||||
|
||||
Guards return one of two standardized shapes:
|
||||
|
||||
```tri
|
||||
guardOk value
|
||||
guardFail
|
||||
```
|
||||
|
||||
Guards do not provide diagnostics. The checked-exec runner owns diagnostics.
|
||||
Malformed guard output is treated as a checked-runtime failure.
|
||||
|
||||
## Checked Execution Protocol
|
||||
|
||||
A successful typed-program check returns a checked-execution artifact, not a raw
|
||||
payload.
|
||||
|
||||
Current constructors:
|
||||
|
||||
```tri
|
||||
checkedPure value
|
||||
checkedFail diagnostic
|
||||
checkedGuard view guard value continuation
|
||||
checkedGuardWithContext context view guard value continuation
|
||||
checkedBind exec continuation
|
||||
```
|
||||
|
||||
`checkedGuard` is the compatibility/default constructor. It lowers to
|
||||
`checkedGuardWithContext` with an unknown context. Checker-injected guard
|
||||
boundaries use `checkedGuardWithContext` so failures can identify where the
|
||||
boundary came from.
|
||||
|
||||
Runner:
|
||||
|
||||
```tri
|
||||
runChecked checkedExec
|
||||
```
|
||||
|
||||
Semantics:
|
||||
|
||||
```text
|
||||
runChecked (checkedPure value)
|
||||
= checkedRuntimeOk value
|
||||
|
||||
runChecked (checkedFail diagnostic)
|
||||
= checkedRuntimeFail diagnostic
|
||||
|
||||
runChecked (checkedGuardWithContext context view guard value continuation)
|
||||
= case guard value of
|
||||
guardOk checkedValue -> runChecked (continuation checkedValue)
|
||||
guardFail -> checkedRuntimeFail (guardFailed context view)
|
||||
malformed -> checkedRuntimeFail (malformedGuardResult context view malformed)
|
||||
|
||||
runChecked (checkedGuard view guard value continuation)
|
||||
= runChecked (checkedGuardWithContext unknownContext view guard value continuation)
|
||||
|
||||
runChecked (checkedBind exec continuation)
|
||||
= case runChecked exec of
|
||||
checkedRuntimeOk value -> runChecked (continuation value)
|
||||
checkedRuntimeFail diag -> checkedRuntimeFail diag
|
||||
```
|
||||
|
||||
Important invariant:
|
||||
|
||||
> Guard failure is consumed by `runChecked`. It is never passed into ordinary
|
||||
> user code.
|
||||
|
||||
## Checker Result Shape
|
||||
|
||||
`checkTypedProgramWith` returns checked-exec on success:
|
||||
|
||||
```tri
|
||||
ok checkedExec env
|
||||
```
|
||||
|
||||
Even unguarded programs return:
|
||||
|
||||
```tri
|
||||
checkedPure rootPayload
|
||||
```
|
||||
|
||||
Compatibility helper:
|
||||
|
||||
```tri
|
||||
checkedProgramTree result
|
||||
```
|
||||
|
||||
`checkedProgramTree` runs/unwraps checked-exec to preserve older raw-tree helper
|
||||
behavior.
|
||||
|
||||
The Haskell `tricu check` path now evaluates successful checker output through
|
||||
`runChecked`, so source-level guarded annotations fail through the same portable
|
||||
checked-exec protocol.
|
||||
|
||||
## Boundary Semantics
|
||||
|
||||
Guard insertion follows correctness-first semantics:
|
||||
|
||||
> Every guarded View observation on the reachable checked-execution path runs
|
||||
> its guard.
|
||||
|
||||
Important boundary kinds:
|
||||
|
||||
### Guarded typed value
|
||||
|
||||
```tri
|
||||
typedValue sym (viewGuarded base guard) payload
|
||||
```
|
||||
|
||||
This observes `sym` as a guarded value. It also supplies base-view evidence for
|
||||
flow checking.
|
||||
|
||||
### Guarded requirement
|
||||
|
||||
```tri
|
||||
typedRequire sym (viewGuarded base guard) payload
|
||||
```
|
||||
|
||||
The symbol must satisfy `base`; the guarded observation is attached to `sym` and
|
||||
is enforced when `sym` is used or exposed along the reachable root path.
|
||||
|
||||
### Guarded function argument
|
||||
|
||||
For:
|
||||
|
||||
```tri
|
||||
viewFn [(viewGuarded base guard)] result
|
||||
```
|
||||
|
||||
application checking guards the argument before the callee receives it.
|
||||
|
||||
### Guarded function result
|
||||
|
||||
For:
|
||||
|
||||
```tri
|
||||
viewFn [arg] (viewGuarded base guard)
|
||||
```
|
||||
|
||||
application checking guards the application result before exposing it as the
|
||||
result value.
|
||||
|
||||
### Guarded callee symbol
|
||||
|
||||
If a function symbol itself has a guarded observation, that guard runs before the
|
||||
function value is applied. A successful guard may transform the function value;
|
||||
the application uses the guarded value.
|
||||
|
||||
## Global Symbol Observations
|
||||
|
||||
Guarded `typedValue` and `typedRequire` nodes are **global per-symbol
|
||||
observations**, not position-sensitive flow events.
|
||||
|
||||
All guarded observations for a symbol compose in typed-node order whenever that
|
||||
symbol is used or exposed on the reachable checked-execution path.
|
||||
|
||||
This means a later requirement still applies to an earlier syntactic use:
|
||||
|
||||
```tri
|
||||
typedValue 1 viewString "x"
|
||||
typedApply 2 f 1 "x"
|
||||
typedRequire 1 (viewGuarded viewString guard) "x"
|
||||
```
|
||||
|
||||
The guarded requirement is attached to symbol `1`; compiling the reachable root
|
||||
path that uses symbol `1` runs that guard.
|
||||
|
||||
Rationale:
|
||||
|
||||
- typed programs are declarative symbol graphs, not imperative event traces;
|
||||
- global observations are simpler and more correct-by-default;
|
||||
- producers cannot accidentally bypass a guard by ordering a requirement too
|
||||
late;
|
||||
- staged raw/checked phases should use distinct symbols.
|
||||
|
||||
## Reachability and Repetition
|
||||
|
||||
Guards are not run eagerly for every guarded node in a program.
|
||||
|
||||
Execution is root-reachable:
|
||||
|
||||
```tri
|
||||
compileSymbol (typedProgramRoot program)
|
||||
```
|
||||
|
||||
Only guarded observations reachable from the root checked-execution path run.
|
||||
Unreachable guarded symbols do not pay guard cost and do not fail execution.
|
||||
|
||||
Repeated reachable uses rerun guards. There is currently no deduplication or
|
||||
hoisting. This is intentional: each guarded observation/use is a runtime contract
|
||||
boundary.
|
||||
|
||||
Future optimization policies may add explicit deduplication or hoisting, but the
|
||||
baseline semantics are repeated, deterministic guard execution.
|
||||
|
||||
## Function and Application Compilation
|
||||
|
||||
Checked execution is built compositionally from typed-node dependencies:
|
||||
|
||||
1. compile the callee symbol;
|
||||
2. compile the argument symbol;
|
||||
3. run any guarded observations attached to the argument symbol;
|
||||
4. run the guarded function-argument boundary, if present;
|
||||
5. apply the callee to the checked argument;
|
||||
6. run the guarded function-result boundary, if present;
|
||||
7. run guarded observations attached to the application result symbol.
|
||||
|
||||
This handles nested and curried application chains because each `typedApply`
|
||||
consumes one function argument and produces a symbol whose inferred view is the
|
||||
function residual/result view.
|
||||
|
||||
## Diagnostics
|
||||
|
||||
Guards do not author diagnostics. The checked-exec runner renders diagnostics
|
||||
from checker-owned boundary context plus the guarded View.
|
||||
|
||||
Checker-injected guard nodes carry portable structural context. Current context
|
||||
kinds are:
|
||||
|
||||
- root `typedValue` exposure;
|
||||
- root `typedRequire` exposure;
|
||||
- non-root `typedValue` symbol observation;
|
||||
- non-root `typedRequire` symbol observation;
|
||||
- function argument boundary;
|
||||
- function result boundary;
|
||||
- unknown/default context for manually constructed `checkedGuard` values.
|
||||
|
||||
Examples:
|
||||
|
||||
```text
|
||||
guard failed at root typedValue symbol 0 for Guarded String
|
||||
guard failed at root typedRequire symbol 3 for Guarded String
|
||||
guard failed at typedRequire symbol 6 for Guarded String
|
||||
guard failed at argument 0 of application symbol 2 (callee symbol 0, arg symbol 1) for Guarded String
|
||||
guard failed at result of application symbol 2 (callee symbol 0, arg symbol 1) for Guarded String
|
||||
malformed guard result at argument 0 of application symbol 2 (callee symbol 0, arg symbol 1) for Guarded String
|
||||
```
|
||||
|
||||
Manually constructed `checkedGuard` values use unknown context and therefore
|
||||
render without a boundary suffix:
|
||||
|
||||
```text
|
||||
guard failed for String
|
||||
malformed guard result for String
|
||||
```
|
||||
|
||||
The context is diagnostic-only. It does not affect guard execution, View
|
||||
compatibility, success/failure semantics, or continuation values.
|
||||
|
||||
The context deliberately contains raw portable data such as symbols and
|
||||
application edges. It does not preserve source aliases such as `NonEmptyString`,
|
||||
and it does not rely on Haskell-side post-processing or source-name annotation.
|
||||
Named View rendering is a separate future design topic.
|
||||
|
||||
## Why Not Abort in Haskell?
|
||||
|
||||
A host-level abort primitive would move guard semantics into Haskell. The design
|
||||
instead encodes guard failure in portable checked-exec artifacts and interprets
|
||||
it with portable `tricu` code.
|
||||
|
||||
Haskell may evaluate the runner, but Haskell is not the semantic source of guard
|
||||
validity or failure behavior.
|
||||
|
||||
## Why Not Maybe / Result Everywhere?
|
||||
|
||||
Returning `Maybe` or `Result` from every guarded boundary would infect ordinary
|
||||
APIs. A function expecting a `List Byte` would have to accept
|
||||
`Maybe (List Byte)` or `Result Error (List Byte)`, and every downstream caller
|
||||
would need defensive handling.
|
||||
|
||||
The checked-exec runner avoids this. It unwraps successful guard results before
|
||||
continuing and stops checked execution on failure.
|
||||
|
||||
## Known Sharp Edges
|
||||
|
||||
### Guard divergence
|
||||
|
||||
A user-written guard may diverge. This design handles intentional failure via
|
||||
`guardFail`; it does not solve arbitrary nontermination. Fuel or timeouts are
|
||||
separate runtime concerns.
|
||||
|
||||
### Payload trust
|
||||
|
||||
Typed nodes carry executable payloads. Guard injection must not expose an
|
||||
unchecked precomputed payload at a guarded boundary. Boundaries are mediated by
|
||||
checked-exec nodes.
|
||||
|
||||
This does not make malicious producer forgery impossible; it gives honest
|
||||
frontends a portable, checkable protocol that avoids accidental bypasses.
|
||||
|
||||
### Cyclic typed-apply graphs
|
||||
|
||||
The current symbol compiler assumes typed programs are well-founded dependency
|
||||
graphs as emitted by the frontend/lowering path. Cyclic typed-apply graphs are a
|
||||
malformed-program validation concern, not a guard-specific semantic feature.
|
||||
|
||||
## Current Implementation Status
|
||||
|
||||
Implemented in `lib/view.tri` and exercised by tests:
|
||||
|
||||
- `guardOk` / `guardFail`;
|
||||
- `checkedPure`, `checkedFail`, `checkedGuard`, `checkedGuardWithContext`, `checkedBind`;
|
||||
- `runChecked`;
|
||||
- success from `checkTypedProgramWith` returns checked-exec;
|
||||
- `checkedProgramTree` compatibility helper;
|
||||
- guarded root exposure;
|
||||
- guarded `typedValue` and `typedRequire`;
|
||||
- guarded function arguments and results;
|
||||
- guarded callee observations;
|
||||
- nested/curried application guard composition;
|
||||
- global per-symbol observations;
|
||||
- root-reachability behavior;
|
||||
- repeated reachable uses rerun guards;
|
||||
- source/Haskell `tricu check` integration;
|
||||
- imported/module `VTGuarded` lowering to portable `viewGuarded`;
|
||||
- portable guard boundary diagnostics with symbol/application context.
|
||||
505
docs/module-system-design.md
Normal file
505
docs/module-system-design.md
Normal file
@@ -0,0 +1,505 @@
|
||||
# Module System and Content Store Design
|
||||
|
||||
Status: design draft.
|
||||
|
||||
This document records the intended direction for reworking `tricu` modules,
|
||||
imports, Arboricx storage/transport, and the content store. It is not an
|
||||
implementation plan yet; it is a shared design target.
|
||||
|
||||
## 1. Problem Statement
|
||||
|
||||
The current module/import/content-store system is useful as a prototype, but it
|
||||
is not coherent enough to build on indefinitely.
|
||||
|
||||
Current behavior combines several partially-overlapping systems:
|
||||
|
||||
- `!import "path.tri" Namespace` and `!import "path.tri" !Local` perform
|
||||
filesystem-relative source preprocessing;
|
||||
- imported definitions are flattened into one program;
|
||||
- namespace qualification is implemented by string rewriting;
|
||||
- evaluation uses a flat `Map String T` environment;
|
||||
- the Haskell content store stores Tree Calculus Merkle nodes plus an ad hoc
|
||||
`terms` table with comma-separated names and tags;
|
||||
- the REPL can resolve names from the content store, including multiple versions;
|
||||
- Arboricx bundles provide compact indexed transport objects;
|
||||
- `lib/arboricx/server.tri` already sketches a filesystem-backed object store.
|
||||
|
||||
This works only when users and maintainers are mindful of sharp edges:
|
||||
|
||||
- names serve too many roles at once;
|
||||
- modules are not first-class semantic objects;
|
||||
- imports are closer to AST paste-and-prefix than resolution;
|
||||
- `!Local` imports can create global collisions;
|
||||
- content identity, human aliases, source files, and evaluated terms are not
|
||||
cleanly separated;
|
||||
- the SQLite schema is convenient but not a principled content-addressed store;
|
||||
- Arboricx transport and long-lived storage are not clearly distinguished.
|
||||
|
||||
## 2. Design Principles
|
||||
|
||||
### 2.1 Content addressability is foundational
|
||||
|
||||
Immutable content should be identified by hashes. Human names should be metadata
|
||||
or workspace aliases over content, not semantic identity.
|
||||
|
||||
This follows the core lesson from systems such as Unison: separate stable
|
||||
content identity from ergonomic naming and namespace organization.
|
||||
|
||||
### 2.2 The content store is language-neutral
|
||||
|
||||
The content store must not be married to `tricu` or Haskell.
|
||||
|
||||
It stores a small set of portable Arboricx artifacts: module manifests,
|
||||
complete tree terms, and direct View Contract types. Lower-level Merkle/bundle
|
||||
formats exist for transport and DAG tooling, but the store core should treat all
|
||||
objects as content-addressed bytes with formats/media types.
|
||||
|
||||
`tricu` and Haskell are clients/tooling. They are not the semantic owners of the
|
||||
store.
|
||||
|
||||
### 2.3 View Contracts are portable enough to integrate
|
||||
|
||||
The store may integrate with View Contracts because the checker and evidence
|
||||
format are pure Tree Calculus / portable tree data. View Contracts are not a
|
||||
Haskell-private or `tricu`-private semantic layer.
|
||||
|
||||
The module resolver may emit typed-program evidence, but checker semantics remain
|
||||
unchanged:
|
||||
|
||||
```text
|
||||
Haskell emits evidence.
|
||||
tricu judges evidence.
|
||||
```
|
||||
|
||||
### 2.4 Modules should reflect definitions as they actually exist
|
||||
|
||||
The module system should conform to the reality of content-addressed immutable
|
||||
artifacts and mutable human aliases. We should not contort definitions to fit a
|
||||
traditional text-file module system if that fights the storage model.
|
||||
|
||||
### 2.5 Transport and storage are different jobs
|
||||
|
||||
Indexed Arboricx bundles are excellent transport/execution objects. Merkle DAGs
|
||||
are better long-lived persistence objects. These should remain separate but
|
||||
interoperable representations.
|
||||
|
||||
## 3. Conceptual Architecture
|
||||
|
||||
```text
|
||||
Content Store
|
||||
neutral content-addressed object store
|
||||
|
||||
Arboricx CAS / Merkle Store
|
||||
Tree Calculus node/object formats suitable for persistence and dedupe
|
||||
|
||||
Arboricx Bundle
|
||||
compact indexed transport/execution format
|
||||
|
||||
View Contract Artifact
|
||||
portable evidence/checker data over tree artifacts
|
||||
|
||||
Module Manifest
|
||||
immutable export map from names to content objects and optional contracts
|
||||
|
||||
Workspace
|
||||
mutable aliases, selected versions, package pins, and user-facing names
|
||||
|
||||
tricu
|
||||
one frontend/toolchain that emits/consumes these portable artifacts
|
||||
```
|
||||
|
||||
The content store stores objects. Arboricx defines important object formats.
|
||||
View Contracts define portable checking artifacts. `tricu` produces and consumes
|
||||
those formats.
|
||||
|
||||
### 3.1 Execution imports versus contract checking
|
||||
|
||||
Import resolution has two intentionally different performance profiles.
|
||||
|
||||
For normal execution/evaluation, resolving a module import should hydrate only
|
||||
the executable exports directly demanded by the importing source. Exported Tree
|
||||
Calculus values are complete normal forms: importing `foo` does not require
|
||||
hydrating separate `bar` or `baz` exports that may have helped build it. This is
|
||||
the fast path for `!import`, including `!Local` imports.
|
||||
|
||||
View Contract checking is a separate evidence-gathering path. It may load
|
||||
exported direct view types for the symbols that participate in a check. That
|
||||
slower path must remain behind the typed program boundary:
|
||||
|
||||
```text
|
||||
Haskell emits evidence.
|
||||
tricu judges evidence.
|
||||
```
|
||||
|
||||
Reusable view catalogs are ordinary tricu libraries/tree terms, not a separate
|
||||
core CAS artifact kind.
|
||||
|
||||
For locally built workspace modules, advertised direct export views are
|
||||
producer-checked before the manifest alias is written. Producer checking includes
|
||||
advertised views from any imported modules used by that source, so a module
|
||||
cannot publish a local annotated export that contradicts a dependency's exported
|
||||
view. If producer checking fails, the module alias is not written.
|
||||
|
||||
Consumer checking then resolves selected module exports, decodes their
|
||||
`arboricx.view-contract.type.v1` refs, and emits trusted `KnownView` evidence
|
||||
for the local imported symbols. Those facts are module-boundary assumptions:
|
||||
local workspace builds create them after producer-side checking, while external
|
||||
or prebuilt manifests are trusted inputs for now. In all cases, compatibility
|
||||
with local requirements is still judged by the portable checker in `lib/view.tri`.
|
||||
|
||||
## 4. Content Store Direction
|
||||
|
||||
### 4.1 Store core
|
||||
|
||||
The store core should be a content-addressed object store:
|
||||
|
||||
```text
|
||||
hash -> object bytes
|
||||
hash -> object kind / media type
|
||||
hash -> optional metadata/index entries
|
||||
```
|
||||
|
||||
The hash should be over canonical bytes with domain separation. The object kind
|
||||
or media type determines how a client interprets those bytes.
|
||||
|
||||
Current module/check object kinds:
|
||||
|
||||
```text
|
||||
arboricx.module-manifest.v1
|
||||
arboricx.tree-term.v1
|
||||
arboricx.view-contract.type.v1
|
||||
```
|
||||
|
||||
Merkle nodes and indexed bundles remain lower-level Arboricx transport/DAG
|
||||
formats, but they are not the module/eval storage model. typed programs and view
|
||||
catalogs are ordinary tree terms unless a future external tooling use case proves
|
||||
that they need their own object kind.
|
||||
|
||||
The store core should not need to know what a `tricu` definition means.
|
||||
|
||||
### 4.2 Filesystem-backed layout
|
||||
|
||||
The long-term store should converge with the direction already sketched in
|
||||
`lib/arboricx/server.tri`:
|
||||
|
||||
```text
|
||||
store/
|
||||
objects/
|
||||
abc/
|
||||
abc123...object
|
||||
aliases/
|
||||
names/
|
||||
modules/
|
||||
packages/
|
||||
manifests/
|
||||
tmp/
|
||||
```
|
||||
|
||||
SQLite may remain useful as an optional index/cache, but it should not be the
|
||||
canonical store model.
|
||||
|
||||
### 4.3 Structural references, not language dependencies
|
||||
|
||||
The store may understand structural content references when they are part of an
|
||||
object format. For example, a Merkle node naturally references child hashes:
|
||||
|
||||
```text
|
||||
Leaf
|
||||
Stem childHash
|
||||
Fork leftHash rightHash
|
||||
```
|
||||
|
||||
This is not a `tricu` dependency graph. It is content structure.
|
||||
|
||||
Language/tool-level relationships such as "compiled from source", "exported by
|
||||
module", or "checked with contract" can live in manifests or indexes. They
|
||||
should not be required by the store core.
|
||||
|
||||
## 5. Arboricx Role
|
||||
|
||||
Arboricx should be understood as a family of portable Tree Calculus artifact
|
||||
formats, not as a single storage mechanism.
|
||||
|
||||
### 5.1 Arboricx Bundle
|
||||
|
||||
The existing indexed `.arboricx` format remains the preferred transport and
|
||||
execution object:
|
||||
|
||||
- compact;
|
||||
- self-contained;
|
||||
- deterministic;
|
||||
- easy to parse in constrained runtimes;
|
||||
- suitable for deployment and HTTP serving;
|
||||
- structurally verifiable without hash recomputation per node.
|
||||
|
||||
It says:
|
||||
|
||||
```text
|
||||
Here is everything you need, densely packed.
|
||||
```
|
||||
|
||||
### 5.2 Arboricx CAS / Merkle Store
|
||||
|
||||
The persistent store should use content-addressed structural objects:
|
||||
|
||||
```text
|
||||
Leaf
|
||||
Stem childHash
|
||||
Fork leftHash rightHash
|
||||
```
|
||||
|
||||
This enables dedupe across definitions, modules, packages, and versions. A large
|
||||
program that shares subtrees with other programs should not store those subtrees
|
||||
multiple times.
|
||||
|
||||
It says:
|
||||
|
||||
```text
|
||||
Here are immutable objects, addressable independently.
|
||||
```
|
||||
|
||||
### 5.3 Pack and unpack
|
||||
|
||||
Transport and storage should interoperate explicitly:
|
||||
|
||||
```text
|
||||
CAS root(s) -> pack -> indexed Arboricx bundle
|
||||
Arboricx bundle -> unpack -> CAS root(s)
|
||||
```
|
||||
|
||||
The bundle can be treated as an opaque content-addressed blob by the store, and
|
||||
it can also be unpacked into Merkle nodes for dedupe and partial reuse.
|
||||
|
||||
## 6. Modules
|
||||
|
||||
### 6.1 Module identity
|
||||
|
||||
A module should be an immutable manifest object. Its identity is the hash of its
|
||||
canonical manifest bytes.
|
||||
|
||||
A module name is not identity. It is a workspace alias or package-level alias to
|
||||
a module hash.
|
||||
|
||||
### 6.2 Module contents
|
||||
|
||||
A module manifest should primarily be an export map:
|
||||
|
||||
```text
|
||||
module hash
|
||||
exports:
|
||||
name -> content reference
|
||||
metadata:
|
||||
package
|
||||
version
|
||||
description
|
||||
license
|
||||
createdBy
|
||||
optional:
|
||||
view contract artifact refs
|
||||
ABI/media type info
|
||||
source/provenance refs
|
||||
```
|
||||
|
||||
The manifest should be portable and mostly format-oriented. It should not depend
|
||||
on Haskell data structures or `tricu`-specific internal semantics.
|
||||
|
||||
### 6.3 Export entries
|
||||
|
||||
An export entry may eventually look conceptually like:
|
||||
|
||||
```text
|
||||
name: "map"
|
||||
object: sha256:...
|
||||
kind: arboricx.tree-term.v1
|
||||
abi: arboricx.abi.tree.v1
|
||||
view: sha256:... -- optional View Contract artifact
|
||||
source: sha256:... -- optional source/provenance object
|
||||
```
|
||||
|
||||
Executable module exports are complete normalized tree terms stored as one
|
||||
`arboricx.tree-term.v1` object per named export. Merkle-node storage remains
|
||||
available for DAG-oriented tooling, but module/eval imports should not store or
|
||||
hydrate every subtree as a separate filesystem object.
|
||||
|
||||
### 6.4 Import behavior
|
||||
|
||||
Imports should resolve module aliases or content references to module manifests,
|
||||
then bind selected exports into the local source scope.
|
||||
|
||||
Export selection has one intentional aggregator special case:
|
||||
|
||||
```text
|
||||
module with local top-level definitions -> exports only those local definitions
|
||||
module with only imports -> reexports the evaluated import env
|
||||
```
|
||||
|
||||
This lets files such as `prelude.tri` act as explicit barrel modules without
|
||||
making every ordinary module reexport its imports. A module that defines even one
|
||||
local top-level name does not implicitly reexport imported names.
|
||||
|
||||
The future pipeline should be:
|
||||
|
||||
```text
|
||||
parse source
|
||||
resolve imports/names to module exports and content refs
|
||||
lower source using resolved refs
|
||||
emit a view-tree artifact
|
||||
check evidence when requested
|
||||
store/export artifacts
|
||||
```
|
||||
|
||||
It should not be:
|
||||
|
||||
```text
|
||||
paste imported ASTs into one file and rewrite strings
|
||||
```
|
||||
|
||||
## 7. Workspace Layer
|
||||
|
||||
Mutable human-facing state belongs in a workspace layer.
|
||||
|
||||
Examples:
|
||||
|
||||
```text
|
||||
List -> module hash
|
||||
Http -> module hash
|
||||
map -> definition/tree hash
|
||||
selected List version -> module hash
|
||||
package pin prelude -> package/module hash
|
||||
```
|
||||
|
||||
The workspace is where names, selections, pins, and aliases live. Renaming should
|
||||
usually mutate workspace aliases, not immutable content objects.
|
||||
|
||||
This gives humans stable ergonomic names without making names semantic identity.
|
||||
|
||||
## 8. Definition Identity
|
||||
|
||||
There are two useful identities and we should support both.
|
||||
|
||||
### 8.1 Tree identity
|
||||
|
||||
A Tree Calculus value has a Merkle root hash. This identifies the executable tree
|
||||
itself.
|
||||
|
||||
This is the right identity for:
|
||||
|
||||
- execution;
|
||||
- dedupe;
|
||||
- bundle roots;
|
||||
- low-level artifact sharing.
|
||||
|
||||
### 8.2 Module/export identity
|
||||
|
||||
The module manifest is the higher-level artifact boundary. It pairs each export
|
||||
name with its compiled tree term and optional direct View Contract type.
|
||||
|
||||
The content store should not require extra definition/source/provenance objects,
|
||||
and fully untyped Tree Calculus code must remain valid.
|
||||
|
||||
## 9. View Contract Integration
|
||||
|
||||
View Contracts should attach to modules/exports as portable artifacts.
|
||||
|
||||
An imported definition can be assigned a local numeric symbol while lowering a
|
||||
typed program. Its global identity remains a content hash or module export ref.
|
||||
|
||||
This is the intended split:
|
||||
|
||||
```text
|
||||
typed program local symbol: 3
|
||||
Debug label: "List.map"
|
||||
Resolved object: sha256:...
|
||||
Exported view: Fn [...]
|
||||
```
|
||||
|
||||
De Bruijn-style integer symbols are still appropriate inside a typed program. They
|
||||
are local evidence identifiers, not global content identity.
|
||||
|
||||
We should not make global objects depend on numeric checker symbols.
|
||||
|
||||
Untyped code remains valid with no contract artifact. If a boundary needs to
|
||||
participate in checking but has no information, it may use `Any` or rely on
|
||||
policy. We should not pretend all untyped functions have an infinite
|
||||
`Any -> Any -> ...` contract.
|
||||
|
||||
## 10. Import Syntax Direction
|
||||
|
||||
Exact syntax is future work, but the current `!import` form should be considered
|
||||
a transitional mechanism.
|
||||
|
||||
Future imports should distinguish:
|
||||
|
||||
- path-based source imports for local development;
|
||||
- workspace/module alias imports;
|
||||
- explicit content-addressed imports;
|
||||
- selected/exposed names;
|
||||
- qualified versus unqualified binding.
|
||||
|
||||
Possible directions:
|
||||
|
||||
```tri
|
||||
import "./list.tri" as List
|
||||
import List exposing (map foldl)
|
||||
import #abc123... as List
|
||||
```
|
||||
|
||||
The syntax should be designed after the object/module model is clearer.
|
||||
|
||||
## 11. Migration Strategy
|
||||
|
||||
A plausible migration path:
|
||||
|
||||
1. Define the neutral object store model and filesystem layout.
|
||||
2. Implement Merkle node persistence against that layout.
|
||||
3. Add pack/unpack between CAS roots and indexed Arboricx bundles.
|
||||
4. Replace ad hoc SQLite `terms` names/tags with workspace aliases or a clearer
|
||||
index layer.
|
||||
5. Define module manifest objects.
|
||||
6. Teach source imports to resolve manifests/exports instead of rewriting ASTs.
|
||||
7. Attach View Contract artifacts to module exports.
|
||||
8. Gradually migrate existing `lib/` and `demos/` imports.
|
||||
|
||||
Compatibility shims may keep existing `!import` working during migration.
|
||||
|
||||
## 12. Open Questions
|
||||
|
||||
- What exact canonical byte format should store objects use?
|
||||
- Should module manifests be binary, tree-encoded, or both?
|
||||
- What media type/kind registry do we need first?
|
||||
- How should object references be represented in source syntax?
|
||||
- How should workspaces be stored and shared?
|
||||
- What is the minimum useful module manifest?
|
||||
- Should source files compile directly to module manifests, or should manifests
|
||||
be produced by explicit package commands?
|
||||
- How much Arboricx bundle metadata should reference CAS roots?
|
||||
- What GC/reachability model should the store eventually use?
|
||||
|
||||
## 13. Summary
|
||||
|
||||
The desired design is:
|
||||
|
||||
```text
|
||||
Content store:
|
||||
portable CAS for immutable objects and structural references
|
||||
|
||||
Arboricx bundle:
|
||||
compact indexed transport/execution object
|
||||
|
||||
Arboricx CAS:
|
||||
persistent Merkle DAG/object representation for dedupe and partial reuse
|
||||
|
||||
Modules:
|
||||
immutable manifests mapping export names to content objects and optional
|
||||
contracts
|
||||
|
||||
Workspace:
|
||||
mutable human aliases, version selections, and package/module pins
|
||||
|
||||
View Contracts:
|
||||
portable evidence artifacts attached to exports and checked by pure Tree
|
||||
Calculus code
|
||||
```
|
||||
|
||||
The key architectural rule is that hashes provide stable identity, while names
|
||||
provide human usability. The module system should be built on that separation.
|
||||
@@ -327,7 +327,7 @@ err code rest
|
||||
The error code is a Tree Calculus number. Error constants are defined in:
|
||||
|
||||
- `lib/binary.tri`
|
||||
- `lib/arboricx-common.tri`
|
||||
- `lib/arboricx/common.tri`
|
||||
- `lib/arboricx.tri` for Host ABI codec errors, currently `errHostCodecFailed = 14`
|
||||
|
||||
Typed runners return `errHostCodecFailed` if the application result cannot be interpreted as the requested type.
|
||||
|
||||
582
docs/view-contract-syntax.md
Normal file
582
docs/view-contract-syntax.md
Normal file
@@ -0,0 +1,582 @@
|
||||
# View Contract Syntax Design
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
This document specifies source-level syntax sugar for emitting View Contract
|
||||
metadata from annotated `tricu` definitions.
|
||||
|
||||
The syntax is frontend sugar. It lowers to ordinary typed-program nodes consumed
|
||||
by the portable checker in `lib/view.tri` and catalog helpers in
|
||||
`lib/views/catalog.tri`.
|
||||
|
||||
The checker remains independent of source syntax.
|
||||
|
||||
## 2. Definition Annotations
|
||||
|
||||
A definition may carry argument and return view annotations directly in its head.
|
||||
|
||||
```tri
|
||||
name arg1@Type1 arg2@Type2 =@ReturnType body
|
||||
```
|
||||
|
||||
This declares:
|
||||
|
||||
```text
|
||||
name : Fn [Type1 Type2] ReturnType
|
||||
arg1 : Type1
|
||||
arg2 : Type2
|
||||
```
|
||||
|
||||
and lowers to View Contract metadata:
|
||||
|
||||
```tri
|
||||
typedDeclareFn nameSym [(Type1) (Type2)] ReturnType t
|
||||
typedValue arg1Sym Type1 t
|
||||
typedValue arg2Sym Type2 t
|
||||
```
|
||||
|
||||
If body flow metadata is emitted, the body result is required to satisfy the
|
||||
appropriate residual view.
|
||||
|
||||
## 3. Syntax Forms
|
||||
|
||||
### 3.1 Binder annotation
|
||||
|
||||
```tri
|
||||
x@Bool
|
||||
xs@(List Bool)
|
||||
f@(Fn [Bool] String)
|
||||
```
|
||||
|
||||
A binder annotation introduces a normal term binder and contributes an argument
|
||||
view to the function contract.
|
||||
|
||||
### 3.2 Phantom argument annotation
|
||||
|
||||
```tri
|
||||
name @A @B =@C body
|
||||
```
|
||||
|
||||
A phantom argument annotation contributes an argument view to the function
|
||||
contract but introduces no term binder.
|
||||
|
||||
This is useful for point-free and combinator-heavy definitions.
|
||||
|
||||
```tri
|
||||
name @A @B =@C body
|
||||
```
|
||||
|
||||
declares:
|
||||
|
||||
```text
|
||||
name : Fn [A B] C
|
||||
```
|
||||
|
||||
The body itself must satisfy the residual function view:
|
||||
|
||||
```text
|
||||
Fn [A B] C
|
||||
```
|
||||
|
||||
### 3.3 Binder prefix with phantom tail
|
||||
|
||||
Phantom annotations may appear after binder annotations:
|
||||
|
||||
```tri
|
||||
name x@A @B =@C body
|
||||
```
|
||||
|
||||
This declares:
|
||||
|
||||
```text
|
||||
name : Fn [A B] C
|
||||
x : A
|
||||
```
|
||||
|
||||
The body must satisfy:
|
||||
|
||||
```text
|
||||
Fn [B] C
|
||||
```
|
||||
|
||||
This allows a named binder prefix with a point-free tail.
|
||||
|
||||
### 3.4 Return annotation
|
||||
|
||||
```tri
|
||||
name x@A =@B body
|
||||
name =@B body
|
||||
```
|
||||
|
||||
`=@B` contributes the result view.
|
||||
|
||||
A definition with no arguments and a return annotation is a value contract, not a
|
||||
zero-arity function contract:
|
||||
|
||||
```tri
|
||||
name =@Bool body
|
||||
```
|
||||
|
||||
lowers to:
|
||||
|
||||
```tri
|
||||
typedValue nameSym viewBool t
|
||||
```
|
||||
|
||||
not:
|
||||
|
||||
```tri
|
||||
typedDeclareFn nameSym [] viewBool t
|
||||
```
|
||||
|
||||
## 4. Ordering Rule
|
||||
|
||||
Phantom argument annotations may only appear at the end of the argument list.
|
||||
|
||||
Valid:
|
||||
|
||||
```tri
|
||||
foo x@A y@B =@C body
|
||||
foo @A @B =@C body
|
||||
foo x@A @B =@C body
|
||||
foo x y@B @C =@D body
|
||||
```
|
||||
|
||||
Invalid:
|
||||
|
||||
```tri
|
||||
foo x@A @B z@C =@D body
|
||||
foo @A x@B =@C body
|
||||
```
|
||||
|
||||
Once a phantom `@Type` item appears, no later named binder may appear.
|
||||
|
||||
## 5. Contract-Bearing Definitions
|
||||
|
||||
A definition is contract-bearing if its head contains any of:
|
||||
|
||||
```text
|
||||
binder@Type
|
||||
@Type
|
||||
=@Type
|
||||
```
|
||||
|
||||
Ordinary unannotated definitions do not emit View Contract metadata.
|
||||
|
||||
```tri
|
||||
foo x y = body
|
||||
```
|
||||
|
||||
emits no contract metadata.
|
||||
|
||||
## 6. Unannotated Binders in Contract-Bearing Heads
|
||||
|
||||
In a contract-bearing definition, an unannotated binder contributes `Any`.
|
||||
|
||||
```tri
|
||||
foo x y@Bool =@String body
|
||||
```
|
||||
|
||||
means:
|
||||
|
||||
```text
|
||||
foo : Fn [Any Bool] String
|
||||
x : Any
|
||||
y : Bool
|
||||
```
|
||||
|
||||
This keeps mixed annotation lightweight without emitting contracts for fully
|
||||
unannotated definitions.
|
||||
|
||||
## 7. Missing Return Annotation
|
||||
|
||||
If a contract-bearing definition has argument annotations but no return
|
||||
annotation, the return view defaults to `Any`.
|
||||
|
||||
```tri
|
||||
foo x@Bool = body
|
||||
```
|
||||
|
||||
means:
|
||||
|
||||
```text
|
||||
foo : Fn [Bool] Any
|
||||
x : Bool
|
||||
```
|
||||
|
||||
## 8. Type Annotation Grammar
|
||||
|
||||
Annotations are intentionally small at the attachment site.
|
||||
|
||||
After `@` or `=@`, the parser accepts either a single atomic view expression or
|
||||
a parenthesized compound view expression.
|
||||
|
||||
Valid:
|
||||
|
||||
```tri
|
||||
x@Bool
|
||||
x@(List Bool)
|
||||
f@(Fn [Bool] String)
|
||||
r@(Result String Bool)
|
||||
name =@Bool body
|
||||
name =@(List Bool) body
|
||||
```
|
||||
|
||||
These are not structural annotations:
|
||||
|
||||
```tri
|
||||
x@List Bool
|
||||
f@Fn [Bool] String
|
||||
name =@List Bool body
|
||||
```
|
||||
|
||||
They are parsed according to normal definition-head rules. For example,
|
||||
`x@List Bool` means binder `x` has the atomic view expression `List`, followed by
|
||||
an unannotated binder named `Bool`. Use parentheses when the annotation itself is
|
||||
an application.
|
||||
|
||||
## 9. Type Grammar
|
||||
|
||||
View expressions are ordinary value-level expressions in a restricted annotation
|
||||
grammar:
|
||||
|
||||
```text
|
||||
ViewExpr
|
||||
= name
|
||||
| integer
|
||||
| [ViewExpr...]
|
||||
| ViewExpr ViewExpr
|
||||
| (ViewExpr)
|
||||
```
|
||||
|
||||
Built-in names lower to standard view values:
|
||||
|
||||
```text
|
||||
Any -> viewAny
|
||||
Bool -> viewBool
|
||||
String -> viewString
|
||||
Byte -> viewByte
|
||||
Unit -> viewUnit
|
||||
```
|
||||
|
||||
Atomic refs lower explicitly. String refs are the preferred user-facing form;
|
||||
numeric refs remain available for low-level/generated code:
|
||||
|
||||
```text
|
||||
Ref "Nat" -> viewRef "Nat"
|
||||
Ref 10 -> viewRef 10
|
||||
```
|
||||
|
||||
Additional named views and view constructors are ordinary `tricu` values:
|
||||
|
||||
```tri
|
||||
Nat = viewRef "Nat"
|
||||
Box a = viewPair (viewRef "Box") a
|
||||
|
||||
idNat x@Nat =@Nat x
|
||||
idBox x@(Box String) =@(Box String) x
|
||||
```
|
||||
|
||||
The frontend resolves names and evaluates view expressions, but well-formedness
|
||||
is judged by the self-hosted checker (`wellFormedView?` in `lib/view.tri`).
|
||||
Malformed view values are rejected when checked or published.
|
||||
|
||||
## 10. List Syntax in Types
|
||||
|
||||
Function argument lists use the source type grammar:
|
||||
|
||||
```tri
|
||||
Fn [Bool String] Unit
|
||||
Fn [(List Bool) (Maybe String)] Unit
|
||||
```
|
||||
|
||||
The lowered typed program must still respect ordinary `tricu` list syntax, where
|
||||
each list element is parenthesized when needed:
|
||||
|
||||
```tri
|
||||
viewFn [(viewBool) (viewString)] viewUnit
|
||||
```
|
||||
|
||||
## 11. Residual Body View
|
||||
|
||||
For a contract-bearing definition, the full definition view is always:
|
||||
|
||||
```text
|
||||
Fn [allArgumentViews...] returnView
|
||||
```
|
||||
|
||||
except for nullary value annotations, which use the return view directly.
|
||||
|
||||
The body obligation depends on how many argument views are represented by named
|
||||
binders in the definition head.
|
||||
|
||||
Let:
|
||||
|
||||
```text
|
||||
argViews = [A B C]
|
||||
returnView = R
|
||||
binderCount = number of named binders before the phantom tail
|
||||
remaining = drop binderCount argViews
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```text
|
||||
bodyRequiredView = residual(remaining, returnView)
|
||||
```
|
||||
|
||||
where:
|
||||
|
||||
```text
|
||||
residual([], R) = R
|
||||
residual([A ...], R) = Fn [A ...] R
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
```tri
|
||||
foo x@A y@B =@C body
|
||||
```
|
||||
|
||||
Body required view:
|
||||
|
||||
```text
|
||||
C
|
||||
```
|
||||
|
||||
```tri
|
||||
foo @A @B =@C body
|
||||
```
|
||||
|
||||
Body required view:
|
||||
|
||||
```text
|
||||
Fn [A B] C
|
||||
```
|
||||
|
||||
```tri
|
||||
foo x@A @B =@C body
|
||||
```
|
||||
|
||||
Body required view:
|
||||
|
||||
```text
|
||||
Fn [B] C
|
||||
```
|
||||
|
||||
## 12. Lowering Examples
|
||||
|
||||
### 12.1 Fully annotated binders
|
||||
|
||||
Source:
|
||||
|
||||
```tri
|
||||
foo x@Bool xs@(List Bool) =@String body
|
||||
```
|
||||
|
||||
Definition contract:
|
||||
|
||||
```tri
|
||||
typedDeclareFn fooSym [(viewBool) (viewList viewBool)] viewString t
|
||||
typedValue xSym viewBool t
|
||||
typedValue xsSym (viewList viewBool) t
|
||||
```
|
||||
|
||||
Body obligation:
|
||||
|
||||
```tri
|
||||
typedRequire bodySym viewString t
|
||||
```
|
||||
|
||||
### 12.2 Pure phantom signature
|
||||
|
||||
Source:
|
||||
|
||||
```tri
|
||||
foo @Bool @(List Bool) =@String body
|
||||
```
|
||||
|
||||
Definition contract:
|
||||
|
||||
```tri
|
||||
typedDeclareFn fooSym [(viewBool) (viewList viewBool)] viewString t
|
||||
```
|
||||
|
||||
Body obligation:
|
||||
|
||||
```tri
|
||||
typedRequire bodySym (viewFn [(viewBool) (viewList viewBool)] viewString) t
|
||||
```
|
||||
|
||||
### 12.3 Binder prefix with phantom tail
|
||||
|
||||
Source:
|
||||
|
||||
```tri
|
||||
foo x@Bool @(List Bool) =@String body
|
||||
```
|
||||
|
||||
Definition contract:
|
||||
|
||||
```tri
|
||||
typedDeclareFn fooSym [(viewBool) (viewList viewBool)] viewString t
|
||||
typedValue xSym viewBool t
|
||||
```
|
||||
|
||||
Body obligation:
|
||||
|
||||
```tri
|
||||
typedRequire bodySym (viewFn [(viewList viewBool)] viewString) t
|
||||
```
|
||||
|
||||
### 12.4 Value annotation
|
||||
|
||||
Source:
|
||||
|
||||
```tri
|
||||
message =@String "hello"
|
||||
```
|
||||
|
||||
Definition contract:
|
||||
|
||||
```tri
|
||||
typedValue messageSym viewString t
|
||||
```
|
||||
|
||||
Body obligation:
|
||||
|
||||
```tri
|
||||
typedRequire bodySym viewString t
|
||||
```
|
||||
|
||||
## 13. `tricu check`
|
||||
|
||||
`tricu check` consumes an annotated program, lowers annotations to typed program
|
||||
metadata, runs the checker, and reports either `ok` or rendered diagnostics.
|
||||
|
||||
Initial behavior:
|
||||
|
||||
```bash
|
||||
tricu check path/to/program.tri
|
||||
```
|
||||
|
||||
outputs checker success or errors. Diagnostics are rendered by the portable
|
||||
checker, then annotated by the frontend with source/debug labels when available:
|
||||
|
||||
```tri
|
||||
id x@String =@Bool x
|
||||
```
|
||||
|
||||
reports:
|
||||
|
||||
```text
|
||||
symbol 1 (x) expected Bool but got String
|
||||
```
|
||||
|
||||
Application result labels include the application head when known:
|
||||
|
||||
```tri
|
||||
xs =@(List String) [(g "hi")]
|
||||
g y@String =@Bool y
|
||||
```
|
||||
|
||||
reports:
|
||||
|
||||
```text
|
||||
symbol 3 (g application result) expected String but got Bool
|
||||
```
|
||||
|
||||
These labels are presentation-only metadata. The checker still judges only the
|
||||
emitted typed-program evidence.
|
||||
|
||||
Future behavior may include:
|
||||
|
||||
```bash
|
||||
tricu check --out path/to/executable.arboricx path/to/program.tri
|
||||
```
|
||||
|
||||
which checks an annotated source program and emits an executable Arboricx bundle.
|
||||
|
||||
The checker library remains available independently of the CLI workflow.
|
||||
|
||||
## 14. Frontend Lowering Boundaries
|
||||
|
||||
The annotation syntax is frontend sugar. The canonical checker input remains a
|
||||
plain typed program: ordinary `typedValue`, `typedDeclareFn`,
|
||||
`typedRequire`, and `typedApply` nodes represented as portable `tricu`
|
||||
data.
|
||||
|
||||
The frontend may emit richer evidence from source forms, but it does not decide
|
||||
semantic compatibility. In short:
|
||||
|
||||
```text
|
||||
Haskell emits evidence.
|
||||
tricu judges evidence.
|
||||
```
|
||||
|
||||
Current source-driven evidence includes:
|
||||
|
||||
- literal views for strings, bytes, unit, and homogeneous list literals;
|
||||
- expected element requirements for `List T` bodies;
|
||||
- expected `Fn` requirements for lambda literals and curried application spines;
|
||||
- application argument requirements when the callee has a known `Fn` view;
|
||||
- expected constructor flow for unshadowed stdlib constructors:
|
||||
- `pair` with expected `Pair A B`;
|
||||
- `just` and `nothing` with expected `Maybe A`;
|
||||
- `ok` and `err` with expected `Result E A`.
|
||||
|
||||
Constructor lowering only applies when the constructor name is not shadowed by a
|
||||
local binder or top-level definition in the checked source. If a program defines
|
||||
its own `pair`, `just`, `nothing`, `ok`, or `err`, checking falls back to normal
|
||||
application evidence.
|
||||
|
||||
For tooling and regression tests, the frontend exposes a lowering-only API that
|
||||
returns emitted typed program text without invoking the checker:
|
||||
|
||||
```hs
|
||||
lowerSource :: String -> Either String String
|
||||
```
|
||||
|
||||
It also exposes debug labels for symbols:
|
||||
|
||||
```hs
|
||||
lowerSourceWithDebug :: String -> Either String (String, Map Integer String)
|
||||
```
|
||||
|
||||
Debug labels are presentation metadata only. They are not part of checker
|
||||
semantics and are not consumed by `lib/view.tri`.
|
||||
|
||||
`do` blocks have no separate View Contract semantics. The parser lowers them
|
||||
through their explicit bind operator:
|
||||
|
||||
```tri
|
||||
do bind
|
||||
x <- action
|
||||
next x
|
||||
```
|
||||
|
||||
becomes ordinary application/lambda structure. Checking then follows the known
|
||||
`Fn` view of the bind operator, including the callback argument view when it is
|
||||
available.
|
||||
|
||||
## 15. Summary
|
||||
|
||||
The annotation syntax is:
|
||||
|
||||
```tri
|
||||
name arg@A arg2@B =@C body
|
||||
name @A @B =@C body
|
||||
name arg@A @B =@C body
|
||||
name =@C body
|
||||
```
|
||||
|
||||
Core rules:
|
||||
|
||||
1. Binder annotations introduce binders and argument views.
|
||||
2. Phantom annotations introduce argument views only.
|
||||
3. Phantom annotations may only appear after all binders.
|
||||
4. Unannotated binders in contract-bearing heads contribute `Any`.
|
||||
5. Missing return annotations in contract-bearing heads default to `Any`.
|
||||
6. Nullary `=@T` definitions are value contracts, not zero-arity functions.
|
||||
7. Compound annotation types must be parenthesized.
|
||||
8. Lowering emits ordinary typed-program nodes for the existing checker.
|
||||
516
docs/view-contracts.md
Normal file
516
docs/view-contracts.md
Normal file
@@ -0,0 +1,516 @@
|
||||
# View Contracts and View Trees
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
View Contracts are the portable checking layer for Tree Calculus programs.
|
||||
|
||||
The checker does not consume detached metadata about a separate executable. Its
|
||||
canonical input is a typed, checkable tree artifact: ordinary tree data that
|
||||
contains both the executable program payloads and the view/contract structure
|
||||
needed to validate and transform them.
|
||||
|
||||
The checker consumes this artifact and returns either:
|
||||
|
||||
```text
|
||||
checked-execution artifact
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```text
|
||||
structured diagnostic
|
||||
```
|
||||
|
||||
A checked-execution artifact is interpreted by `runChecked`. Unguarded programs
|
||||
are represented as `checkedPure rootPayload`; guarded programs contain explicit
|
||||
checked guard/bind nodes.
|
||||
|
||||
This keeps checking independent of any particular host implementation. A typed
|
||||
artifact may be produced by any frontend, compiler, hand-written generator, or
|
||||
future self-hosted `tricu` toolchain.
|
||||
|
||||
## 2. Design Principle
|
||||
|
||||
The model follows the same discipline as interaction trees.
|
||||
|
||||
Interaction trees use tagged structural envelopes with explicit executable
|
||||
payloads:
|
||||
|
||||
```tri
|
||||
io action = pair "tricuIO" (pair version action)
|
||||
pure x = pair 0 x
|
||||
bind action k = pair 1 (pair action k)
|
||||
```
|
||||
|
||||
The interpreter understands the outer structure, but it does not recursively
|
||||
mistake every subtree for interpreter metadata. A continuation `k` is an opaque
|
||||
executable tree until the interpreter reaches the `bind` step that applies it.
|
||||
|
||||
View trees use the same rule:
|
||||
|
||||
```text
|
||||
structure says how to check;
|
||||
opaque executable fields are only executed/applied by the checker at the
|
||||
appropriate step.
|
||||
```
|
||||
|
||||
This is the key distinction that allows Views to carry guards without confusing
|
||||
ordinary program trees with View metadata.
|
||||
|
||||
## 3. Views
|
||||
|
||||
A View is an extrinsic contract over an ordinary Tree Calculus value. Tree
|
||||
Calculus values do not carry native runtime types; a View describes how a value
|
||||
may be treated by the checker or by a checked boundary.
|
||||
|
||||
Core View forms:
|
||||
|
||||
```text
|
||||
Any
|
||||
Ref ref
|
||||
Fn [argView...] resultView
|
||||
List elemView
|
||||
Maybe elemView
|
||||
Pair leftView rightView
|
||||
Result errView okView
|
||||
Guarded baseView guard
|
||||
```
|
||||
|
||||
`Ref` supports both generated/numeric and symbolic references. Symbolic refs are
|
||||
preferred for user-authored views:
|
||||
|
||||
```tri
|
||||
UserId = viewRef "UserId"
|
||||
```
|
||||
|
||||
A guarded view refines a base view with an executable guard:
|
||||
|
||||
```tri
|
||||
UserId = viewGuarded (viewRef "UserId") userIdGuard
|
||||
```
|
||||
|
||||
The guard is ordinary program code. The View validator checks that the guarded
|
||||
view envelope is well-formed, and recursively validates the `baseView`, but it
|
||||
must treat the guard payload/reference as opaque executable data, not as another
|
||||
View.
|
||||
|
||||
## 4. Polymorphic and Abstract Views
|
||||
|
||||
View Contracts support portable polymorphism over Views. The View language is
|
||||
interpreted by the same portable checker model implemented in `tricu` terms.
|
||||
|
||||
Source syntax may use underscore-prefixed names as View variables inside
|
||||
annotations:
|
||||
|
||||
```tri
|
||||
id x@_a =@_a x
|
||||
const x@_a y@_b =@_a x
|
||||
compose f@(Fn [_b] _c) g@(Fn [_a] _b) x@_a =@_c f (g x)
|
||||
```
|
||||
|
||||
In the portable artifact, these lower to scoped View binders rather than
|
||||
unscoped source-name conventions. This fits the existing View encoding style:
|
||||
Views are tagged records with numeric tags and tagged fields. Polymorphic forms
|
||||
are View records such as:
|
||||
|
||||
```text
|
||||
Var localId
|
||||
Forall binders body
|
||||
Exists binders body
|
||||
```
|
||||
|
||||
The current durable encoding uses stable local binder IDs. For example,
|
||||
`id x@_a =@_a x` exports a shape equivalent to:
|
||||
|
||||
```text
|
||||
Forall [0] (Fn [Var 0] (Var 0))
|
||||
```
|
||||
|
||||
Source names like `_a` are for authoring; the artifact carries binder scope and
|
||||
local IDs rather than relying on source-name identity.
|
||||
|
||||
`Forall` supports generic contracts:
|
||||
|
||||
```tri
|
||||
map f@(Fn [_a] _b) xs@(List _a) =@(List _b) ...
|
||||
head xs@(NonEmptyList _a) =@_a ...
|
||||
```
|
||||
|
||||
At each checked use, the checker instantiates quantified variables into
|
||||
use-local internal variables and solves View compatibility constraints. The
|
||||
portable checker uses structural use-local IDs rather than expensive numeric
|
||||
freshening, and treats unconstrained variable-variable matches as constraints
|
||||
that do not create substitution cycles. Concrete observations still bind these
|
||||
variables when enough information is available. This is what lets explicitly
|
||||
annotated higher-order boundaries accept polymorphic values, for example
|
||||
`compose id id "x"`, and lets quantified values satisfy concrete requirements
|
||||
such as `Fn [String] String`. It gives useful polymorphic contracts for
|
||||
explicitly declared/imported View facts.
|
||||
|
||||
`Exists` supports checked abstraction boundaries. A module can expose a value as
|
||||
"some representation `_repr` plus capabilities over `_repr`":
|
||||
|
||||
```text
|
||||
Exists _repr.
|
||||
Pair
|
||||
(Fn [String] _repr) -- constructor
|
||||
(Fn [_repr] String) -- renderer / eliminator
|
||||
```
|
||||
|
||||
This does not make raw Tree Calculus inspection impossible. Unchecked code can
|
||||
always inspect trees. It means checked clients cannot justify
|
||||
representation-specific operations through the View system unless the package
|
||||
exports an appropriate capability or eliminator.
|
||||
|
||||
This leads to an important distinction for future checked subsets:
|
||||
|
||||
```text
|
||||
controlled observation: Bool/List/Maybe/Result/etc. eliminators with Views
|
||||
raw observation: direct tree-shape inspection through triage-like power
|
||||
```
|
||||
|
||||
Useful application code can live mostly in the controlled fragment and receive
|
||||
explicit View validation over lambdas, application, let, and typed eliminators.
|
||||
Low-level library code may still use raw intensionality, but should expose
|
||||
disciplined Views and capabilities above it. Scott-encoded constructors and
|
||||
eliminators are a natural tricu-native representation for these APIs.
|
||||
|
||||
Tree Calculus terms do not carry intrinsic principal Views, and raw intensional
|
||||
code can invalidate parametric claims. View Contracts are an explicit evidence
|
||||
and contract layer over tricu programs; limited polymorphic Views are supported
|
||||
when they are declared or imported as facts with provenance.
|
||||
|
||||
The first stdlib annotation island starts with parametric functions that do not
|
||||
inspect representation:
|
||||
|
||||
```tri
|
||||
id x@_a =@_a x
|
||||
const x@_a y@_b =@_a x
|
||||
compose f@(Fn [_b] _c) g@(Fn [_a] _b) x@_a =@_c f (g x)
|
||||
```
|
||||
|
||||
Re-export-only modules preserve imported View metadata, so these contracts flow
|
||||
through `prelude` rather than only through direct `base` imports.
|
||||
|
||||
Functions built on raw `t`/`triage` should enter the checked world through
|
||||
trusted, controlled eliminator contracts rather than by treating arbitrary raw
|
||||
inspection as parametric.
|
||||
|
||||
|
||||
## 5. Guards
|
||||
|
||||
Guards are ordinary `tricu` values/functions grouped with the Views they refine.
|
||||
|
||||
Example:
|
||||
|
||||
```tri
|
||||
userIdGuard = value :
|
||||
-- ordinary program that validates value
|
||||
|
||||
UserId = viewGuarded (viewRef "UserId") userIdGuard
|
||||
|
||||
loadUser id@UserId = ...
|
||||
```
|
||||
|
||||
Guards return the standard checked-runtime protocol:
|
||||
|
||||
```tri
|
||||
guardOk value
|
||||
guardFail
|
||||
```
|
||||
|
||||
Guards do not author diagnostics. The checked-exec runner owns guard failure and
|
||||
malformed-guard diagnostics using boundary context from the checked artifact.
|
||||
|
||||
Guards are injected by the checker. They are not discovered by the runtime as a
|
||||
separate metadata layer. The checking process transforms a view tree into an
|
||||
executable tree with the necessary guard applications inserted.
|
||||
|
||||
## 6. View Tree Artifact
|
||||
|
||||
The primary checker-facing artifact is a view executable term graph.
|
||||
|
||||
Conceptually:
|
||||
|
||||
```text
|
||||
ViewTree
|
||||
version
|
||||
root node id
|
||||
nodes
|
||||
```
|
||||
|
||||
Each node is tagged tree data. Nodes combine executable payloads, view claims,
|
||||
and structural relationships in one graph.
|
||||
|
||||
Representative node forms:
|
||||
|
||||
```text
|
||||
Value node view executableTree
|
||||
Apply node calleeNode argNode expectedOrInferredView
|
||||
Require node requiredView sourceNode
|
||||
External node name view
|
||||
```
|
||||
|
||||
This is not a mandatory final encoding; it is the semantic target. The important
|
||||
property is that executable trees and checking structure are carried together in
|
||||
a single portable artifact.
|
||||
|
||||
A node may contain opaque executable fields. Those fields are tree terms, but
|
||||
they are not recursively decoded as view-tree nodes or Views unless the node's
|
||||
semantics explicitly says so.
|
||||
|
||||
View facts may also carry explicit per-fact trust provenance:
|
||||
|
||||
```text
|
||||
Checked -- derived by checked lowering / checker validation
|
||||
Trusted -- asserted by a trusted boundary, e.g. a primitive eliminator API
|
||||
Unchecked -- raw or assumed; no parametricity/abstraction guarantee
|
||||
```
|
||||
|
||||
In the portable view-tree envelope this provenance is represented as an optional
|
||||
field on `typedValue` / `typedRequire` facts. In module manifests the same
|
||||
provenance is carried beside the exported View Contract object reference so that
|
||||
imports and re-exports preserve it without relying on module-level convention.
|
||||
Absent provenance is interpreted conservatively as `Unchecked` at use sites.
|
||||
|
||||
For parametric checked definitions, the frontend now performs a conservative
|
||||
raw-intensionality dependency pass over local definitions. If a definition with
|
||||
scoped View variables depends directly or indirectly on raw `triage` / raw `t`
|
||||
construction, or on an imported `Unchecked` fact, lowering fails and asks the
|
||||
author to route observation through a trusted eliminator boundary. This is
|
||||
intentionally provenance/dependency based; it is not an attempt to decide
|
||||
whether arbitrary Tree Calculus reduction will ever reach rule 3.
|
||||
|
||||
View facts can be authored as ordinary value-level Tree Calculus metadata under
|
||||
one conventional top-level name:
|
||||
|
||||
```text
|
||||
viewFacts = [fact ...]
|
||||
fact = pair exportName (pair provenance view)
|
||||
```
|
||||
|
||||
where `exportName` is a string naming a value exported by the module,
|
||||
`provenance` is `0 = Checked`, `1 = Trusted`, or `2 = Unchecked`, and `view` is
|
||||
the same portable View record used by `view-tree` artifacts. The host evaluates
|
||||
this value and decodes the data schema; it does not infer trust from source
|
||||
syntax, AST shape, module name, or a Haskell-side catalog.
|
||||
|
||||
The initial trusted eliminator facts are authored this way in clearly separated
|
||||
stdlib `viewFacts` sections:
|
||||
|
||||
```text
|
||||
matchBool : forall r. r -> r -> Bool -> r
|
||||
matchMaybe : forall a r. r -> (a -> r) -> Maybe a -> r
|
||||
matchList : forall a r. r -> (a -> List a -> r) -> List a -> r
|
||||
```
|
||||
|
||||
The `base` module provides small `facts*` authoring helpers for this advanced
|
||||
metadata, e.g. `factsFact`, `factsChecked`, `factsTrusted`, `factsUnchecked`,
|
||||
`factsForall`, `factsFn`, `factsVar`, `factsBool`, `factsString`, `factsByte`,
|
||||
`factsUnit`, `factsMaybe`, and `factsList`. These helpers construct ordinary
|
||||
Tree data; authority comes from the exported `viewFacts` value and its explicit
|
||||
provenance tags. Loader validation rejects duplicate fact names and facts for
|
||||
names the module does not export.
|
||||
|
||||
Initial derived stdlib annotations using this trusted kernel include:
|
||||
|
||||
```text
|
||||
maybeMap : forall a b. (a -> b) -> Maybe a -> Maybe b
|
||||
maybeBind : forall a b. Maybe a -> (a -> Maybe b) -> Maybe b
|
||||
maybeOr : forall a. a -> Maybe a -> a
|
||||
```
|
||||
|
||||
Recursive list combinators are currently published as explicit `Trusted`
|
||||
value-level facts rather than `Checked` source annotations, because their bodies
|
||||
pass through raw fixed-point machinery that the conservative parametric taint
|
||||
pass intentionally does not prove safe. This is the stabilized boundary: raw
|
||||
stdlib kernels establish conventions with explicit authority; ordinary checked
|
||||
clients consume those facts rather than re-proving the internals.
|
||||
|
||||
```text
|
||||
headMaybe / lastMaybe / nthMaybe
|
||||
append / map / filter / foldl / foldr
|
||||
length / reverse / snoc / count / all? / any? / intersect
|
||||
take / drop / splitAt / concatMap / find / partition / zipWith
|
||||
string/list-byte helpers such as strLength, startsWith?, lines, words
|
||||
```
|
||||
|
||||
## 7. Checker Semantics
|
||||
|
||||
The checker is an interpreter over the view tree.
|
||||
|
||||
For each node it may:
|
||||
|
||||
1. validate the node envelope;
|
||||
2. validate Views referenced by the node;
|
||||
3. check compatibility between expected and actual Views;
|
||||
4. recursively check child nodes;
|
||||
5. inject guards required by guarded Views;
|
||||
6. produce the executable tree for that node;
|
||||
7. memoize node results by node id.
|
||||
|
||||
The root node result is a checked-execution program.
|
||||
|
||||
In abstract form:
|
||||
|
||||
```text
|
||||
checkViewTree : ViewTree -> Result CheckedExec Diagnostic
|
||||
```
|
||||
|
||||
or, in self-hosted terms:
|
||||
|
||||
```tri
|
||||
checkViewTree viewTree = ... -- ok checkedExec / err diagnostic
|
||||
```
|
||||
|
||||
## 8. Compatibility and Guard Injection
|
||||
|
||||
Structural compatibility is about Views. Guard injection is about producing the
|
||||
checked-execution tree.
|
||||
|
||||
For example, if a node is required to satisfy:
|
||||
|
||||
```tri
|
||||
viewGuarded (viewRef "UserId") userIdGuard
|
||||
```
|
||||
|
||||
then the checker verifies the underlying View relationship and emits executable
|
||||
code that applies `userIdGuard` at the appropriate checked boundary.
|
||||
|
||||
The checker, not the runtime metadata system, owns this transformation.
|
||||
|
||||
## 9. Source Annotations
|
||||
|
||||
Source annotations are one frontend syntax for producing view-tree nodes.
|
||||
|
||||
Examples:
|
||||
|
||||
```tri
|
||||
Nat = viewRef "Nat"
|
||||
Box a = viewPair (viewRef "Box") a
|
||||
|
||||
idNat x@Nat =@Nat x
|
||||
idBox x@(Box String) =@(Box String) x
|
||||
```
|
||||
|
||||
Annotations are value-level View expressions. Names such as `Nat` and `Box` are
|
||||
ordinary program values/functions that evaluate to Views.
|
||||
|
||||
A frontend that supports this syntax should lower the source into a view tree
|
||||
that contains the relevant executable terms, views, and checking structure. The
|
||||
artifact must not depend on source names or on the frontend implementation that
|
||||
produced it.
|
||||
|
||||
## 10. Contract Expressions
|
||||
|
||||
Contract-expression helpers remain useful as authoring/building tools, but they
|
||||
are not the fundamental artifact model.
|
||||
|
||||
Preferred style for expression-oriented authoring is pipeline-first:
|
||||
|
||||
```tri
|
||||
mapBoolStringUse = cFn <|
|
||||
[(viewFn [(viewBool)] viewString) (viewList viewBool)] (viewList viewString)
|
||||
|> cApply (cFn [(viewBool)] viewString)
|
||||
|> cApply (cValue (viewList viewBool))
|
||||
|> cRequire (viewList viewString)
|
||||
```
|
||||
|
||||
These helpers should be understood as convenient ways to build typed/checkable
|
||||
structure, not as a permanent replacement for view-tree artifacts.
|
||||
|
||||
## 11. Artifact Direction
|
||||
|
||||
The target direction is to make the view tree the canonical checked-program
|
||||
artifact.
|
||||
|
||||
Older split concepts remain useful internally or during development:
|
||||
|
||||
```text
|
||||
tree term
|
||||
view value
|
||||
typed-program node
|
||||
module/export manifest
|
||||
```
|
||||
|
||||
But the durable design should avoid treating contracts as detached facts about a
|
||||
separate program. The portable checker input is the checkable program itself.
|
||||
|
||||
In short:
|
||||
|
||||
```text
|
||||
Do not store code over here and contracts over there.
|
||||
Store a view tree: executable code plus the structure needed to check and guard it.
|
||||
```
|
||||
|
||||
## 12. IO Interaction Trees
|
||||
|
||||
`tricu` IO is represented as ordinary interaction-tree data:
|
||||
|
||||
```tri
|
||||
io action = pair "tricuIO" (pair version action)
|
||||
pure value = pair 0 value
|
||||
bind action k = pair 1 (pair action k)
|
||||
```
|
||||
|
||||
View Contracts do not change that representation. A checked program may produce
|
||||
an ordinary IO interaction tree, and the existing IO driver can execute it
|
||||
unchanged.
|
||||
|
||||
For source evaluation with contracts enabled, `tricu eval --io` performs an
|
||||
additional frontend instrumentation pass over visible IO continuations. When a
|
||||
continuation returns a `pure (...)` value that mentions source-annotated
|
||||
functions, the frontend lowers that pure expression into the existing portable
|
||||
checked-exec protocol before returning the next IO action.
|
||||
|
||||
This means source sugar works for practical checked IO paths such as:
|
||||
|
||||
```tri
|
||||
acceptNames xs@(NonEmptyList String) =@String "accepted"
|
||||
|
||||
main = io (bind (pure []) (xs : pure (acceptNames xs)))
|
||||
```
|
||||
|
||||
and for explicit higher-order boundaries:
|
||||
|
||||
```tri
|
||||
useHandler handler@(Fn [(NonEmptyList String)] String) xs@(List String) =@String
|
||||
handler xs
|
||||
|
||||
main = io (bind (pure []) (xs : pure (useHandler acceptNames xs)))
|
||||
```
|
||||
|
||||
The IO runtime does not perform View inference or guard injection at every step.
|
||||
The source/frontend pass constructs checked-exec boundaries once; the runtime
|
||||
only evaluates the resulting interaction tree.
|
||||
|
||||
Current limitations:
|
||||
|
||||
- This is source-visible instrumentation, not whole-program function-flow
|
||||
tracking.
|
||||
- Higher-order guarantees require explicit annotated boundaries.
|
||||
- Raw prebuilt interaction trees, imported executable artifacts, and content-store
|
||||
terms are not automatically re-instrumented unless they pass through this
|
||||
source-lowering path.
|
||||
- The IO action shape itself is only shallowly checkable unless users provide
|
||||
guarded Views for the relevant boundaries.
|
||||
- Continuation result Views are not inferred from external effects; dynamic IO
|
||||
values should cross annotated/guarded boundaries when runtime enforcement is
|
||||
required.
|
||||
|
||||
Making IO checking more complete is future work. In particular, a future design
|
||||
may validate every continuation-produced action structurally, carry checked
|
||||
wrappers with higher-order function values, or define a portable checked-IO
|
||||
artifact instead of relying on Haskell/frontend source instrumentation.
|
||||
|
||||
## 13. Host Independence
|
||||
|
||||
No part of the core View Tree design is specific to Haskell or to the current implementation.
|
||||
|
||||
Any producer may emit a view-tree artifact if it follows the portable tree-data
|
||||
encoding. Any checker implementation may consume it if it implements the typed
|
||||
node semantics.
|
||||
|
||||
The current implementation can produce and consume these artifacts, but it is
|
||||
not the semantic authority. The artifact format and the self-hosted checker
|
||||
semantics are the authority.
|
||||
1
ext/js/.gitignore
vendored
Normal file
1
ext/js/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
29
ext/js/package-lock.json
generated
Normal file
29
ext/js/package-lock.json
generated
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "arboricx-runtime",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "arboricx-runtime",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"koffi": "^2.16.2"
|
||||
},
|
||||
"bin": {
|
||||
"arboricx-run": "src/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/koffi": {
|
||||
"version": "2.16.2",
|
||||
"resolved": "https://registry.npmjs.org/koffi/-/koffi-2.16.2.tgz",
|
||||
"integrity": "sha512-owU0MRwv6xkrVqCd+33uw6BaYppkTRXbO/rVdJNI2dvZG0gzyRhYwW25eWtc5pauwK8TGh3AbkFONSezdykfSA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://liberapay.com/Koromix"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "arboricx-runtime",
|
||||
"version": "0.1.0",
|
||||
"description": "Arboricx portable bundle runtime — JavaScript reference implementation",
|
||||
"description": "Arboricx portable bundle runtime — JavaScript host via libarboricx FFI",
|
||||
"type": "module",
|
||||
"main": "src/bundle.js",
|
||||
"main": "src/lib.js",
|
||||
"bin": {
|
||||
"arboricx-run": "src/cli.js"
|
||||
},
|
||||
@@ -12,6 +12,9 @@
|
||||
"inspect": "node src/cli.js inspect",
|
||||
"run": "node src/cli.js run"
|
||||
},
|
||||
"keywords": ["arboricx", "tree-calculus", "trie", "runtime"],
|
||||
"dependencies": {
|
||||
"koffi": "^2.16.0"
|
||||
},
|
||||
"keywords": ["arboricx", "tree-calculus", "trie", "runtime", "ffi"],
|
||||
"license": "MIT"
|
||||
}
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
/**
|
||||
* bundle.js — Parse an Arboricx portable bundle binary into a JavaScript object.
|
||||
*
|
||||
* Format (v1):
|
||||
* Header (32 bytes):
|
||||
* Magic 8B "ARBORICX"
|
||||
* Major 2B u16 BE (must be 1)
|
||||
* Minor 2B u16 BE
|
||||
* SectionCount 4B u32 BE
|
||||
* Flags 8B u64 BE
|
||||
* DirOffset 8B u64 BE
|
||||
* Section Directory (SectionCount × 60 bytes):
|
||||
* Type 4B u32 BE
|
||||
* Version 2B u16 BE
|
||||
* Flags 2B u16 BE (bit 0 = critical)
|
||||
* Compression 2B u16 BE
|
||||
* DigestAlgo 2B u16 BE
|
||||
* Offset 8B u64 BE
|
||||
* Length 8B u64 BE
|
||||
* SHA256Digest 32B raw
|
||||
* Manifest: fixed-order core + TLV tail (ARBMNFST magic)
|
||||
* Nodes: binary section
|
||||
*/
|
||||
|
||||
import { createHash } from "node:crypto";
|
||||
import { decodeManifest } from "./manifest.js";
|
||||
|
||||
// ── Constants ───────────────────────────────────────────────────────────────
|
||||
|
||||
const MAGIC = Buffer.from([0x41, 0x52, 0x42, 0x4f, 0x52, 0x49, 0x43, 0x58]); // "ARBORICX"
|
||||
const HEADER_LENGTH = 32;
|
||||
const SECTION_ENTRY_LENGTH = 60;
|
||||
const SECTION_MANIFEST = 1;
|
||||
const SECTION_NODES = 2;
|
||||
const FLAG_CRITICAL = 0x0001;
|
||||
const COMPRESSION_NONE = 0;
|
||||
const DIGEST_SHA256 = 1;
|
||||
const MAJOR_VERSION = 1;
|
||||
const MINOR_VERSION = 0;
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function readU16BE(buf, offset) {
|
||||
return buf.readUint16BE(offset);
|
||||
}
|
||||
function readU32BE(buf, offset) {
|
||||
return buf.readUint32BE(offset);
|
||||
}
|
||||
function readU64BE(buf, offset) {
|
||||
return buf.readBigUInt64BE(offset);
|
||||
}
|
||||
|
||||
function sha256(data) {
|
||||
return createHash("sha256").update(data).digest();
|
||||
}
|
||||
|
||||
// ── Public API ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Parse a bundle Buffer into a Bundle object.
|
||||
*
|
||||
* Returns { version, sectionCount, sections } where sections maps
|
||||
* section type numbers to parsed section info (offset, length, data).
|
||||
*/
|
||||
export function parseBundle(buffer) {
|
||||
if (buffer.length < HEADER_LENGTH) {
|
||||
throw new Error("bundle too short for header");
|
||||
}
|
||||
|
||||
// Check magic
|
||||
if (!buffer.slice(0, 8).equals(MAGIC)) {
|
||||
throw new Error("invalid magic: expected ARBORICX");
|
||||
}
|
||||
|
||||
// Parse header
|
||||
const major = readU16BE(buffer, 8);
|
||||
const minor = readU16BE(buffer, 10);
|
||||
const sectionCount = readU32BE(buffer, 12);
|
||||
|
||||
if (major !== MAJOR_VERSION) {
|
||||
throw new Error(
|
||||
`unsupported bundle major version: ${major} (expected ${MAJOR_VERSION})`
|
||||
);
|
||||
}
|
||||
|
||||
const dirOffset = Number(readU64BE(buffer, 24));
|
||||
|
||||
// Parse section directory
|
||||
const dirStart = dirOffset;
|
||||
const dirEnd = dirStart + sectionCount * SECTION_ENTRY_LENGTH;
|
||||
|
||||
if (buffer.length < dirEnd) {
|
||||
throw new Error("bundle truncated in section directory");
|
||||
}
|
||||
|
||||
const entries = [];
|
||||
for (let i = 0; i < sectionCount; i++) {
|
||||
const off = dirStart + i * SECTION_ENTRY_LENGTH;
|
||||
const entry = {
|
||||
type: readU32BE(buffer, off),
|
||||
version: readU16BE(buffer, off + 4),
|
||||
flags: readU16BE(buffer, off + 6),
|
||||
compression: readU16BE(buffer, off + 8),
|
||||
digestAlgorithm: readU16BE(buffer, off + 10),
|
||||
offset: Number(readU64BE(buffer, off + 12)),
|
||||
length: Number(readU64BE(buffer, off + 20)),
|
||||
digest: buffer.slice(off + 28, off + 28 + 32),
|
||||
};
|
||||
entries.push(entry);
|
||||
}
|
||||
|
||||
// Validate sections
|
||||
for (const entry of entries) {
|
||||
const isCritical = (entry.flags & FLAG_CRITICAL) !== 0;
|
||||
const isKnown =
|
||||
entry.type === SECTION_MANIFEST || entry.type === SECTION_NODES;
|
||||
if (isCritical && !isKnown) {
|
||||
throw new Error(`unknown critical section type: ${entry.type}`);
|
||||
}
|
||||
if (entry.compression !== COMPRESSION_NONE) {
|
||||
throw new Error(
|
||||
`unsupported compression codec in section ${entry.type}`
|
||||
);
|
||||
}
|
||||
if (entry.digestAlgorithm !== DIGEST_SHA256) {
|
||||
throw new Error(
|
||||
`unsupported digest algorithm in section ${entry.type}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify section digests and extract data
|
||||
const sections = new Map();
|
||||
for (const entry of entries) {
|
||||
if (entry.offset < 0 || entry.length < 0) {
|
||||
throw new Error(`section ${entry.type} has negative offset/length`);
|
||||
}
|
||||
if (buffer.length < entry.offset + entry.length) {
|
||||
throw new Error(
|
||||
`section ${entry.type} extends beyond bundle end`
|
||||
);
|
||||
}
|
||||
|
||||
const data = buffer.slice(entry.offset, entry.offset + entry.length);
|
||||
|
||||
// Verify digest
|
||||
const computed = sha256(data);
|
||||
if (!computed.equals(entry.digest)) {
|
||||
throw new Error(
|
||||
`section digest mismatch for section type ${entry.type}`
|
||||
);
|
||||
}
|
||||
|
||||
sections.set(entry.type, {
|
||||
...entry,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// Check required sections
|
||||
if (!sections.has(SECTION_MANIFEST)) {
|
||||
throw new Error("missing required section: manifest");
|
||||
}
|
||||
if (!sections.has(SECTION_NODES)) {
|
||||
throw new Error("missing required section: nodes");
|
||||
}
|
||||
|
||||
return {
|
||||
version: `${major}.${minor}`,
|
||||
sectionCount,
|
||||
sections,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience: parse and return the manifest from the fixed-order binary format.
|
||||
*/
|
||||
export function parseManifest(buffer) {
|
||||
const bundle = parseBundle(buffer);
|
||||
const manifestEntry = bundle.sections.get(SECTION_MANIFEST);
|
||||
return decodeManifest(manifestEntry.data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience: parse and return the node section binary.
|
||||
*/
|
||||
export function parseNodeSection(buffer) {
|
||||
const bundle = parseBundle(buffer);
|
||||
const nodesEntry = bundle.sections.get(SECTION_NODES);
|
||||
return nodesEntry.data;
|
||||
}
|
||||
@@ -1,249 +1,104 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* cli.js — Minimal CLI for inspecting and running Arboricx bundles.
|
||||
* cli.js — Arboricx JS host shell via libarboricx C ABI.
|
||||
*
|
||||
* Usage:
|
||||
* node cli.js inspect <bundle>
|
||||
* node cli.js run <bundle> [exportName] [input]
|
||||
* node cli.js inspect <bundle.arboricx>
|
||||
* node cli.js run <bundle.arboricx> [args...]
|
||||
*/
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { parseBundle, parseManifest } from "./bundle.js";
|
||||
import { parseNodeSection as parseNodeSectionMerkle } from "./merkle.js";
|
||||
import { readFileSync } from 'node:fs';
|
||||
import {
|
||||
validateManifest,
|
||||
selectExport,
|
||||
printManifestInfo,
|
||||
} from "./manifest.js";
|
||||
import { parseNodeSection as parseNodeSectionBundle } from "./bundle.js";
|
||||
import {
|
||||
verifyNodeHashes,
|
||||
verifyClosure,
|
||||
verifyRootClosure,
|
||||
} from "./merkle.js";
|
||||
import { isTree, apply, triage, isFork, isStem } from "./tree.js";
|
||||
import { decodeResult, formatTree } from "./codecs.js";
|
||||
init,
|
||||
free,
|
||||
loadBundleDefault,
|
||||
reduce,
|
||||
app,
|
||||
ofNumber,
|
||||
ofString,
|
||||
decode,
|
||||
decodeType,
|
||||
findLib,
|
||||
} from './lib.js';
|
||||
|
||||
// ── Commands ────────────────────────────────────────────────────────────────
|
||||
// ── Commands ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function cmdInspect(bundlePath) {
|
||||
const buffer = readFileSync(bundlePath);
|
||||
const ctx = init();
|
||||
try {
|
||||
const manifest = parseManifest(buffer);
|
||||
validateManifest(manifest);
|
||||
|
||||
const nodeSectionBytes = parseNodeSectionBundle(buffer);
|
||||
const { nodeMap } = parseNodeSectionMerkle(nodeSectionBytes);
|
||||
|
||||
const bundle = readFileSync(bundlePath);
|
||||
console.log(`Bundle: ${bundlePath}`);
|
||||
console.log("");
|
||||
console.log(`Size: ${bundle.length} bytes\n`);
|
||||
|
||||
printManifestInfo(manifest, " ");
|
||||
const term = loadBundleDefault(ctx, bundle);
|
||||
const result = reduce(ctx, term);
|
||||
|
||||
console.log(` Nodes: ${nodeMap.size}`);
|
||||
|
||||
// Verify hashes
|
||||
const { verified: hashesOk, mismatches } = verifyNodeHashes(nodeMap);
|
||||
console.log(` Hash verification: ${hashesOk ? "OK" : "FAIL"}`);
|
||||
for (const m of mismatches) {
|
||||
console.log(` MISMATCH ${m.type} ${m.hash.substring(0, 16)}... expected ${m.expected.substring(0, 16)}...`);
|
||||
const type = decodeType(ctx, result);
|
||||
let value;
|
||||
try {
|
||||
value = decode(ctx, result);
|
||||
} catch {
|
||||
value = '(raw tree)';
|
||||
}
|
||||
|
||||
// Verify closure
|
||||
const { complete: closureOk, missing } = verifyClosure(nodeMap);
|
||||
console.log(` Closure verification: ${closureOk ? "OK" : "FAIL"}`);
|
||||
for (const m of missing) {
|
||||
console.log(` MISSING ${m.parent.substring(0, 16)}... → ${m.child.substring(0, 16)}...`);
|
||||
}
|
||||
|
||||
// Verify root closure for each export
|
||||
for (const exp of manifest.exports || []) {
|
||||
const { complete, missingRoots } = verifyRootClosure(
|
||||
nodeMap,
|
||||
exp.root
|
||||
);
|
||||
if (!complete) {
|
||||
console.log(
|
||||
` Root closure for "${exp.name}": FAIL — missing: ${missingRoots
|
||||
.map((r) => r.substring(0, 16) + "...")
|
||||
.join(", ")}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("");
|
||||
console.log("Inspection complete.");
|
||||
console.log(`Type: ${type}`);
|
||||
console.log(`Value: ${value}`);
|
||||
} catch (e) {
|
||||
console.error(`Error: ${e.message}`);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
function cmdRun(bundlePath, exportName, inputArg) {
|
||||
const buffer = readFileSync(bundlePath);
|
||||
let result;
|
||||
function cmdRun(bundlePath, args) {
|
||||
const ctx = init();
|
||||
try {
|
||||
const manifest = parseManifest(buffer);
|
||||
validateManifest(manifest);
|
||||
const bundle = readFileSync(bundlePath);
|
||||
let term = loadBundleDefault(ctx, bundle);
|
||||
|
||||
const selectedExport = selectExport(manifest, exportName);
|
||||
|
||||
const nodeSectionBytes = parseNodeSectionBundle(buffer);
|
||||
const { nodeMap } = parseNodeSectionMerkle(nodeSectionBytes);
|
||||
|
||||
// Verify hashes
|
||||
const { verified, mismatches } = verifyNodeHashes(nodeMap);
|
||||
if (!verified) {
|
||||
console.error(
|
||||
`Node hash mismatch:\n ${mismatches
|
||||
.map((m) => ` ${m.type}: ${m.hash} (expected ${m.expected})`)
|
||||
.join("\n")}`
|
||||
);
|
||||
process.exit(1);
|
||||
for (const arg of args) {
|
||||
const argTree = /^\d+$/.test(arg) ? ofNumber(ctx, BigInt(arg)) : ofString(ctx, arg);
|
||||
term = app(ctx, term, argTree);
|
||||
}
|
||||
|
||||
// Reconstruct the tree for the selected export
|
||||
const root = buildTreeFromNodeMap(nodeMap, selectedExport.root);
|
||||
if (!isTree(root)) {
|
||||
console.error("Reconstructed root is not a valid tree value");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Apply input if provided
|
||||
let term = root;
|
||||
if (inputArg !== undefined) {
|
||||
// TODO: parse input (string/number) into a tree
|
||||
// For now, just run the term as-is
|
||||
}
|
||||
|
||||
// Reduce with fuel limit
|
||||
const finalTerm = reduce(term, 1_000_000);
|
||||
|
||||
// Print result as tree calculus form
|
||||
console.log(formatTree(finalTerm));
|
||||
const result = reduce(ctx, term);
|
||||
console.log(decode(ctx, result));
|
||||
} catch (e) {
|
||||
console.error(`Error: ${e.message}`);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tree reconstruction ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Reconstruct a tree from a node map.
|
||||
*
|
||||
* Node map: Map<hexHash, { type, childHash?, leftHash?, rightHash? }>
|
||||
*
|
||||
* Returns the tree representation: [] for Leaf, [child] for Stem, [right, left] for Fork.
|
||||
* Uses memoization to avoid re-processing nodes.
|
||||
*/
|
||||
export function buildTreeFromNodeMap(nodeMap, hash, memo = new Map()) {
|
||||
if (memo.has(hash)) return memo.get(hash);
|
||||
|
||||
const node = nodeMap.get(hash);
|
||||
if (!node) {
|
||||
throw new Error(`missing node in bundle: ${hash}`);
|
||||
}
|
||||
|
||||
let tree;
|
||||
switch (node.type) {
|
||||
case "leaf":
|
||||
tree = [];
|
||||
break;
|
||||
case "stem":
|
||||
tree = [buildTreeFromNodeMap(nodeMap, node.childHash, memo)];
|
||||
break;
|
||||
case "fork":
|
||||
tree = [
|
||||
buildTreeFromNodeMap(nodeMap, node.rightHash, memo),
|
||||
buildTreeFromNodeMap(nodeMap, node.leftHash, memo),
|
||||
];
|
||||
break;
|
||||
default:
|
||||
throw new Error(`unknown node type: ${node.type}`);
|
||||
}
|
||||
|
||||
memo.set(hash, tree);
|
||||
return tree;
|
||||
}
|
||||
|
||||
// ── Reduction ───────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Reduce a term to normal form with a fuel limit.
|
||||
* Uses the stack-based approach from the TS evaluator.
|
||||
*/
|
||||
export function reduce(term, fuel) {
|
||||
const stack = [term];
|
||||
let remaining = fuel;
|
||||
|
||||
while (stack.length >= 2 && remaining-- > 0) {
|
||||
// Pop right (top), then left
|
||||
const b = stack.pop(); // right
|
||||
const a = stack.pop(); // left
|
||||
|
||||
if (stack.length >= 2) {
|
||||
// Push a back for potential further reduction
|
||||
stack.push(a);
|
||||
}
|
||||
|
||||
const result = apply(a, b);
|
||||
|
||||
if (isTree(result)) {
|
||||
// If result is a value, push it. But if it's a Fork/Stem,
|
||||
// we need to push its components for further reduction.
|
||||
if (isFork(result)) {
|
||||
// Push right first (so it's popped second), then left
|
||||
stack.push(result[1]); // left
|
||||
stack.push(result[0]); // right
|
||||
} else if (isStem(result)) {
|
||||
stack.push(result[0]); // child
|
||||
} else {
|
||||
stack.push(result); // Leaf
|
||||
}
|
||||
} else {
|
||||
// Not a tree — push as-is (shouldn't happen after buildTree)
|
||||
stack.push(result);
|
||||
}
|
||||
}
|
||||
|
||||
if (remaining <= 0) {
|
||||
throw new Error("reduction step limit exceeded");
|
||||
}
|
||||
|
||||
if (stack.length === 1) {
|
||||
return stack[0];
|
||||
}
|
||||
return stack[0]; // fallback
|
||||
}
|
||||
|
||||
// ── Main ────────────────────────────────────────────────────────────────────
|
||||
// ── Main ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const command = args[0];
|
||||
|
||||
switch (command) {
|
||||
case "inspect": {
|
||||
case 'inspect': {
|
||||
if (args.length < 2) {
|
||||
console.error("Usage: node cli.js inspect <bundle>");
|
||||
console.error('Usage: node cli.js inspect <bundle.arboricx>');
|
||||
process.exit(1);
|
||||
}
|
||||
cmdInspect(args[1]);
|
||||
break;
|
||||
}
|
||||
case "run": {
|
||||
case 'run': {
|
||||
if (args.length < 2) {
|
||||
console.error("Usage: node cli.js run <bundle> [exportName] [input]");
|
||||
console.error('Usage: node cli.js run <bundle.arboricx> [args...]');
|
||||
process.exit(1);
|
||||
}
|
||||
cmdRun(args[1], args[2], args[3]);
|
||||
cmdRun(args[1], args.slice(2));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.log("Arboricx JS Runtime");
|
||||
console.log("");
|
||||
console.log("Usage:");
|
||||
console.log(" node cli.js inspect <bundle>");
|
||||
console.log(" node cli.js run <bundle> [exportName] [input]");
|
||||
console.log('Arboricx JS Host (via libarboricx FFI)');
|
||||
console.log('');
|
||||
console.log('Usage:');
|
||||
console.log(' node cli.js inspect <bundle.arboricx>');
|
||||
console.log(' node cli.js run <bundle.arboricx> [args...]');
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
/**
|
||||
* codecs.js — Minimal codecs for decoding tree results.
|
||||
*
|
||||
* Implements: decodeResult (from Research.hs)
|
||||
* - Leaf → "t"
|
||||
* - Numbers: toNumber
|
||||
* - Strings: toString
|
||||
* - Lists: toList
|
||||
* - Fallback: raw tree format
|
||||
*/
|
||||
|
||||
// ── toNumber ────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Decode a tree as a binary number (big-endian).
|
||||
* Leaf = 0, Fork(Leaf, rest) = 2*n, Fork(Stem Leaf, rest) = 2*n+1.
|
||||
*/
|
||||
export function toNumber(t) {
|
||||
if (!Array.isArray(t)) return null;
|
||||
if (t.length === 0) return 0; // Leaf = 0
|
||||
if (t.length !== 2) return null; // must be Fork
|
||||
|
||||
const [right, left] = t;
|
||||
// Fork structure: [right, left]
|
||||
// left child determines bit: Leaf = 0, Stem(Leaf) = 1
|
||||
let bit;
|
||||
if (Array.isArray(left) && left.length === 0) {
|
||||
bit = 0; // Leaf
|
||||
} else if (Array.isArray(left) && left.length === 1) {
|
||||
const child = left[0];
|
||||
if (Array.isArray(child) && child.length === 0) {
|
||||
bit = 1; // Stem(Leaf) = 1
|
||||
} else {
|
||||
return null; // Stem of something other than Leaf
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rest = toNumber(right);
|
||||
if (rest === null) return null;
|
||||
|
||||
return bit + 2 * rest;
|
||||
}
|
||||
|
||||
// ── toString ────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Decode a tree as a list of numbers (characters).
|
||||
* Fork(x, rest) = x : list.
|
||||
*/
|
||||
export function toList(t) {
|
||||
if (!Array.isArray(t)) return null;
|
||||
if (t.length === 0) return []; // Leaf = empty list
|
||||
if (t.length !== 2) return null; // must be Fork
|
||||
|
||||
const [right, left] = t;
|
||||
const rest = toList(right);
|
||||
if (rest === null) return null;
|
||||
|
||||
return [left, ...rest];
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a tree as a string.
|
||||
*/
|
||||
export function toString(t) {
|
||||
const list = toList(t);
|
||||
if (list === null) return null;
|
||||
try {
|
||||
return list.map((ch) => String.fromCharCode(ch)).join("");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── decodeResult ────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Decode a tree result using multiple strategies:
|
||||
* 1. Leaf → "t"
|
||||
* 2. String (if all chars are printable)
|
||||
* 3. Number
|
||||
* 4. List
|
||||
* 5. Raw tree format
|
||||
*/
|
||||
export function decodeResult(t) {
|
||||
if (!Array.isArray(t)) {
|
||||
return String(t);
|
||||
}
|
||||
|
||||
// Leaf
|
||||
if (t.length === 0) {
|
||||
return "t";
|
||||
}
|
||||
|
||||
// Try string first (list of char codes)
|
||||
const list = toList(t);
|
||||
if (list !== null && list.length > 0) {
|
||||
const str = list.map((n) => {
|
||||
if (n < 32 || n > 126) return null;
|
||||
return String.fromCharCode(n);
|
||||
}).join("");
|
||||
if (str) return `"${str}"`;
|
||||
}
|
||||
|
||||
// Try number
|
||||
const num = toNumber(t);
|
||||
if (num !== null) {
|
||||
return String(num);
|
||||
}
|
||||
|
||||
// Try list (elements are trees)
|
||||
if (t.length === 2) {
|
||||
const elements = toList(t);
|
||||
if (elements !== null) {
|
||||
const decoded = elements.map((e) => decodeResult(e));
|
||||
return `[${decoded.join(", ")}]`;
|
||||
}
|
||||
}
|
||||
|
||||
// Raw tree format
|
||||
return formatTree(t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a tree as a parenthesized expression.
|
||||
*/
|
||||
export function formatTree(t) {
|
||||
if (!Array.isArray(t)) return String(t);
|
||||
if (t.length === 0) return "Leaf";
|
||||
if (t.length === 1) return `Stem(${formatTree(t[0])})`;
|
||||
if (t.length === 2) return `Fork(${formatTree(t[1])}, ${formatTree(t[0])})`;
|
||||
return `[${t.map(formatTree).join(", ")}]`;
|
||||
}
|
||||
224
ext/js/src/lib.js
Normal file
224
ext/js/src/lib.js
Normal file
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* lib.js — FFI wrapper around libarboricx.so via koffi.
|
||||
*
|
||||
* Exports low-level C ABI bindings and high-level helpers.
|
||||
*/
|
||||
|
||||
import { existsSync } from 'node:fs';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import koffi from 'koffi';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
koffi.opaque('arb_ctx_t');
|
||||
|
||||
// ── Library discovery ───────────────────────────────────────────────────────
|
||||
|
||||
export function findLib() {
|
||||
const env = process.env.ARBORICX_LIB;
|
||||
if (env) {
|
||||
if (existsSync(env)) return env;
|
||||
throw new Error(`ARBORICX_LIB set but file not found: ${env}`);
|
||||
}
|
||||
|
||||
const candidates = [
|
||||
resolve(__dirname, 'libarboricx.so'),
|
||||
'libarboricx.so',
|
||||
'./libarboricx.so',
|
||||
'/usr/local/lib/libarboricx.so',
|
||||
'/usr/lib/libarboricx.so',
|
||||
];
|
||||
|
||||
for (const p of candidates) {
|
||||
if (existsSync(p)) return p;
|
||||
}
|
||||
|
||||
throw new Error('libarboricx.so not found. Set ARBORICX_LIB to its full path.');
|
||||
}
|
||||
|
||||
// ── FFI setup ───────────────────────────────────────────────────────────────
|
||||
|
||||
let _lib = null;
|
||||
let _libPath = null;
|
||||
|
||||
function ensureLib() {
|
||||
if (_lib) return _lib;
|
||||
const path = findLib();
|
||||
_lib = koffi.load(path);
|
||||
_libPath = path;
|
||||
return _lib;
|
||||
}
|
||||
|
||||
export function loadLib(path) {
|
||||
if (_lib && _libPath === path) return;
|
||||
_lib = koffi.load(path);
|
||||
_libPath = path;
|
||||
}
|
||||
|
||||
function getLib() {
|
||||
if (_lib) return _lib;
|
||||
return ensureLib();
|
||||
}
|
||||
|
||||
// ── Context lifecycle ───────────────────────────────────────────────────────
|
||||
|
||||
export function init(libPath) {
|
||||
if (libPath) loadLib(libPath);
|
||||
const lib = getLib();
|
||||
const ctx = lib.func('arb_ctx_t *arboricx_init(void)')();
|
||||
if (!ctx) throw new Error('arboricx_init failed');
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export function free(ctx) {
|
||||
getLib().func('void arboricx_free(arb_ctx_t *ctx)')(ctx);
|
||||
}
|
||||
|
||||
// ── Bundle loading ──────────────────────────────────────────────────────────
|
||||
|
||||
export function loadBundle(ctx, bytes, name) {
|
||||
const result = getLib().func('uint32_t arb_load_bundle(arb_ctx_t *ctx, _In_ uint8_t *bytes, size_t len, const char *name)')(ctx, bytes, bytes.length, name);
|
||||
if (result === 0) throw new Error(`arb_load_bundle failed for export "${name}"`);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function loadBundleDefault(ctx, bytes) {
|
||||
const result = getLib().func('uint32_t arb_load_bundle_default(arb_ctx_t *ctx, _In_ uint8_t *bytes, size_t len)')(ctx, bytes, bytes.length);
|
||||
if (result === 0) throw new Error('arb_load_bundle_default failed');
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Reduction ───────────────────────────────────────────────────────────────
|
||||
|
||||
export function reduce(ctx, root, fuel = 1_000_000_000n) {
|
||||
const f = getLib().func('uint32_t arb_reduce(arb_ctx_t *ctx, uint32_t root, uint64_t fuel)');
|
||||
return f(ctx, root, typeof fuel === 'bigint' ? fuel : BigInt(fuel));
|
||||
}
|
||||
|
||||
// ── Tree construction ───────────────────────────────────────────────────────
|
||||
|
||||
export function leaf(ctx) {
|
||||
return getLib().func('uint32_t arb_leaf(arb_ctx_t *ctx)')(ctx);
|
||||
}
|
||||
|
||||
export function stem(ctx, child) {
|
||||
return getLib().func('uint32_t arb_stem(arb_ctx_t *ctx, uint32_t child)')(ctx, child);
|
||||
}
|
||||
|
||||
export function fork(ctx, left, right) {
|
||||
return getLib().func('uint32_t arb_fork(arb_ctx_t *ctx, uint32_t left, uint32_t right)')(ctx, left, right);
|
||||
}
|
||||
|
||||
export function app(ctx, func, arg) {
|
||||
return getLib().func('uint32_t arb_app(arb_ctx_t *ctx, uint32_t func, uint32_t arg)')(ctx, func, arg);
|
||||
}
|
||||
|
||||
// ── Codec constructors ──────────────────────────────────────────────────────
|
||||
|
||||
export function ofNumber(ctx, n) {
|
||||
const big = typeof n === 'bigint' ? n : BigInt(n);
|
||||
return getLib().func('uint32_t arb_of_number(arb_ctx_t *ctx, uint64_t n)')(ctx, big);
|
||||
}
|
||||
|
||||
export function ofString(ctx, s) {
|
||||
return getLib().func('uint32_t arb_of_string(arb_ctx_t *ctx, const char *s)')(ctx, s);
|
||||
}
|
||||
|
||||
export function ofBytes(ctx, bytes) {
|
||||
return getLib().func('uint32_t arb_of_bytes(arb_ctx_t *ctx, _In_ uint8_t *bytes, size_t len)')(ctx, bytes, bytes.length);
|
||||
}
|
||||
|
||||
export function ofList(ctx, items) {
|
||||
const arr = new Uint32Array(items);
|
||||
return getLib().func('uint32_t arb_of_list(arb_ctx_t *ctx, _In_ uint32_t *items, size_t len)')(ctx, arr, arr.length);
|
||||
}
|
||||
|
||||
// ── Codec destructors ───────────────────────────────────────────────────────
|
||||
|
||||
export function toNumber(ctx, root) {
|
||||
const out = [0];
|
||||
const ok = getLib().func('int arb_to_number(arb_ctx_t *ctx, uint32_t root, _Out_ uint64_t *out)')(ctx, root, out);
|
||||
if (!ok) throw new Error('arb_to_number failed');
|
||||
return typeof out[0] === 'bigint' ? Number(out[0]) : out[0];
|
||||
}
|
||||
|
||||
export function toString(ctx, root) {
|
||||
const ptrOut = [null];
|
||||
const lenOut = [0];
|
||||
const ok = getLib().func('int arb_to_string(arb_ctx_t *ctx, uint32_t root, _Out_ uint8_t **out_ptr, _Out_ size_t *out_len)')(ctx, root, ptrOut, lenOut);
|
||||
if (!ok) throw new Error('arb_to_string failed');
|
||||
|
||||
const bytes = koffi.decode(ptrOut[0], 'uint8_t', lenOut[0]);
|
||||
const str = Buffer.from(bytes).toString('utf-8');
|
||||
getLib().func('void arboricx_free_buf(arb_ctx_t *ctx, uint8_t *ptr, size_t len)')(ctx, ptrOut[0], lenOut[0]);
|
||||
return str;
|
||||
}
|
||||
|
||||
export function toBytes(ctx, root) {
|
||||
const ptrOut = [null];
|
||||
const lenOut = [0];
|
||||
const ok = getLib().func('int arb_to_bytes(arb_ctx_t *ctx, uint32_t root, _Out_ uint8_t **out_ptr, _Out_ size_t *out_len)')(ctx, root, ptrOut, lenOut);
|
||||
if (!ok) throw new Error('arb_to_bytes failed');
|
||||
|
||||
const bytes = Buffer.from(koffi.decode(ptrOut[0], 'uint8_t', lenOut[0]));
|
||||
getLib().func('void arboricx_free_buf(arb_ctx_t *ctx, uint8_t *ptr, size_t len)')(ctx, ptrOut[0], lenOut[0]);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
export function toBool(ctx, root) {
|
||||
const out = [0];
|
||||
const ok = getLib().func('int arb_to_bool(arb_ctx_t *ctx, uint32_t root, _Out_ int *out)')(ctx, root, out);
|
||||
if (!ok) throw new Error('arb_to_bool failed');
|
||||
return out[0] !== 0;
|
||||
}
|
||||
|
||||
// ── Result unwrapping ───────────────────────────────────────────────────────
|
||||
|
||||
export function unwrapResult(ctx, root) {
|
||||
const outOk = [0];
|
||||
const outValue = [0];
|
||||
const outRest = [0];
|
||||
const ok = getLib().func('int arb_unwrap_result(arb_ctx_t *ctx, uint32_t root, _Out_ int *out_ok, _Out_ uint32_t *out_value, _Out_ uint32_t *out_rest)')(ctx, root, outOk, outValue, outRest);
|
||||
if (!ok) throw new Error('arb_unwrap_result failed');
|
||||
return { ok: outOk[0] !== 0, value: outValue[0], rest: outRest[0] };
|
||||
}
|
||||
|
||||
export function unwrapHostValue(ctx, root) {
|
||||
const outTag = [0n];
|
||||
const outPayload = [0];
|
||||
const ok = getLib().func('int arb_unwrap_host_value(arb_ctx_t *ctx, uint32_t root, _Out_ uint64_t *out_tag, _Out_ uint32_t *out_payload)')(ctx, root, outTag, outPayload);
|
||||
if (!ok) throw new Error('arb_unwrap_host_value failed');
|
||||
return { tag: outTag[0], payload: outPayload[0] };
|
||||
}
|
||||
|
||||
// ── Kernel ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export function kernelRoot(ctx) {
|
||||
return getLib().func('uint32_t arb_kernel_root(arb_ctx_t *ctx)')(ctx);
|
||||
}
|
||||
|
||||
// ── High-level helpers ──────────────────────────────────────────────────────
|
||||
|
||||
export function decode(ctx, root) {
|
||||
try {
|
||||
return toBool(ctx, root) ? 'true' : 'false';
|
||||
} catch {
|
||||
try {
|
||||
return toString(ctx, root);
|
||||
} catch {
|
||||
try {
|
||||
return String(toNumber(ctx, root));
|
||||
} catch {
|
||||
throw new Error('could not decode result');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function decodeType(ctx, root) {
|
||||
try { toBool(ctx, root); return 'bool'; } catch {}
|
||||
try { toString(ctx, root); return 'string'; } catch {}
|
||||
try { toNumber(ctx, root); return 'number'; } catch {}
|
||||
return 'unknown (raw tree)';
|
||||
}
|
||||
@@ -1,374 +0,0 @@
|
||||
/**
|
||||
* manifest.js — Fixed-order manifest parsing and export lookup.
|
||||
*
|
||||
* The manifest binary format (ManifestV1):
|
||||
* magic(8) + major(u16) + minor(u16)
|
||||
* + schema(string) + bundleType(string)
|
||||
* + treeCalculus(string) + treeHashAlgorithm(string) + treeHashDomain(string) + treeNodePayload(string)
|
||||
* + runtimeSemantics(string) + runtimeEvaluation(string) + runtimeAbi(string)
|
||||
* + capabilityCount(u32) + capabilities(string[])
|
||||
* + closure(u8)
|
||||
* + rootCount(u32) + roots[]
|
||||
* + exportCount(u32) + exports[]
|
||||
* + metadataFieldCount(u32) + metadataTLVs[]
|
||||
* + extensionFieldCount(u32) + extensionTLVs[]
|
||||
*
|
||||
* String format: u32 BE length + UTF-8 bytes.
|
||||
* Root: 32 bytes raw hash + role(string).
|
||||
* Export: name(string) + 32 bytes raw root hash + kind(string) + abi(string).
|
||||
* TLV: u16 tag + u32 length + value bytes.
|
||||
*/
|
||||
|
||||
// ── Constants ───────────────────────────────────────────────────────────────
|
||||
|
||||
const MANIFEST_MAGIC = "ARBMNFST";
|
||||
const MANIFEST_MAJOR = 1;
|
||||
const MANIFEST_MINOR = 0;
|
||||
|
||||
// Metadata TLV tags
|
||||
const TAG_PACKAGE = 1;
|
||||
const TAG_VERSION = 2;
|
||||
const TAG_DESCRIPTION = 3;
|
||||
const TAG_LICENSE = 4;
|
||||
const TAG_CREATED_BY = 5;
|
||||
|
||||
// Closure bytes
|
||||
const CLOSURE_COMPLETE = 0;
|
||||
const CLOSURE_PARTIAL = 1;
|
||||
|
||||
// ── Binary helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
function u16(buf, off) {
|
||||
if (off + 2 > buf.length) throw new Error("manifest: not enough bytes for u16");
|
||||
return { value: buf.readUint16BE(off), next: off + 2 };
|
||||
}
|
||||
|
||||
function u32(buf, off) {
|
||||
if (off + 4 > buf.length) throw new Error("manifest: not enough bytes for u32");
|
||||
return { value: buf.readUint32BE(off), next: off + 4 };
|
||||
}
|
||||
|
||||
function u8(buf, off) {
|
||||
if (off >= buf.length) throw new Error("manifest: not enough bytes for u8");
|
||||
return { value: buf.readUint8(off), next: off + 1 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a length-prefixed UTF-8 string: u32 BE length + UTF-8 bytes.
|
||||
* Returns { text, next }.
|
||||
*/
|
||||
function readStr(buf, off) {
|
||||
const { value: len, next: afterLen } = u32(buf, off);
|
||||
if (afterLen + len > buf.length) throw new Error("manifest: string extends beyond input");
|
||||
return { text: buf.toString("utf-8", afterLen, afterLen + len), next: afterLen + len };
|
||||
}
|
||||
|
||||
/**
|
||||
* Read raw bytes of given length.
|
||||
* Returns { bytes, next }.
|
||||
*/
|
||||
function readRaw(buf, off, n) {
|
||||
if (off + n > buf.length) throw new Error(`manifest: not enough bytes for ${n}-byte read`);
|
||||
return { value: buf.slice(off, off + n), next: off + n };
|
||||
}
|
||||
|
||||
// ── Manifest decoder ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Decode the manifest binary from a Buffer.
|
||||
*
|
||||
* Returns a normalized manifest object matching the shape expected
|
||||
* by validateManifest / selectExport.
|
||||
*/
|
||||
export function decodeManifest(buf) {
|
||||
let off = 0;
|
||||
|
||||
// Magic (8 bytes)
|
||||
const magic = buf.toString("utf-8", 0, 8);
|
||||
if (magic !== MANIFEST_MAGIC) {
|
||||
throw new Error(`invalid manifest magic: expected ${MANIFEST_MAGIC}, got "${magic}"`);
|
||||
}
|
||||
off = 8;
|
||||
|
||||
// Version
|
||||
const { value: major } = u16(buf, off);
|
||||
if (major !== MANIFEST_MAJOR) throw new Error(`unsupported manifest major version: ${major}`);
|
||||
off += 4; // u16 major + u16 minor
|
||||
|
||||
// Helper: read length-prefixed text
|
||||
const readText = () => {
|
||||
const { text, next } = readStr(buf, off);
|
||||
off = next;
|
||||
return text;
|
||||
};
|
||||
|
||||
// Core strings
|
||||
const schema = readText();
|
||||
const bundleType = readText();
|
||||
const treeCalculus = readText();
|
||||
const treeHashAlgorithm = readText();
|
||||
const treeHashDomain = readText();
|
||||
const treeNodePayload = readText();
|
||||
const runtimeSemantics = readText();
|
||||
const runtimeEvaluation = readText();
|
||||
const runtimeAbi = readText();
|
||||
|
||||
// Capabilities (u32 count + string[])
|
||||
const { value: capCount } = u32(buf, off);
|
||||
off += 4;
|
||||
const capabilities = [];
|
||||
for (let i = 0; i < capCount; i++) {
|
||||
capabilities.push(readText());
|
||||
}
|
||||
|
||||
// Closure (u8)
|
||||
const { value: closureByte } = u8(buf, off);
|
||||
off += 1;
|
||||
const closure = closureByte === CLOSURE_COMPLETE ? "complete" : "partial";
|
||||
|
||||
// Roots (u32 count + Root[])
|
||||
// Root: 32 bytes raw hash + role(string)
|
||||
const { value: rootCount } = u32(buf, off);
|
||||
off += 4;
|
||||
const roots = [];
|
||||
for (let i = 0; i < rootCount; i++) {
|
||||
const { value: hashRaw } = readRaw(buf, off, 32);
|
||||
off += 32;
|
||||
const { text: role, next: rOff } = readStr(buf, off);
|
||||
off = rOff;
|
||||
roots.push({ hash: hashRaw.toString("hex"), role });
|
||||
}
|
||||
|
||||
// Exports (u32 count + Export[])
|
||||
// Export: name(string) + 32 bytes raw root hash + kind(string) + abi(string)
|
||||
const { value: exportCount } = u32(buf, off);
|
||||
off += 4;
|
||||
const exports = [];
|
||||
for (let i = 0; i < exportCount; i++) {
|
||||
const { text: name, next: nOff } = readStr(buf, off);
|
||||
off = nOff;
|
||||
const { value: expHashRaw } = readRaw(buf, off, 32);
|
||||
off += 32;
|
||||
const { text: kind, next: kOff } = readStr(buf, off);
|
||||
off = kOff;
|
||||
const { text: abi, next: aOff } = readStr(buf, off);
|
||||
off = aOff;
|
||||
exports.push({ name, root: expHashRaw.toString("hex"), kind, abi });
|
||||
}
|
||||
|
||||
// Metadata (u32 count + TLV[])
|
||||
// TLV: u16 tag + u32 length + value bytes
|
||||
const { value: metaCount } = u32(buf, off);
|
||||
off += 4;
|
||||
const metadata = {};
|
||||
for (let i = 0; i < metaCount; i++) {
|
||||
const { value: tag } = u16(buf, off);
|
||||
off += 2;
|
||||
const { value: tlvLen } = u32(buf, off);
|
||||
off += 4;
|
||||
const { value: tlvRaw } = readRaw(buf, off, tlvLen);
|
||||
off += tlvLen;
|
||||
const val = tlvRaw.toString("utf-8");
|
||||
switch (tag) {
|
||||
case TAG_PACKAGE: metadata.package = val; break;
|
||||
case TAG_VERSION: metadata.version = val; break;
|
||||
case TAG_DESCRIPTION: metadata.description = val; break;
|
||||
case TAG_LICENSE: metadata.license = val; break;
|
||||
case TAG_CREATED_BY: metadata.createdBy = val; break;
|
||||
}
|
||||
}
|
||||
|
||||
// Extensions (u32 count + TLV[] — skip all)
|
||||
const { value: extCount } = u32(buf, off);
|
||||
off += 4;
|
||||
for (let i = 0; i < extCount; i++) {
|
||||
const { value: _tag } = u16(buf, off);
|
||||
off += 2;
|
||||
const { value: tlvLen } = u32(buf, off);
|
||||
off += 4;
|
||||
off += tlvLen; // skip value
|
||||
}
|
||||
|
||||
return {
|
||||
schema,
|
||||
bundleType,
|
||||
tree: {
|
||||
calculus: treeCalculus,
|
||||
nodeHash: {
|
||||
algorithm: treeHashAlgorithm,
|
||||
domain: treeHashDomain,
|
||||
},
|
||||
nodePayload: treeNodePayload,
|
||||
},
|
||||
runtime: {
|
||||
semantics: runtimeSemantics,
|
||||
evaluation: runtimeEvaluation,
|
||||
abi: runtimeAbi,
|
||||
capabilities,
|
||||
},
|
||||
closure,
|
||||
roots,
|
||||
exports,
|
||||
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Validation ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Validate the manifest against the runtime profile requirements.
|
||||
* Throws on violation.
|
||||
*/
|
||||
export function validateManifest(manifest) {
|
||||
if (manifest.schema !== "arboricx.bundle.manifest.v1") {
|
||||
throw new Error(
|
||||
`unsupported manifest schema: ${manifest.schema}`
|
||||
);
|
||||
}
|
||||
if (manifest.bundleType !== "tree-calculus-executable-object") {
|
||||
throw new Error(
|
||||
`unsupported bundle type: ${manifest.bundleType}`
|
||||
);
|
||||
}
|
||||
|
||||
const tree = manifest.tree;
|
||||
if (tree.calculus !== "tree-calculus.v1") {
|
||||
throw new Error(`unsupported calculus: ${tree.calculus}`);
|
||||
}
|
||||
if (tree.nodeHash.algorithm !== "sha256") {
|
||||
throw new Error(
|
||||
`unsupported node hash algorithm: ${tree.nodeHash.algorithm}`
|
||||
);
|
||||
}
|
||||
if (tree.nodeHash.domain !== "arboricx.merkle.node.v1") {
|
||||
throw new Error(
|
||||
`unsupported node hash domain: ${tree.nodeHash.domain}`
|
||||
);
|
||||
}
|
||||
if (tree.nodePayload !== "arboricx.merkle.payload.v1") {
|
||||
throw new Error(`unsupported node payload: ${tree.nodePayload}`);
|
||||
}
|
||||
|
||||
const runtime = manifest.runtime;
|
||||
if (runtime.semantics !== "tree-calculus.v1") {
|
||||
throw new Error(`unsupported runtime semantics: ${runtime.semantics}`);
|
||||
}
|
||||
if (runtime.abi !== "arboricx.abi.tree.v1") {
|
||||
throw new Error(`unsupported runtime ABI: ${runtime.abi}`);
|
||||
}
|
||||
if (runtime.capabilities && runtime.capabilities.length > 0) {
|
||||
throw new Error(
|
||||
`host/runtime capabilities not supported: ${runtime.capabilities.join(", ")}`
|
||||
);
|
||||
}
|
||||
|
||||
if (manifest.closure !== "complete") {
|
||||
throw new Error("bundle v1 requires closure = complete");
|
||||
}
|
||||
if (manifest.imports && manifest.imports.length > 0) {
|
||||
throw new Error("bundle v1 requires an empty imports list");
|
||||
}
|
||||
if (!manifest.roots || manifest.roots.length === 0) {
|
||||
throw new Error("manifest has no roots");
|
||||
}
|
||||
if (!manifest.exports || manifest.exports.length === 0) {
|
||||
throw new Error("manifest has no exports");
|
||||
}
|
||||
|
||||
for (const exp of manifest.exports) {
|
||||
if (!exp.name) {
|
||||
throw new Error("manifest export has empty name");
|
||||
}
|
||||
if (!exp.root) {
|
||||
throw new Error("manifest export has empty root");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select an export hash given a requested name.
|
||||
*
|
||||
* Selection strategy:
|
||||
* 1. Explicit export name
|
||||
* 2. Export named "main"
|
||||
* 3. Single export (auto-select)
|
||||
* 4. Error if multiple exports and no "main"
|
||||
*/
|
||||
export function selectExport(manifest, requestedName) {
|
||||
const exports = manifest.exports || [];
|
||||
|
||||
// Strategy 1: explicit name
|
||||
if (requestedName) {
|
||||
const found = exports.find((e) => e.name === requestedName);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
throw new Error(
|
||||
`requested export "${requestedName}" not found. Available: ${exports.map((e) => e.name).join(", ")}`
|
||||
);
|
||||
}
|
||||
|
||||
// Strategy 2: prefer "main"
|
||||
const mainExport = exports.find((e) => e.name === "main");
|
||||
if (mainExport) {
|
||||
return mainExport;
|
||||
}
|
||||
|
||||
// Strategy 3: single export
|
||||
if (exports.length === 1) {
|
||||
return exports[0];
|
||||
}
|
||||
|
||||
// Strategy 4: multiple exports, require explicit
|
||||
throw new Error(
|
||||
`multiple exports available but none named "main": ${exports.map((e) => e.name).join(", ")}. Specify an export name.`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all root hashes from the manifest.
|
||||
*/
|
||||
export function getRootHashes(manifest) {
|
||||
return (manifest.roots || []).map((r) => r.hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all export names.
|
||||
*/
|
||||
export function getExportNames(manifest) {
|
||||
return (manifest.exports || []).map((e) => e.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print manifest summary info.
|
||||
*/
|
||||
export function printManifestInfo(manifest, indent = "") {
|
||||
const tree = manifest.tree;
|
||||
const runtime = manifest.runtime;
|
||||
|
||||
console.log(`${indent}Schema: ${manifest.schema}`);
|
||||
console.log(`${indent}Bundle type: ${manifest.bundleType}`);
|
||||
console.log(`${indent}Closure: ${manifest.closure}`);
|
||||
console.log(`${indent}Tree calculus: ${tree.calculus}`);
|
||||
console.log(`${indent}Hash algo: ${tree.nodeHash.algorithm}`);
|
||||
console.log(`${indent}Hash domain: ${tree.nodeHash.domain}`);
|
||||
console.log(`${indent}Runtime: ${runtime.semantics}`);
|
||||
console.log(`${indent}ABI: ${runtime.abi}`);
|
||||
console.log(`${indent}Evaluation: ${runtime.evaluation || "N/A"}`);
|
||||
console.log("");
|
||||
console.log(`${indent}Roots (${getRootHashes(manifest).length}):`);
|
||||
for (const root of getRootHashes(manifest)) {
|
||||
console.log(`${indent} ${root.substring(0, 16)}...`);
|
||||
}
|
||||
console.log("");
|
||||
console.log(`${indent}Exports (${getExportNames(manifest).length}):`);
|
||||
for (const name of getExportNames(manifest)) {
|
||||
console.log(`${indent} ${name}`);
|
||||
}
|
||||
|
||||
const meta = manifest.metadata;
|
||||
if (meta && meta.createdBy) {
|
||||
console.log("");
|
||||
console.log(`${indent}Created by: ${meta.createdBy}`);
|
||||
}
|
||||
}
|
||||
@@ -1,276 +0,0 @@
|
||||
/**
|
||||
* merkle.js — Node payload decoding and hash verification.
|
||||
*
|
||||
* Node payload format:
|
||||
* Leaf: 0x00
|
||||
* Stem: 0x01 || child_hash (32 bytes raw)
|
||||
* Fork: 0x02 || left_hash (32 bytes raw) || right_hash (32 bytes raw)
|
||||
*
|
||||
* Hash computation:
|
||||
* hash = SHA256( "arboricx.merkle.node.v1" || 0x00 || node_payload )
|
||||
*/
|
||||
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
// ── Constants ───────────────────────────────────────────────────────────────
|
||||
|
||||
const DOMAIN_TAG = "arboricx.merkle.node.v1";
|
||||
const HASH_LENGTH = 32; // raw hash bytes
|
||||
const HEX_LENGTH = 64; // hex-encoded hash length
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function rawToHex(buf) {
|
||||
if (buf.length !== HASH_LENGTH) {
|
||||
throw new Error(`raw hash must be ${HASH_LENGTH} bytes, got ${buf.length}`);
|
||||
}
|
||||
return buf.toString("hex");
|
||||
}
|
||||
|
||||
function hexToRaw(hex) {
|
||||
const buf = Buffer.from(hex, "hex");
|
||||
if (buf.length !== HASH_LENGTH) {
|
||||
throw new Error(`hex hash must decode to ${HASH_LENGTH} bytes`);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
function sha256(data) {
|
||||
return createHash("sha256").update(data).digest();
|
||||
}
|
||||
|
||||
function nodeHash(prefix, payload) {
|
||||
return sha256(Buffer.concat([Buffer.from(prefix), Buffer.from([0x00]), payload]));
|
||||
}
|
||||
|
||||
// ── Node payload types ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Deserialize a node payload into { type, childHash, leftHash, rightHash }.
|
||||
*
|
||||
* type: "leaf" | "stem" | "fork"
|
||||
* childHash: hex string (for stem)
|
||||
* leftHash: hex string (for fork)
|
||||
* rightHash: hex string (for fork)
|
||||
*/
|
||||
export function deserializePayload(payload) {
|
||||
if (payload.length === 0) {
|
||||
throw new Error("empty payload");
|
||||
}
|
||||
|
||||
const type = payload.readUInt8(0);
|
||||
|
||||
switch (type) {
|
||||
case 0x00:
|
||||
if (payload.length !== 1) {
|
||||
throw new Error(
|
||||
`invalid leaf payload: expected 1 byte, got ${payload.length}`
|
||||
);
|
||||
}
|
||||
return { type: "leaf" };
|
||||
|
||||
case 0x01:
|
||||
if (payload.length !== 1 + HASH_LENGTH) {
|
||||
throw new Error(
|
||||
`invalid stem payload: expected ${1 + HASH_LENGTH} bytes, got ${payload.length}`
|
||||
);
|
||||
}
|
||||
return {
|
||||
type: "stem",
|
||||
childHash: rawToHex(payload.slice(1, 1 + HASH_LENGTH)),
|
||||
};
|
||||
|
||||
case 0x02:
|
||||
if (payload.length !== 1 + 2 * HASH_LENGTH) {
|
||||
throw new Error(
|
||||
`invalid fork payload: expected ${1 + 2 * HASH_LENGTH} bytes, got ${payload.length}`
|
||||
);
|
||||
}
|
||||
return {
|
||||
type: "fork",
|
||||
leftHash: rawToHex(payload.slice(1, 1 + HASH_LENGTH)),
|
||||
rightHash: rawToHex(payload.slice(1 + HASH_LENGTH, 1 + 2 * HASH_LENGTH)),
|
||||
};
|
||||
|
||||
default:
|
||||
throw new Error(
|
||||
`invalid merkle node payload: unknown type 0x${type.toString(16)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the canonical payload bytes for a given tree node structure.
|
||||
*/
|
||||
export function serializeNode(node) {
|
||||
switch (node.type) {
|
||||
case "leaf":
|
||||
return Buffer.from([0x00]);
|
||||
case "stem":
|
||||
return Buffer.concat([Buffer.from([0x01]), hexToRaw(node.childHash)]);
|
||||
case "fork":
|
||||
return Buffer.concat([
|
||||
Buffer.from([0x02]),
|
||||
hexToRaw(node.leftHash),
|
||||
hexToRaw(node.rightHash),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the Merkle hash of a node from its type and parameters.
|
||||
*/
|
||||
export function computeNodeHash(node) {
|
||||
const payload = serializeNode(node);
|
||||
const hash = nodeHash(DOMAIN_TAG, payload);
|
||||
return hash.toString("hex");
|
||||
}
|
||||
|
||||
// ── Node section parsing ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Parse the node section binary into a Map<hexHash, { type, payload, node }>.
|
||||
*
|
||||
* Node section format:
|
||||
* nodeCount (8B u64 BE)
|
||||
* entries[]:
|
||||
* hash (32B raw)
|
||||
* payloadLen (4B u32 BE)
|
||||
* payload (payloadLen bytes)
|
||||
*/
|
||||
export function parseNodeSection(data) {
|
||||
if (data.length < 8) {
|
||||
throw new Error("node section too short for count");
|
||||
}
|
||||
|
||||
const nodeCount = Number(data.readBigUInt64BE(0));
|
||||
let offset = 8;
|
||||
|
||||
const nodeMap = new Map();
|
||||
const errors = [];
|
||||
|
||||
for (let i = 0; i < nodeCount; i++) {
|
||||
// Read hash
|
||||
if (offset + HASH_LENGTH > data.length) {
|
||||
errors.push(`node ${i}: not enough bytes for hash`);
|
||||
break;
|
||||
}
|
||||
const hash = rawToHex(data.slice(offset, offset + HASH_LENGTH));
|
||||
offset += HASH_LENGTH;
|
||||
|
||||
// Read payload length
|
||||
if (offset + 4 > data.length) {
|
||||
errors.push(`node ${i} (${hash}): not enough bytes for payload length`);
|
||||
break;
|
||||
}
|
||||
const payloadLen = data.readUint32BE(offset);
|
||||
offset += 4;
|
||||
|
||||
// Read payload
|
||||
if (offset + payloadLen > data.length) {
|
||||
errors.push(`node ${i} (${hash}): payload extends beyond section end`);
|
||||
break;
|
||||
}
|
||||
const payload = data.slice(offset, offset + payloadLen);
|
||||
offset += payloadLen;
|
||||
|
||||
// Deserialize payload
|
||||
let node;
|
||||
try {
|
||||
node = deserializePayload(payload);
|
||||
} catch (e) {
|
||||
errors.push(`node ${i} (${hash}): ${e.message}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
nodeMap.set(hash, {
|
||||
hash,
|
||||
payload,
|
||||
...node,
|
||||
});
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(
|
||||
`node section parse errors:\n ${errors.join("\n ")}`
|
||||
);
|
||||
}
|
||||
|
||||
return { nodeMap, count: nodeCount };
|
||||
}
|
||||
|
||||
// ── Verification ────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Verify all node hashes match their payloads.
|
||||
* Returns { verified, mismatches }
|
||||
*/
|
||||
export function verifyNodeHashes(nodeMap) {
|
||||
const mismatches = [];
|
||||
|
||||
for (const [hash, node] of nodeMap) {
|
||||
const expected = computeNodeHash(node);
|
||||
if (hash !== expected) {
|
||||
mismatches.push({
|
||||
hash,
|
||||
expected,
|
||||
type: node.type,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { verified: mismatches.length === 0, mismatches };
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that all child references exist in the node map (closure).
|
||||
* Returns { complete, missing } where missing is an array of { parent, child }.
|
||||
*/
|
||||
export function verifyClosure(nodeMap) {
|
||||
const missing = [];
|
||||
|
||||
for (const [hash, node] of nodeMap) {
|
||||
if (node.type === "stem") {
|
||||
if (!nodeMap.has(node.childHash)) {
|
||||
missing.push({ parent: hash, child: node.childHash });
|
||||
}
|
||||
} else if (node.type === "fork") {
|
||||
if (!nodeMap.has(node.leftHash)) {
|
||||
missing.push({ parent: hash, child: node.leftHash });
|
||||
}
|
||||
if (!nodeMap.has(node.rightHash)) {
|
||||
missing.push({ parent: hash, child: node.rightHash });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { complete: missing.length === 0, missing };
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify closure for a specific root hash (transitive reachability).
|
||||
* Returns { complete, missingRoots }.
|
||||
*/
|
||||
export function verifyRootClosure(nodeMap, rootHash) {
|
||||
const visited = new Set();
|
||||
const missingRoots = [];
|
||||
|
||||
function visit(hash) {
|
||||
if (visited.has(hash)) return;
|
||||
if (!nodeMap.has(hash)) {
|
||||
missingRoots.push(hash);
|
||||
return;
|
||||
}
|
||||
visited.add(hash);
|
||||
const node = nodeMap.get(hash);
|
||||
if (node.type === "stem") {
|
||||
visit(node.childHash);
|
||||
} else if (node.type === "fork") {
|
||||
visit(node.leftHash);
|
||||
visit(node.rightHash);
|
||||
}
|
||||
}
|
||||
|
||||
visit(rootHash);
|
||||
return { complete: missingRoots.length === 0, missingRoots };
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
/**
|
||||
* tree.js — Runtime tree representation.
|
||||
*
|
||||
* The JS tree uses a simple array representation matching the
|
||||
* TypeScript reference evaluator:
|
||||
*
|
||||
* Leaf = []
|
||||
* Stem = [child] (array length === 1)
|
||||
* Fork = [right, left] (array length === 2)
|
||||
*
|
||||
* This is a "flattened stack" representation: when reduced, terms
|
||||
* become arrays and the evaluator pops three elements at a time.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check if a value is a Leaf (empty array).
|
||||
*/
|
||||
export function isLeaf(t) {
|
||||
return Array.isArray(t) && t.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value is a Stem (single element array).
|
||||
*/
|
||||
export function isStem(t) {
|
||||
return Array.isArray(t) && t.length === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value is a Fork (two element array).
|
||||
*/
|
||||
export function isFork(t) {
|
||||
return Array.isArray(t) && t.length === 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value is a valid tree calculus value (Leaf, Stem, or Fork).
|
||||
*/
|
||||
export function isTree(t) {
|
||||
return isLeaf(t) || isStem(t) || isFork(t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triage a tree: classify it as Leaf/Stem/Fork.
|
||||
* The tree must be in normal form (no reducible redexes).
|
||||
*
|
||||
* Returns { kind: "leaf"|"stem"|"fork", ...rest }
|
||||
*/
|
||||
export function triage(t) {
|
||||
if (!Array.isArray(t)) {
|
||||
throw new Error("not a tree (not an array)");
|
||||
}
|
||||
if (t.length === 0) return { kind: "leaf" };
|
||||
if (t.length === 1) return { kind: "stem", child: t[0] };
|
||||
if (t.length === 2) return { kind: "fork", right: t[0], left: t[1] };
|
||||
throw new Error(`not a value/binary tree: length ${t.length}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the Tree Calculus apply rules.
|
||||
*
|
||||
* apply(a, b) computes the application of term a to term b.
|
||||
*
|
||||
* Rules:
|
||||
* apply(Fork(Leaf, a), _) = a
|
||||
* apply(Fork(Stem(a), b), c) = apply(apply(a, c), apply(b, c))
|
||||
* apply(Fork(Fork, _, _), Leaf) = left of inner Fork
|
||||
* apply(Fork(Fork, _, _), Stem) = right of inner Fork
|
||||
* apply(Fork(Fork, _, _), Fork) = apply(apply(c, u), v) where c=Fork(u,v)
|
||||
* apply(Leaf, b) = Stem(b)
|
||||
* apply(Stem(a), b) = Fork(a, b)
|
||||
*
|
||||
* For Fork, the inner structure is [right, left], so:
|
||||
* a = right, b = left
|
||||
*/
|
||||
export function apply(a, b) {
|
||||
// apply(Fork(Leaf, a), _) = a
|
||||
// Fork = [right, left] = [Leaf, a] → left child is Leaf
|
||||
if (isFork(a) && isLeaf(a[1])) {
|
||||
return a[0]; // return right child
|
||||
}
|
||||
|
||||
// apply(Fork(Stem(a), b), c)
|
||||
if (isFork(a) && isStem(a[1])) {
|
||||
const stemChild = a[1][0]; // left child of fork
|
||||
const right = a[0]; // right child of fork
|
||||
const innerA = stemChild;
|
||||
const innerB = right;
|
||||
const appliedA = apply(innerA, b);
|
||||
const appliedB = apply(innerB, b);
|
||||
return apply(appliedA, appliedB);
|
||||
}
|
||||
|
||||
// apply(Fork(Fork, _, _), Leaf)
|
||||
if (isFork(a) && isFork(a[1]) && isLeaf(b)) {
|
||||
return a[1][0]; // right child of inner fork (which is left child)
|
||||
}
|
||||
|
||||
// apply(Fork(Fork, _, _), Stem)
|
||||
if (isFork(a) && isFork(a[1]) && isStem(b)) {
|
||||
return a[1][1]; // left child of inner fork
|
||||
}
|
||||
|
||||
// apply(Fork(Fork, _, _), Fork)
|
||||
if (isFork(a) && isFork(a[1]) && isFork(b)) {
|
||||
// b = Fork(u, v) = [v, u]
|
||||
const u = b[0];
|
||||
const v = b[1];
|
||||
// apply(apply(c, u), v) where c = inner fork
|
||||
const applied = apply(apply(a[1], u), v);
|
||||
return applied;
|
||||
}
|
||||
|
||||
// apply(Leaf, b) = Stem(b)
|
||||
if (isLeaf(a)) {
|
||||
return [b];
|
||||
}
|
||||
|
||||
// apply(Stem(a), b) = Fork(a, b)
|
||||
if (isStem(a)) {
|
||||
return [b, a[0]]; // [right, left]
|
||||
}
|
||||
|
||||
throw new Error("apply: undefined reduction for terms");
|
||||
}
|
||||
@@ -1,134 +1,93 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { strictEqual, ok, throws } from "node:assert";
|
||||
import { createHash } from "node:crypto";
|
||||
import { describe, it } from "node:test";
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { strictEqual, ok, throws } from 'node:assert';
|
||||
import { describe, it } from 'node:test';
|
||||
import {
|
||||
parseBundle,
|
||||
parseManifest,
|
||||
} from "../src/bundle.js";
|
||||
import {
|
||||
parseNodeSection as bundleParseNodeSection,
|
||||
} from "../src/bundle.js";
|
||||
import {
|
||||
verifyNodeHashes,
|
||||
parseNodeSection as parseNodes,
|
||||
} from "../src/merkle.js";
|
||||
findLib,
|
||||
init,
|
||||
free,
|
||||
loadBundle,
|
||||
loadBundleDefault,
|
||||
kernelRoot,
|
||||
} from '../src/lib.js';
|
||||
|
||||
const fixtureDir = "../../test/fixtures";
|
||||
const fixtureDir = '../../test/fixtures';
|
||||
const libPath = findLib();
|
||||
|
||||
describe("bundle parsing", () => {
|
||||
it("valid bundle parses header and sections", () => {
|
||||
const bundle = parseBundle(
|
||||
readFileSync(`${fixtureDir}/id.arboricx`)
|
||||
);
|
||||
strictEqual(bundle.version, "1.0");
|
||||
strictEqual(bundle.sectionCount, 2);
|
||||
ok(bundle.sections.has(1)); // manifest
|
||||
ok(bundle.sections.has(2)); // nodes
|
||||
});
|
||||
|
||||
it("parseManifest returns valid manifest", () => {
|
||||
const manifest = parseManifest(
|
||||
readFileSync(`${fixtureDir}/id.arboricx`)
|
||||
);
|
||||
strictEqual(manifest.schema, "arboricx.bundle.manifest.v1");
|
||||
strictEqual(manifest.bundleType, "tree-calculus-executable-object");
|
||||
strictEqual(manifest.closure, "complete");
|
||||
strictEqual(manifest.tree.calculus, "tree-calculus.v1");
|
||||
strictEqual(manifest.tree.nodeHash.algorithm, "sha256");
|
||||
strictEqual(manifest.tree.nodeHash.domain, "arboricx.merkle.node.v1");
|
||||
strictEqual(manifest.runtime.semantics, "tree-calculus.v1");
|
||||
strictEqual(manifest.runtime.abi, "arboricx.abi.tree.v1");
|
||||
describe('library discovery', () => {
|
||||
it('findLib returns an existing .so path', () => {
|
||||
ok(libPath.endsWith('.so') || libPath.endsWith('.dylib') || libPath.endsWith('.dll'));
|
||||
ok(readFileSync(libPath));
|
||||
});
|
||||
});
|
||||
|
||||
describe("hash verification", () => {
|
||||
it("valid bundle nodes verify", () => {
|
||||
const data = bundleParseNodeSection(
|
||||
readFileSync(`${fixtureDir}/id.arboricx`)
|
||||
);
|
||||
const { nodeMap } = parseNodes(data);
|
||||
const { verified } = verifyNodeHashes(nodeMap);
|
||||
ok(verified, "all node hashes should verify");
|
||||
describe('context lifecycle', () => {
|
||||
it('init creates a valid context', () => {
|
||||
const ctx = init(libPath);
|
||||
ok(ctx);
|
||||
free(ctx);
|
||||
});
|
||||
|
||||
it('kernel root is available', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const root = kernelRoot(ctx);
|
||||
ok(root > 0, 'kernel root should be a positive index');
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("errors", () => {
|
||||
it("bad magic fails", () => {
|
||||
const buf = Buffer.alloc(32, 0);
|
||||
buf.write("WRONGMAG", 0, 8);
|
||||
throws(() => parseBundle(buf), /invalid magic/);
|
||||
describe('bundle loading', () => {
|
||||
it('loadBundleDefault loads id.arboricx', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const bundle = readFileSync(`${fixtureDir}/id.arboricx`);
|
||||
const root = loadBundleDefault(ctx, bundle);
|
||||
ok(root > 0, 'loaded root should be a positive index');
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
it("unsupported version fails", () => {
|
||||
const buf = Buffer.alloc(32, 0);
|
||||
buf.write("ARBORICX", 0, 8);
|
||||
buf.writeUInt16BE(2, 8); // major version 2
|
||||
throws(() => parseBundle(buf), /unsupported bundle major version/);
|
||||
it('loadBundleDefault loads true.arboricx', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const bundle = readFileSync(`${fixtureDir}/true.arboricx`);
|
||||
const root = loadBundleDefault(ctx, bundle);
|
||||
ok(root > 0);
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
it("bad section digest fails", () => {
|
||||
const buf = readFileSync(`${fixtureDir}/id.arboricx`);
|
||||
// Corrupt one byte in the manifest section
|
||||
buf[152] ^= 0x01;
|
||||
throws(() => parseBundle(buf), /digest mismatch/);
|
||||
it('loadBundle loads named export from id.arboricx', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const bundle = readFileSync(`${fixtureDir}/id.arboricx`);
|
||||
const root = loadBundle(ctx, bundle, 'id');
|
||||
ok(root > 0);
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
it("truncated bundle fails", () => {
|
||||
const buf = readFileSync(`${fixtureDir}/id.arboricx`);
|
||||
const truncated = buf.slice(0, 40);
|
||||
throws(() => parseBundle(truncated), /truncated/);
|
||||
it('loadBundle fails for missing export name', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const bundle = readFileSync(`${fixtureDir}/id.arboricx`);
|
||||
throws(() => loadBundle(ctx, bundle, 'nonexistent'), /failed/);
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
it("missing nodes section fails", () => {
|
||||
// Build a bundle with only manifest entry in the directory (1 section instead of 2)
|
||||
const header = Buffer.alloc(32, 0);
|
||||
header.write("ARBORICX", 0, 8);
|
||||
header.writeUInt16BE(1, 8); // major version
|
||||
header.writeUInt16BE(0, 10); // minor version
|
||||
header.writeUInt32BE(1, 12); // 1 section
|
||||
|
||||
// Build a manifest JSON
|
||||
const manifestObj = {
|
||||
schema: "arboricx.bundle.manifest.v1",
|
||||
bundleType: "tree-calculus-executable-object",
|
||||
tree: {
|
||||
calculus: "tree-calculus.v1",
|
||||
nodeHash: {
|
||||
algorithm: "sha256",
|
||||
domain: "arboricx.merkle.node.v1"
|
||||
},
|
||||
nodePayload: "arboricx.merkle.payload.v1"
|
||||
},
|
||||
runtime: {
|
||||
semantics: "tree-calculus.v1",
|
||||
evaluation: "normal-order",
|
||||
abi: "arboricx.abi.tree.v1",
|
||||
capabilities: []
|
||||
},
|
||||
closure: "complete",
|
||||
roots: [{ hash: Buffer.alloc(32).toString("hex"), role: "default" }],
|
||||
exports: [{ name: "root", root: Buffer.alloc(32).toString("hex"), kind: "term", abi: "arboricx.abi.tree.v1" }],
|
||||
metadata: { createdBy: "arboricx" }
|
||||
};
|
||||
const manifestJson = JSON.stringify(manifestObj);
|
||||
const manifestBytes = Buffer.from(manifestJson);
|
||||
|
||||
// Section directory entry (60 bytes, all fields are u64 after the u16s)
|
||||
const entry = Buffer.alloc(60, 0);
|
||||
entry.writeUInt32BE(1, 0); // type: manifest
|
||||
entry.writeUInt16BE(1, 4); // version
|
||||
entry.writeUInt16BE(1, 6); // flags: critical
|
||||
entry.writeUInt16BE(0, 8); // compression: none
|
||||
entry.writeUInt16BE(1, 10); // digest algorithm: sha256
|
||||
entry.writeBigUInt64BE(BigInt(32 + 60), 12); // offset (u64)
|
||||
entry.writeBigUInt64BE(BigInt(manifestBytes.length), 20); // length (u64)
|
||||
entry.set(createHash("sha256").update(manifestBytes).digest(), 28); // digest (32 bytes)
|
||||
|
||||
// Set dirOffset to 32 so parseBundle reads directory from after header
|
||||
header.writeBigUInt64BE(BigInt(32), 24);
|
||||
|
||||
const bundleBuf = Buffer.concat([header, entry, manifestBytes]);
|
||||
throws(() => parseBundle(bundleBuf), /missing required section/);
|
||||
it('loadBundleDefault fails for invalid bytes', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
throws(() => loadBundleDefault(ctx, Buffer.from('not a bundle')), /failed/);
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { strictEqual, ok } from "node:assert";
|
||||
import { describe, it } from "node:test";
|
||||
import { parseNodeSection as bundleParseNodeSection, parseBundle, parseManifest } from "../src/bundle.js";
|
||||
import {
|
||||
verifyNodeHashes,
|
||||
verifyClosure,
|
||||
verifyRootClosure,
|
||||
deserializePayload,
|
||||
computeNodeHash,
|
||||
parseNodeSection,
|
||||
} from "../src/merkle.js";
|
||||
|
||||
describe("merkle — deserializePayload", () => {
|
||||
it("Leaf (0x00)", () => {
|
||||
const result = deserializePayload(Buffer.from([0x00]));
|
||||
strictEqual(result.type, "leaf");
|
||||
});
|
||||
|
||||
it("Stem (0x01 + 32 bytes)", () => {
|
||||
const childHash = Buffer.alloc(32, 0xab);
|
||||
const payload = Buffer.concat([Buffer.from([0x01]), childHash]);
|
||||
const result = deserializePayload(payload);
|
||||
strictEqual(result.type, "stem");
|
||||
strictEqual(result.childHash, "ab".repeat(32));
|
||||
});
|
||||
|
||||
it("Fork (0x02 + 64 bytes)", () => {
|
||||
const left = Buffer.alloc(32, 0x01);
|
||||
const right = Buffer.alloc(32, 0x02);
|
||||
const payload = Buffer.concat([Buffer.from([0x02]), left, right]);
|
||||
const result = deserializePayload(payload);
|
||||
strictEqual(result.type, "fork");
|
||||
strictEqual(result.leftHash, "01".repeat(32));
|
||||
strictEqual(result.rightHash, "02".repeat(32));
|
||||
});
|
||||
|
||||
it("Leaf with extra bytes fails", () => {
|
||||
throws(() => deserializePayload(Buffer.from([0x00, 0x00])), /invalid leaf/);
|
||||
});
|
||||
|
||||
it("Unknown type fails", () => {
|
||||
throws(() => deserializePayload(Buffer.from([0xff])), /unknown type/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("merkle — computeNodeHash", () => {
|
||||
it("Leaf hash is correct length", () => {
|
||||
const leaf = { type: "leaf" };
|
||||
const hash = computeNodeHash(leaf);
|
||||
strictEqual(hash.length, 64);
|
||||
});
|
||||
|
||||
it("Leaf hash matches expected Arboricx domain", () => {
|
||||
const leaf = { type: "leaf" };
|
||||
const hash = computeNodeHash(leaf);
|
||||
strictEqual(hash, "92b8a9796dbeafbcd36757535876256392170d137bf36b319d77f11a37112158");
|
||||
});
|
||||
});
|
||||
|
||||
describe("merkle — node section parsing", () => {
|
||||
const fixtureDir = "../../test/fixtures";
|
||||
|
||||
it("parses id.arboricx with correct node count", () => {
|
||||
const data = bundleParseNodeSection(
|
||||
readFileSync(`${fixtureDir}/id.arboricx`)
|
||||
);
|
||||
const { nodeMap } = parseNodeSection(data);
|
||||
strictEqual(nodeMap.size, 4);
|
||||
});
|
||||
|
||||
it("parses true.arboricx with correct node count", () => {
|
||||
const data = bundleParseNodeSection(
|
||||
readFileSync(`${fixtureDir}/true.arboricx`)
|
||||
);
|
||||
const { nodeMap } = parseNodeSection(data);
|
||||
strictEqual(nodeMap.size, 2);
|
||||
});
|
||||
|
||||
it("parses false.arboricx with correct node count", () => {
|
||||
const data = bundleParseNodeSection(
|
||||
readFileSync(`${fixtureDir}/false.arboricx`)
|
||||
);
|
||||
const { nodeMap } = parseNodeSection(data);
|
||||
strictEqual(nodeMap.size, 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("merkle — hash verification", () => {
|
||||
const fixtureDir = "../../test/fixtures";
|
||||
|
||||
it("id.arboricx nodes all verify", () => {
|
||||
const data = bundleParseNodeSection(
|
||||
readFileSync(`${fixtureDir}/id.arboricx`)
|
||||
);
|
||||
const { nodeMap } = parseNodeSection(data);
|
||||
const { verified, mismatches } = verifyNodeHashes(nodeMap);
|
||||
ok(verified, "id.arboricx node hashes should verify");
|
||||
strictEqual(mismatches.length, 0);
|
||||
});
|
||||
|
||||
it("true.arboricx nodes all verify", () => {
|
||||
const data = bundleParseNodeSection(
|
||||
readFileSync(`${fixtureDir}/true.arboricx`)
|
||||
);
|
||||
const { nodeMap } = parseNodeSection(data);
|
||||
const { verified, mismatches } = verifyNodeHashes(nodeMap);
|
||||
ok(verified, "true.arboricx node hashes should verify");
|
||||
strictEqual(mismatches.length, 0);
|
||||
});
|
||||
|
||||
it("corrupted node payload fails hash verification", () => {
|
||||
const data = bundleParseNodeSection(
|
||||
readFileSync(`${fixtureDir}/id.arboricx`)
|
||||
);
|
||||
const { nodeMap } = parseNodeSection(data);
|
||||
// Find a stem node to corrupt
|
||||
let stemKey = null;
|
||||
for (const [key, node] of nodeMap) {
|
||||
if (node.type === "stem") { stemKey = key; break; }
|
||||
}
|
||||
ok(stemKey, "should find a stem node to corrupt");
|
||||
const stem = nodeMap.get(stemKey);
|
||||
// Corrupt the child hash so serializeNode produces a different payload
|
||||
const corrupted = {
|
||||
...stem,
|
||||
childHash: "00".repeat(32),
|
||||
payload: Buffer.concat([Buffer.from([0x01]), Buffer.alloc(32, 0x00)]),
|
||||
};
|
||||
nodeMap.set(stemKey, corrupted);
|
||||
const { verified, mismatches } = verifyNodeHashes(nodeMap);
|
||||
ok(!verified, "corrupted stem should fail hash verification");
|
||||
ok(mismatches.length > 0, "should have mismatches");
|
||||
});
|
||||
});
|
||||
|
||||
describe("merkle — closure verification", () => {
|
||||
const fixtureDir = "../../test/fixtures";
|
||||
|
||||
it("id.arboricx has complete closure", () => {
|
||||
const data = bundleParseNodeSection(
|
||||
readFileSync(`${fixtureDir}/id.arboricx`)
|
||||
);
|
||||
const { nodeMap } = parseNodeSection(data);
|
||||
const { complete, missing } = verifyClosure(nodeMap);
|
||||
ok(complete, "id.arboricx should have complete closure");
|
||||
strictEqual(missing.length, 0);
|
||||
});
|
||||
|
||||
it("verifyRootClosure checks transitive reachability", () => {
|
||||
const data = bundleParseNodeSection(
|
||||
readFileSync(`${fixtureDir}/id.arboricx`)
|
||||
);
|
||||
const { nodeMap } = parseNodeSection(data);
|
||||
// Use the actual root hash from the fixture's manifest
|
||||
const manifest = parseManifest(readFileSync(`${fixtureDir}/id.arboricx`));
|
||||
const rootHash = manifest.exports[0].root;
|
||||
const { complete, missingRoots } = verifyRootClosure(nodeMap, rootHash);
|
||||
ok(complete, "root should be reachable");
|
||||
strictEqual(missingRoots.length, 0);
|
||||
});
|
||||
|
||||
it("parseNodeSection returns correct node count", () => {
|
||||
const data = bundleParseNodeSection(
|
||||
readFileSync(`${fixtureDir}/id.arboricx`)
|
||||
);
|
||||
const result = parseNodeSection(data);
|
||||
strictEqual(result.count, 4);
|
||||
});
|
||||
});
|
||||
|
||||
// Helper for throws
|
||||
function throws(fn, expected) {
|
||||
try {
|
||||
fn();
|
||||
return false;
|
||||
} catch (e) {
|
||||
return expected.test(e.message);
|
||||
}
|
||||
}
|
||||
@@ -1,80 +1,113 @@
|
||||
import { strictEqual, ok } from "node:assert";
|
||||
import { describe, it } from "node:test";
|
||||
import { apply, isLeaf, isStem, isFork } from "../src/tree.js";
|
||||
import { reduce } from "../src/cli.js";
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { strictEqual, ok } from 'node:assert';
|
||||
import { describe, it } from 'node:test';
|
||||
import {
|
||||
findLib,
|
||||
init,
|
||||
free,
|
||||
leaf,
|
||||
stem,
|
||||
fork,
|
||||
app,
|
||||
reduce,
|
||||
toBool,
|
||||
toString,
|
||||
toNumber,
|
||||
loadBundleDefault,
|
||||
ofString,
|
||||
ofNumber,
|
||||
} from '../src/lib.js';
|
||||
|
||||
describe("tree — basic types", () => {
|
||||
it("Leaf is empty array", () => {
|
||||
ok(isLeaf([]));
|
||||
ok(!isStem([]));
|
||||
ok(!isFork([]));
|
||||
const libPath = findLib();
|
||||
|
||||
describe('tree construction', () => {
|
||||
it('leaf returns a positive index', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const idx = leaf(ctx);
|
||||
ok(idx > 0);
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
it("Stem is single-element array", () => {
|
||||
ok(isStem([[]]));
|
||||
ok(!isLeaf([[]]));
|
||||
it('stem wraps a child', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const l = leaf(ctx);
|
||||
const s = stem(ctx, l);
|
||||
ok(s > 0);
|
||||
ok(s !== l);
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
it("Fork is two-element array", () => {
|
||||
ok(isFork([[], []]));
|
||||
ok(!isLeaf([[], []]));
|
||||
it('fork combines left and right', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const a = leaf(ctx);
|
||||
const b = leaf(ctx);
|
||||
const f = fork(ctx, a, b);
|
||||
ok(f > 0);
|
||||
ok(f !== a && f !== b);
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("tree — apply rules", () => {
|
||||
// Leaf = [], Stem = [child], Fork = [right, left]
|
||||
|
||||
it("apply(Leaf, b) = Stem(b)", () => {
|
||||
const b = []; // Leaf
|
||||
const result = apply([], b);
|
||||
ok(isStem(result), "Stem(b) should be a Stem");
|
||||
strictEqual(result[0], b);
|
||||
describe('reduction — booleans', () => {
|
||||
it('true.arboricx reduces to boolean true', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const bundle = readFileSync('../../test/fixtures/true.arboricx');
|
||||
const root = loadBundleDefault(ctx, bundle);
|
||||
const result = reduce(ctx, root, 1_000_000n);
|
||||
strictEqual(toBool(ctx, result), true);
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
it("apply(Stem(a), b) = Fork(a, b)", () => {
|
||||
const a = []; // Leaf
|
||||
const b = []; // Leaf
|
||||
const result = apply([a], b);
|
||||
ok(isFork(result), "Fork(a, b) should be a Fork");
|
||||
// Fork = [right, left] = [b, a]
|
||||
strictEqual(result[0], b);
|
||||
strictEqual(result[1], a);
|
||||
});
|
||||
|
||||
it("apply(Fork(Leaf, a), _) = a", () => {
|
||||
// Fork(Leaf, a) = [a, Leaf]
|
||||
const a = []; // Leaf
|
||||
const result = apply([a, []], []);
|
||||
strictEqual(result, a);
|
||||
ok(isLeaf(result));
|
||||
it('false.arboricx reduces to boolean false', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const bundle = readFileSync('../../test/fixtures/false.arboricx');
|
||||
const root = loadBundleDefault(ctx, bundle);
|
||||
const result = reduce(ctx, root, 1_000_000n);
|
||||
strictEqual(toBool(ctx, result), false);
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("tree — reduction", () => {
|
||||
it("reduces Leaf to Leaf", () => {
|
||||
const result = reduce([], 100);
|
||||
ok(isLeaf(result));
|
||||
});
|
||||
|
||||
it("reduces Stem Leaf to Stem Leaf", () => {
|
||||
const result = reduce([[]], 100);
|
||||
ok(isStem(result));
|
||||
ok(isLeaf(result[0]));
|
||||
});
|
||||
|
||||
it("reduces Fork Leaf Leaf to Fork Leaf Leaf", () => {
|
||||
const result = reduce([[], []], 100);
|
||||
ok(isFork(result));
|
||||
ok(isLeaf(result[0]));
|
||||
ok(isLeaf(result[1]));
|
||||
});
|
||||
|
||||
it("S combinator applied to Leaf reduces", () => {
|
||||
// S = t (t (t t)) t = Fork (Fork (Fork Leaf Leaf) Leaf) Leaf
|
||||
// In array form: [[[], []], [], []]
|
||||
const s = [[], [[[], []], []]];
|
||||
const leaf = [];
|
||||
const result = reduce([s, leaf], 100);
|
||||
ok(Array.isArray(result), "S Leaf should reduce to an array");
|
||||
describe('reduction — id', () => {
|
||||
it('id applied to string returns the string', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const bundle = readFileSync('../../test/fixtures/id.arboricx');
|
||||
const idRoot = loadBundleDefault(ctx, bundle);
|
||||
const arg = ofString(ctx, 'hello');
|
||||
const applied = app(ctx, idRoot, arg);
|
||||
const result = reduce(ctx, applied, 1_000_000n);
|
||||
strictEqual(toString(ctx, result), 'hello');
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('reduction — numbers', () => {
|
||||
it('ofNumber round-trips through toNumber', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const num = ofNumber(ctx, 42);
|
||||
strictEqual(toNumber(ctx, num), 42);
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,120 +1,125 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { strictEqual, ok, throws } from "node:assert";
|
||||
import { describe, it } from "node:test";
|
||||
import { parseManifest } from "../src/bundle.js";
|
||||
import { parseNodeSection as bundleParseNodeSection } from "../src/bundle.js";
|
||||
import { validateManifest, selectExport } from "../src/manifest.js";
|
||||
import { verifyNodeHashes, parseNodeSection as parseNodes } from "../src/merkle.js";
|
||||
import { buildTreeFromNodeMap } from "../src/cli.js";
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { strictEqual, ok, throws } from 'node:assert';
|
||||
import { describe, it } from 'node:test';
|
||||
import {
|
||||
findLib,
|
||||
init,
|
||||
free,
|
||||
loadBundleDefault,
|
||||
loadBundle,
|
||||
reduce,
|
||||
app,
|
||||
ofString,
|
||||
ofNumber,
|
||||
toBool,
|
||||
toString,
|
||||
decode,
|
||||
decodeType,
|
||||
} from '../src/lib.js';
|
||||
|
||||
const fixtureDir = "../../test/fixtures";
|
||||
const fixtureDir = '../../test/fixtures';
|
||||
const libPath = findLib();
|
||||
|
||||
describe("run bundle — id.arboricx", () => {
|
||||
const bundle = readFileSync(`${fixtureDir}/id.arboricx`);
|
||||
const manifest = parseManifest(bundle);
|
||||
const nodeSectionData = bundleParseNodeSection(bundle);
|
||||
const { nodeMap } = parseNodes(nodeSectionData);
|
||||
|
||||
it("manifest validates", () => {
|
||||
validateManifest(manifest);
|
||||
describe('run bundle — booleans', () => {
|
||||
it('true.arboricx evaluates to true', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const bundle = readFileSync(`${fixtureDir}/true.arboricx`);
|
||||
const root = loadBundleDefault(ctx, bundle);
|
||||
const result = reduce(ctx, root);
|
||||
strictEqual(toBool(ctx, result), true);
|
||||
strictEqual(decodeType(ctx, result), 'bool');
|
||||
strictEqual(decode(ctx, result), 'true');
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
it("node hashes verify", () => {
|
||||
const { verified } = verifyNodeHashes(nodeMap);
|
||||
ok(verified);
|
||||
});
|
||||
|
||||
it("export 'root' is selectable", () => {
|
||||
const exp = selectExport(manifest, "root");
|
||||
strictEqual(exp.name, "root");
|
||||
});
|
||||
|
||||
it("tree reconstructs as a Fork", () => {
|
||||
const exp = selectExport(manifest, "root");
|
||||
const tree = buildTreeFromNodeMap(nodeMap, exp.root);
|
||||
ok(Array.isArray(tree));
|
||||
ok(tree.length >= 2, "tree should be a Fork (length >= 2)");
|
||||
it('false.arboricx evaluates to false', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const bundle = readFileSync(`${fixtureDir}/false.arboricx`);
|
||||
const root = loadBundleDefault(ctx, bundle);
|
||||
const result = reduce(ctx, root);
|
||||
strictEqual(toBool(ctx, result), false);
|
||||
strictEqual(decodeType(ctx, result), 'bool');
|
||||
strictEqual(decode(ctx, result), 'false');
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("run bundle — true.arboricx", () => {
|
||||
const bundle = readFileSync(`${fixtureDir}/true.arboricx`);
|
||||
const manifest = parseManifest(bundle);
|
||||
const nodeSectionData = bundleParseNodeSection(bundle);
|
||||
const { nodeMap } = parseNodes(nodeSectionData);
|
||||
|
||||
it("manifest validates", () => {
|
||||
validateManifest(manifest);
|
||||
});
|
||||
|
||||
it("export 'root' is selectable", () => {
|
||||
const exp = selectExport(manifest, "root");
|
||||
strictEqual(exp.name, "root");
|
||||
});
|
||||
|
||||
it("tree reconstructs as Stem Leaf", () => {
|
||||
const exp = selectExport(manifest, "root");
|
||||
const tree = buildTreeFromNodeMap(nodeMap, exp.root);
|
||||
ok(Array.isArray(tree));
|
||||
strictEqual(tree.length, 1, "true should be a Stem (single child)");
|
||||
strictEqual(tree[0].length, 0, "child should be Leaf");
|
||||
describe('run bundle — id', () => {
|
||||
it('id applied to string returns the string', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const bundle = readFileSync(`${fixtureDir}/id.arboricx`);
|
||||
const idRoot = loadBundleDefault(ctx, bundle);
|
||||
const arg = ofString(ctx, 'hello');
|
||||
const applied = app(ctx, idRoot, arg);
|
||||
const result = reduce(ctx, applied);
|
||||
strictEqual(toString(ctx, result), 'hello');
|
||||
strictEqual(decodeType(ctx, result), 'string');
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("run bundle — false.arboricx", () => {
|
||||
const bundle = readFileSync(`${fixtureDir}/false.arboricx`);
|
||||
const manifest = parseManifest(bundle);
|
||||
const nodeSectionData = bundleParseNodeSection(bundle);
|
||||
const { nodeMap } = parseNodes(nodeSectionData);
|
||||
|
||||
it("manifest validates", () => {
|
||||
validateManifest(manifest);
|
||||
});
|
||||
|
||||
it("export 'root' is selectable", () => {
|
||||
const exp = selectExport(manifest, "root");
|
||||
strictEqual(exp.name, "root");
|
||||
});
|
||||
|
||||
it("tree reconstructs as Leaf", () => {
|
||||
const exp = selectExport(manifest, "root");
|
||||
const tree = buildTreeFromNodeMap(nodeMap, exp.root);
|
||||
strictEqual(tree.length, 0, "false should be Leaf (empty array)");
|
||||
describe('run bundle — append', () => {
|
||||
it('append "hello " "world" = "hello world"', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const bundle = readFileSync(`${fixtureDir}/append.arboricx`);
|
||||
let term = loadBundleDefault(ctx, bundle);
|
||||
term = app(ctx, term, ofString(ctx, 'hello '));
|
||||
term = app(ctx, term, ofString(ctx, 'world'));
|
||||
const result = reduce(ctx, term);
|
||||
strictEqual(toString(ctx, result), 'hello world');
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("run bundle — notQ.arboricx", () => {
|
||||
const bundle = readFileSync(`${fixtureDir}/notQ.arboricx`);
|
||||
const manifest = parseManifest(bundle);
|
||||
const nodeSectionData = bundleParseNodeSection(bundle);
|
||||
const { nodeMap } = parseNodes(nodeSectionData);
|
||||
|
||||
it("manifest validates", () => {
|
||||
validateManifest(manifest);
|
||||
});
|
||||
|
||||
it("node hashes verify", () => {
|
||||
const { verified } = verifyNodeHashes(nodeMap);
|
||||
ok(verified);
|
||||
describe('run bundle — notQ', () => {
|
||||
it('notQ loads and reduces without error', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const bundle = readFileSync(`${fixtureDir}/notQ.arboricx`);
|
||||
const root = loadBundleDefault(ctx, bundle);
|
||||
const result = reduce(ctx, root);
|
||||
ok(result > 0);
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("run bundle — missing export", () => {
|
||||
const bundle = readFileSync(`${fixtureDir}/id.arboricx`);
|
||||
const manifest = parseManifest(bundle);
|
||||
describe('run bundle — named export', () => {
|
||||
it('loadBundle selects named export', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const bundle = readFileSync(`${fixtureDir}/id.arboricx`);
|
||||
const root = loadBundle(ctx, bundle, 'id');
|
||||
ok(root > 0);
|
||||
// id is a function; apply it before reducing
|
||||
const applied = app(ctx, root, ofString(ctx, 'test'));
|
||||
const result = reduce(ctx, applied);
|
||||
strictEqual(toString(ctx, result), 'test');
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
it("nonexistent export fails clearly", () => {
|
||||
throws(() => selectExport(manifest, "nonexistent"), /not found/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("run bundle — auto-select", () => {
|
||||
// true.arboricx has only one export, should auto-select
|
||||
const bundle = readFileSync(`${fixtureDir}/true.arboricx`);
|
||||
const manifest = parseManifest(bundle);
|
||||
|
||||
it("single export auto-selects", () => {
|
||||
const exp = selectExport(manifest, undefined);
|
||||
ok(exp, "should auto-select the only export");
|
||||
it('missing export throws', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const bundle = readFileSync(`${fixtureDir}/id.arboricx`);
|
||||
throws(() => loadBundle(ctx, bundle, 'nonexistent'), /failed/);
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
53
ext/php/public/eval.php
Normal file
53
ext/php/public/eval.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
if (!extension_loaded('ffi')) {
|
||||
http_response_code(500);
|
||||
echo "Error: PHP FFI extension is not loaded.\n";
|
||||
echo "If you are using the Nix build, run the included server script:\n";
|
||||
echo " ./result/bin/tricu-php-server\n";
|
||||
exit;
|
||||
}
|
||||
|
||||
require __DIR__ . '/../src/common.php';
|
||||
|
||||
use function Arboricx\{ctx_init, ctx_free, loadBundleDefault, ofNumber, ofString, app, reduce, decode, findLib, readBundle};
|
||||
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
|
||||
try {
|
||||
if (!isset($_FILES['bundle']) || $_FILES['bundle']['error'] !== UPLOAD_ERR_OK) {
|
||||
throw new \RuntimeException('Bundle upload failed.');
|
||||
}
|
||||
|
||||
$args = [];
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$v = $_POST["arg$i"] ?? '';
|
||||
if ($v !== '') {
|
||||
$args[] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
$libPath = findLib();
|
||||
$ctx = ctx_init($libPath);
|
||||
try {
|
||||
$term = loadBundleDefault($ctx, readBundle($_FILES['bundle']['tmp_name']));
|
||||
|
||||
foreach ($args as $arg) {
|
||||
$argTree = preg_match('/^\d+$/', $arg) ? ofNumber($ctx, (int)$arg) : ofString($ctx, $arg);
|
||||
$term = app($ctx, $term, $argTree);
|
||||
}
|
||||
|
||||
$result = reduce($ctx, $term, 1_000_000_000);
|
||||
echo decode($ctx, $result);
|
||||
} finally {
|
||||
ctx_free($ctx);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo 'Error: ' . $e->getMessage();
|
||||
}
|
||||
30
ext/php/public/index.php
Normal file
30
ext/php/public/index.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Arboricx Web</title>
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Arboricx Bundle Runner</h1>
|
||||
<form hx-post="eval.php" hx-target="#result" enctype="multipart/form-data">
|
||||
<p>
|
||||
<label>Bundle (.arboricx)<br>
|
||||
<input type="file" name="bundle" accept=".arboricx" required></label>
|
||||
</p>
|
||||
<?php for ($i = 0; $i < 5; $i++): ?>
|
||||
<p>
|
||||
<label>Arg <?= $i + 1 ?> <small>(ignored if empty)</small><br>
|
||||
<input type="text" name="arg<?= $i ?>"></label>
|
||||
</p>
|
||||
<?php endfor; ?>
|
||||
<p>
|
||||
<button type="submit">Run</button>
|
||||
</p>
|
||||
</form>
|
||||
<pre id="result"></pre>
|
||||
</body>
|
||||
</html>
|
||||
@@ -11,89 +11,16 @@ declare(strict_types=1);
|
||||
* php run.php inspect <bundle.arboricx>
|
||||
*/
|
||||
|
||||
require __DIR__ . '/src/ffi.php';
|
||||
require __DIR__ . '/src/common.php';
|
||||
|
||||
use function Arboricx\{ctx_init, ctx_free, loadBundleDefault, ofNumber, ofString, app, reduce, toString, toBool, toNumber};
|
||||
|
||||
// ── Locate libarboricx.so ──────────────────────────────────────────────────
|
||||
|
||||
function findLib(): string
|
||||
{
|
||||
$env = getenv('ARBORICX_LIB');
|
||||
if ($env !== false && file_exists($env)) {
|
||||
return $env;
|
||||
}
|
||||
|
||||
$paths = [
|
||||
__DIR__ . '/../../zig/zig-out/lib/libarboricx.so',
|
||||
'/usr/local/lib/libarboricx.so',
|
||||
'/usr/lib/libarboricx.so',
|
||||
'./libarboricx.so',
|
||||
];
|
||||
foreach ($paths as $p) {
|
||||
if (file_exists($p)) {
|
||||
return $p;
|
||||
}
|
||||
}
|
||||
|
||||
fwrite(STDERR, "Error: libarboricx.so not found.\nSet ARBORICX_LIB to its full path.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// ── Decode helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
function decode(\FFI\CData $ctx, int $root): string
|
||||
{
|
||||
// Bool first: false is Leaf, which is also a valid empty string/list.
|
||||
try {
|
||||
return toBool($ctx, $root) ? 'true' : 'false';
|
||||
} catch (\Throwable $e) {
|
||||
try {
|
||||
return toString($ctx, $root);
|
||||
} catch (\Throwable $e2) {
|
||||
try {
|
||||
return (string) toNumber($ctx, $root);
|
||||
} catch (\Throwable $e3) {
|
||||
throw new \RuntimeException('could not decode result');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function decodeType(\FFI\CData $ctx, int $root): string
|
||||
{
|
||||
try {
|
||||
toBool($ctx, $root);
|
||||
return 'bool';
|
||||
} catch (\Throwable $e) {
|
||||
try {
|
||||
toString($ctx, $root);
|
||||
return 'string';
|
||||
} catch (\Throwable $e2) {
|
||||
try {
|
||||
toNumber($ctx, $root);
|
||||
return 'number';
|
||||
} catch (\Throwable $e3) {
|
||||
return 'unknown (raw tree)';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
use function Arboricx\{ctx_init, ctx_free, loadBundleDefault, ofNumber, ofString, app, reduce, toString, toBool, toNumber, findLib, decode, decodeType, readBundle};
|
||||
|
||||
// ── Commands ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function readBundle(string $path): string
|
||||
function bail(string $msg): void
|
||||
{
|
||||
if (!file_exists($path)) {
|
||||
fwrite(STDERR, "Error: bundle not found: $path\n");
|
||||
exit(1);
|
||||
}
|
||||
$bytes = file_get_contents($path);
|
||||
if ($bytes === false) {
|
||||
fwrite(STDERR, "Error: could not read bundle: $path\n");
|
||||
exit(1);
|
||||
}
|
||||
return $bytes;
|
||||
fwrite(STDERR, "Error: $msg\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
function cmdRun(string $libPath, string $bundlePath, array $args): void
|
||||
@@ -109,6 +36,8 @@ function cmdRun(string $libPath, string $bundlePath, array $args): void
|
||||
|
||||
$result = reduce($ctx, $term, 1_000_000_000);
|
||||
echo decode($ctx, $result) . "\n";
|
||||
} catch (\Throwable $e) {
|
||||
bail($e->getMessage());
|
||||
} finally {
|
||||
ctx_free($ctx);
|
||||
}
|
||||
@@ -131,6 +60,8 @@ function cmdInspect(string $libPath, string $bundlePath): void
|
||||
$value = '(raw tree)';
|
||||
}
|
||||
echo " Type: $type\n Value: $value\n";
|
||||
} catch (\Throwable $e) {
|
||||
bail($e->getMessage());
|
||||
} finally {
|
||||
ctx_free($ctx);
|
||||
}
|
||||
|
||||
81
ext/php/src/common.php
Normal file
81
ext/php/src/common.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Arboricx;
|
||||
|
||||
require __DIR__ . '/ffi.php';
|
||||
|
||||
use function Arboricx\{ctx_init, ctx_free, loadBundleDefault, ofNumber, ofString, app, reduce, toString, toBool, toNumber};
|
||||
|
||||
function findLib(): string
|
||||
{
|
||||
$env = getenv('ARBORICX_LIB');
|
||||
if ($env !== false && file_exists($env)) {
|
||||
return $env;
|
||||
}
|
||||
|
||||
$paths = [
|
||||
__DIR__ . '/../../zig/zig-out/lib/libarboricx.so',
|
||||
__DIR__ . '/../libarboricx.so',
|
||||
'/usr/local/lib/libarboricx.so',
|
||||
'/usr/lib/libarboricx.so',
|
||||
'./libarboricx.so',
|
||||
];
|
||||
foreach ($paths as $p) {
|
||||
if (file_exists($p)) {
|
||||
return $p;
|
||||
}
|
||||
}
|
||||
|
||||
throw new \RuntimeException('libarboricx.so not found. Set ARBORICX_LIB to its full path.');
|
||||
}
|
||||
|
||||
function decode(\FFI\CData $ctx, int $root): string
|
||||
{
|
||||
try {
|
||||
return toBool($ctx, $root) ? 'true' : 'false';
|
||||
} catch (\Throwable $e) {
|
||||
try {
|
||||
return toString($ctx, $root);
|
||||
} catch (\Throwable $e2) {
|
||||
try {
|
||||
return (string) toNumber($ctx, $root);
|
||||
} catch (\Throwable $e3) {
|
||||
throw new \RuntimeException('could not decode result');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function decodeType(\FFI\CData $ctx, int $root): string
|
||||
{
|
||||
try {
|
||||
toBool($ctx, $root);
|
||||
return 'bool';
|
||||
} catch (\Throwable $e) {
|
||||
try {
|
||||
toString($ctx, $root);
|
||||
return 'string';
|
||||
} catch (\Throwable $e2) {
|
||||
try {
|
||||
toNumber($ctx, $root);
|
||||
return 'number';
|
||||
} catch (\Throwable $e3) {
|
||||
return 'unknown (raw tree)';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function readBundle(string $path): string
|
||||
{
|
||||
if (!file_exists($path)) {
|
||||
throw new \RuntimeException("bundle not found: $path");
|
||||
}
|
||||
$bytes = file_get_contents($path);
|
||||
if ($bytes === false) {
|
||||
throw new \RuntimeException("could not read bundle: $path");
|
||||
}
|
||||
return $bytes;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1
ext/zig/result
Symbolic link
1
ext/zig/result
Symbolic link
@@ -0,0 +1 @@
|
||||
/nix/store/2sg31y0vamz5bz19aakxagi702glwh24-tricu-zig-0.1.0
|
||||
@@ -2,19 +2,15 @@ const std = @import("std");
|
||||
const tree = @import("tree.zig");
|
||||
const Arena = @import("arena.zig").Arena;
|
||||
|
||||
pub const Hash = [32]u8;
|
||||
|
||||
pub const Error = error{
|
||||
InvalidMagic,
|
||||
InvalidVersion,
|
||||
Truncated,
|
||||
InvalidManifest,
|
||||
InvalidNodePayload,
|
||||
HashMismatch,
|
||||
ExportNotFound,
|
||||
MissingChild,
|
||||
UnexpectedFormat,
|
||||
DigestMismatch,
|
||||
OutOfMemory,
|
||||
};
|
||||
|
||||
@@ -57,13 +53,6 @@ const Parser = struct {
|
||||
return std.mem.readInt(u64, b[0..8], .big);
|
||||
}
|
||||
|
||||
fn readHash(self: *Parser) Error!Hash {
|
||||
const b = try self.expect(32);
|
||||
var h: Hash = undefined;
|
||||
@memcpy(&h, b);
|
||||
return h;
|
||||
}
|
||||
|
||||
fn readLengthPrefixedBytes(self: *Parser, allocator: std.mem.Allocator) Error![]const u8 {
|
||||
const len = try self.readU32();
|
||||
const bytes = try self.expect(len);
|
||||
@@ -77,7 +66,6 @@ const SectionEntry = struct {
|
||||
section_type: u32,
|
||||
offset: u64,
|
||||
length: u64,
|
||||
digest: Hash,
|
||||
};
|
||||
|
||||
fn parseHeader(p: *Parser) Error!struct { major: u16, minor: u16, section_count: u32, dir_offset: u64 } {
|
||||
@@ -104,25 +92,16 @@ fn parseSectionEntries(p: *Parser, count: u32, allocator: std.mem.Allocator) Err
|
||||
_ = try p.readU16(); // section_version
|
||||
_ = try p.readU16(); // section_flags
|
||||
const compression = try p.readU16();
|
||||
const digest_alg = try p.readU16();
|
||||
_ = try p.readU16(); // reserved (was digest_alg)
|
||||
entry.offset = try p.readU64();
|
||||
entry.length = try p.readU64();
|
||||
entry.digest = try p.readHash();
|
||||
_ = try p.readU32(); // reserved padding
|
||||
|
||||
if (compression != 0) return error.UnexpectedFormat;
|
||||
if (digest_alg != 1) return error.UnexpectedFormat;
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
fn sha256Digest(data: []const u8) Hash {
|
||||
var h = std.crypto.hash.sha2.Sha256.init(.{});
|
||||
h.update(data);
|
||||
var out: Hash = undefined;
|
||||
h.final(&out);
|
||||
return out;
|
||||
}
|
||||
|
||||
fn parseManifest(p: *Parser, allocator: std.mem.Allocator) Error!struct { exports: []Export, roots: []Root } {
|
||||
const magic = try p.expect(8);
|
||||
if (!std.mem.eql(u8, magic, "ARBMNFST")) return error.InvalidManifest;
|
||||
@@ -145,15 +124,15 @@ fn parseManifest(p: *Parser, allocator: std.mem.Allocator) Error!struct { export
|
||||
|
||||
const hash_alg = try p.readLengthPrefixedBytes(allocator);
|
||||
defer allocator.free(hash_alg);
|
||||
if (!std.mem.eql(u8, hash_alg, "sha256")) return error.UnexpectedFormat;
|
||||
if (!std.mem.eql(u8, hash_alg, "indexed")) return error.UnexpectedFormat;
|
||||
|
||||
const hash_domain = try p.readLengthPrefixedBytes(allocator);
|
||||
defer allocator.free(hash_domain);
|
||||
if (!std.mem.eql(u8, hash_domain, "arboricx.merkle.node.v1")) return error.UnexpectedFormat;
|
||||
if (!std.mem.eql(u8, hash_domain, "arboricx.indexed.node.v1")) return error.UnexpectedFormat;
|
||||
|
||||
const payload_type = try p.readLengthPrefixedBytes(allocator);
|
||||
defer allocator.free(payload_type);
|
||||
if (!std.mem.eql(u8, payload_type, "arboricx.merkle.payload.v1")) return error.UnexpectedFormat;
|
||||
if (!std.mem.eql(u8, payload_type, "arboricx.indexed.payload.v1")) return error.UnexpectedFormat;
|
||||
|
||||
const sem = try p.readLengthPrefixedBytes(allocator);
|
||||
defer allocator.free(sem);
|
||||
@@ -182,7 +161,7 @@ fn parseManifest(p: *Parser, allocator: std.mem.Allocator) Error!struct { export
|
||||
const roots = try allocator.alloc(Root, root_count);
|
||||
errdefer allocator.free(roots);
|
||||
for (roots) |*r| {
|
||||
r.hash = try p.readHash();
|
||||
r.index = try p.readU32();
|
||||
r.role = try p.readLengthPrefixedBytes(allocator);
|
||||
}
|
||||
|
||||
@@ -198,7 +177,7 @@ fn parseManifest(p: *Parser, allocator: std.mem.Allocator) Error!struct { export
|
||||
}
|
||||
for (exports) |*e| {
|
||||
e.name = try p.readLengthPrefixedBytes(allocator);
|
||||
e.root = try p.readHash();
|
||||
e.root = try p.readU32();
|
||||
e.kind = try p.readLengthPrefixedBytes(allocator);
|
||||
e.abi = try p.readLengthPrefixedBytes(allocator);
|
||||
if (!std.mem.eql(u8, e.abi, "arboricx.abi.tree.v1")) return error.UnexpectedFormat;
|
||||
@@ -225,135 +204,62 @@ fn parseManifest(p: *Parser, allocator: std.mem.Allocator) Error!struct { export
|
||||
|
||||
const Export = struct {
|
||||
name: []const u8,
|
||||
root: Hash,
|
||||
root: u32,
|
||||
kind: []const u8,
|
||||
abi: []const u8,
|
||||
};
|
||||
|
||||
const Root = struct {
|
||||
hash: Hash,
|
||||
index: u32,
|
||||
role: []const u8,
|
||||
};
|
||||
|
||||
fn parseNodeSection(p: *Parser, allocator: std.mem.Allocator) Error!std.AutoHashMap(Hash, []const u8) {
|
||||
/// Parse the node section and build nodes directly into the arena.
|
||||
/// Returns a slice mapping node-section index -> arena index.
|
||||
/// The caller owns the returned slice and must free it with the arena's allocator.
|
||||
fn parseNodeSection(p: *Parser, arena: *Arena) Error![]u32 {
|
||||
const node_count = try p.readU64();
|
||||
var map = std.AutoHashMap(Hash, []const u8).init(allocator);
|
||||
errdefer map.deinit();
|
||||
const indices = try arena.allocator.alloc(u32, node_count);
|
||||
errdefer arena.allocator.free(indices);
|
||||
|
||||
var i: u64 = 0;
|
||||
while (i < node_count) : (i += 1) {
|
||||
const hash = try p.readHash();
|
||||
const plen = try p.readU32();
|
||||
const payload = try p.expect(plen);
|
||||
|
||||
const expected_hash = blk: {
|
||||
var h = std.crypto.hash.sha2.Sha256.init(.{});
|
||||
h.update("arboricx.merkle.node.v1");
|
||||
h.update(&[_]u8{0});
|
||||
h.update(payload);
|
||||
var out: Hash = undefined;
|
||||
h.final(&out);
|
||||
break :blk out;
|
||||
};
|
||||
if (!std.mem.eql(u8, &hash, &expected_hash)) return error.HashMismatch;
|
||||
if (payload.len == 0) return error.InvalidNodePayload;
|
||||
|
||||
try map.put(hash, payload);
|
||||
const idx: u32 = switch (payload[0]) {
|
||||
0x00 => blk: {
|
||||
if (plen != 1) return error.InvalidNodePayload;
|
||||
break :blk try arena.alloc(.leaf);
|
||||
},
|
||||
0x01 => blk: {
|
||||
if (plen != 5) return error.InvalidNodePayload;
|
||||
const child_idx = std.mem.readInt(u32, payload[1..5], .big);
|
||||
if (child_idx >= i) return error.InvalidNodePayload;
|
||||
break :blk try arena.alloc(.{ .stem = .{ .child = indices[child_idx] } });
|
||||
},
|
||||
0x02 => blk: {
|
||||
if (plen != 9) return error.InvalidNodePayload;
|
||||
const left_idx = std.mem.readInt(u32, payload[1..5], .big);
|
||||
const right_idx = std.mem.readInt(u32, payload[5..9], .big);
|
||||
if (left_idx >= i or right_idx >= i) return error.InvalidNodePayload;
|
||||
break :blk try arena.alloc(.{ .fork = .{ .left = indices[left_idx], .right = indices[right_idx] } });
|
||||
},
|
||||
else => return error.InvalidNodePayload,
|
||||
};
|
||||
indices[i] = idx;
|
||||
}
|
||||
|
||||
return map;
|
||||
return indices;
|
||||
}
|
||||
|
||||
fn loadNode(
|
||||
arena: *Arena,
|
||||
payloads: std.AutoHashMap(Hash, []const u8),
|
||||
cache: *std.AutoHashMap(Hash, u32),
|
||||
root_hash: Hash,
|
||||
) Error!u32 {
|
||||
const Frame = struct {
|
||||
hash: Hash,
|
||||
state: u2,
|
||||
};
|
||||
|
||||
const max_stack = payloads.count() * 2;
|
||||
var stack = try arena.allocator.alloc(Frame, max_stack);
|
||||
defer arena.allocator.free(stack);
|
||||
var sp: usize = 0;
|
||||
|
||||
stack[sp] = .{ .hash = root_hash, .state = 0 };
|
||||
sp += 1;
|
||||
|
||||
while (sp > 0) {
|
||||
const frame = &stack[sp - 1];
|
||||
|
||||
if (cache.get(frame.hash)) |_| {
|
||||
sp -= 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (frame.state == 0) {
|
||||
frame.state = 1;
|
||||
const payload = payloads.get(frame.hash) orelse return error.MissingChild;
|
||||
if (payload.len == 0) return error.InvalidNodePayload;
|
||||
|
||||
switch (payload[0]) {
|
||||
0x00 => {
|
||||
if (payload.len != 1) return error.InvalidNodePayload;
|
||||
},
|
||||
0x01 => {
|
||||
if (payload.len != 33) return error.InvalidNodePayload;
|
||||
var child_hash: Hash = undefined;
|
||||
@memcpy(&child_hash, payload[1..33]);
|
||||
if (cache.get(child_hash) == null) {
|
||||
stack[sp] = .{ .hash = child_hash, .state = 0 };
|
||||
sp += 1;
|
||||
}
|
||||
},
|
||||
0x02 => {
|
||||
if (payload.len != 65) return error.InvalidNodePayload;
|
||||
var left_hash: Hash = undefined;
|
||||
var right_hash: Hash = undefined;
|
||||
@memcpy(&left_hash, payload[1..33]);
|
||||
@memcpy(&right_hash, payload[33..65]);
|
||||
const need_right = cache.get(right_hash) == null;
|
||||
const need_left = cache.get(left_hash) == null;
|
||||
if (need_right) {
|
||||
stack[sp] = .{ .hash = right_hash, .state = 0 };
|
||||
sp += 1;
|
||||
}
|
||||
if (need_left) {
|
||||
stack[sp] = .{ .hash = left_hash, .state = 0 };
|
||||
sp += 1;
|
||||
}
|
||||
},
|
||||
else => return error.InvalidNodePayload,
|
||||
}
|
||||
} else {
|
||||
const payload = payloads.get(frame.hash).?;
|
||||
const idx: u32 = switch (payload[0]) {
|
||||
0x00 => try arena.alloc(.leaf),
|
||||
0x01 => blk: {
|
||||
var child_hash: Hash = undefined;
|
||||
@memcpy(&child_hash, payload[1..33]);
|
||||
const child_idx = cache.get(child_hash).?;
|
||||
break :blk try arena.alloc(.{ .stem = .{ .child = child_idx } });
|
||||
},
|
||||
0x02 => blk: {
|
||||
var left_hash: Hash = undefined;
|
||||
var right_hash: Hash = undefined;
|
||||
@memcpy(&left_hash, payload[1..33]);
|
||||
@memcpy(&right_hash, payload[33..65]);
|
||||
const left_idx = cache.get(left_hash).?;
|
||||
const right_idx = cache.get(right_hash).?;
|
||||
break :blk try arena.alloc(.{ .fork = .{ .left = left_idx, .right = right_idx } });
|
||||
},
|
||||
else => unreachable,
|
||||
};
|
||||
try cache.put(frame.hash, idx);
|
||||
sp -= 1;
|
||||
}
|
||||
fn findSection(entries: []SectionEntry, section_type: u32) ?SectionEntry {
|
||||
for (entries) |entry| {
|
||||
if (entry.section_type == section_type) return entry;
|
||||
}
|
||||
|
||||
return cache.get(root_hash) orelse return error.MissingChild;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Parse an Arboricx bundle and load the named export into the arena.
|
||||
@@ -372,20 +278,11 @@ pub fn loadBundleExport(
|
||||
const entries = try parseSectionEntries(&p, header.section_count, allocator);
|
||||
defer allocator.free(entries);
|
||||
|
||||
var manifest_entry: ?SectionEntry = null;
|
||||
var nodes_entry: ?SectionEntry = null;
|
||||
for (entries) |entry| {
|
||||
if (entry.section_type == 1) manifest_entry = entry;
|
||||
if (entry.section_type == 2) nodes_entry = entry;
|
||||
}
|
||||
const manifest_section = manifest_entry orelse return error.InvalidManifest;
|
||||
const nodes_section = nodes_entry orelse return error.InvalidNodePayload;
|
||||
const manifest_section = findSection(entries, 1) orelse return error.InvalidManifest;
|
||||
const nodes_section = findSection(entries, 2) orelse return error.InvalidNodePayload;
|
||||
|
||||
const manifest_bytes = bundle_bytes[@intCast(manifest_section.offset)..@intCast(manifest_section.offset + manifest_section.length)];
|
||||
if (!std.mem.eql(u8, &sha256Digest(manifest_bytes), &manifest_section.digest)) return error.DigestMismatch;
|
||||
|
||||
const nodes_bytes = bundle_bytes[@intCast(nodes_section.offset)..@intCast(nodes_section.offset + nodes_section.length)];
|
||||
if (!std.mem.eql(u8, &sha256Digest(nodes_bytes), &nodes_section.digest)) return error.DigestMismatch;
|
||||
|
||||
var mp = Parser.init(manifest_bytes);
|
||||
const manifest = try parseManifest(&mp, allocator);
|
||||
@@ -402,23 +299,21 @@ pub fn loadBundleExport(
|
||||
allocator.free(manifest.roots);
|
||||
}
|
||||
|
||||
var export_hash: ?Hash = null;
|
||||
var export_root: ?u32 = null;
|
||||
for (manifest.exports) |e| {
|
||||
if (std.mem.eql(u8, e.name, export_name)) {
|
||||
export_hash = e.root;
|
||||
export_root = e.root;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const root_hash = export_hash orelse return error.ExportNotFound;
|
||||
const root_index = export_root orelse return error.ExportNotFound;
|
||||
|
||||
var np = Parser.init(nodes_bytes);
|
||||
var payloads = try parseNodeSection(&np, allocator);
|
||||
defer payloads.deinit();
|
||||
const node_indices = try parseNodeSection(&np, arena);
|
||||
defer allocator.free(node_indices);
|
||||
|
||||
var cache = std.AutoHashMap(Hash, u32).init(allocator);
|
||||
defer cache.deinit();
|
||||
|
||||
return try loadNode(arena, payloads, &cache, root_hash);
|
||||
if (root_index >= node_indices.len) return error.InvalidNodePayload;
|
||||
return node_indices[root_index];
|
||||
}
|
||||
|
||||
/// Parse an Arboricx bundle and load the default (first) root into the arena.
|
||||
@@ -435,20 +330,11 @@ pub fn loadBundleDefaultRoot(
|
||||
const entries = try parseSectionEntries(&p, header.section_count, allocator);
|
||||
defer allocator.free(entries);
|
||||
|
||||
var manifest_entry: ?SectionEntry = null;
|
||||
var nodes_entry: ?SectionEntry = null;
|
||||
for (entries) |entry| {
|
||||
if (entry.section_type == 1) manifest_entry = entry;
|
||||
if (entry.section_type == 2) nodes_entry = entry;
|
||||
}
|
||||
const manifest_section = manifest_entry orelse return error.InvalidManifest;
|
||||
const nodes_section = nodes_entry orelse return error.InvalidNodePayload;
|
||||
const manifest_section = findSection(entries, 1) orelse return error.InvalidManifest;
|
||||
const nodes_section = findSection(entries, 2) orelse return error.InvalidNodePayload;
|
||||
|
||||
const manifest_bytes = bundle_bytes[@intCast(manifest_section.offset)..@intCast(manifest_section.offset + manifest_section.length)];
|
||||
if (!std.mem.eql(u8, &sha256Digest(manifest_bytes), &manifest_section.digest)) return error.DigestMismatch;
|
||||
|
||||
const nodes_bytes = bundle_bytes[@intCast(nodes_section.offset)..@intCast(nodes_section.offset + nodes_section.length)];
|
||||
if (!std.mem.eql(u8, &sha256Digest(nodes_bytes), &nodes_section.digest)) return error.DigestMismatch;
|
||||
|
||||
var mp = Parser.init(manifest_bytes);
|
||||
const manifest = try parseManifest(&mp, allocator);
|
||||
@@ -466,14 +352,12 @@ pub fn loadBundleDefaultRoot(
|
||||
}
|
||||
|
||||
if (manifest.roots.len == 0) return error.ExportNotFound;
|
||||
const root_hash = manifest.roots[0].hash;
|
||||
const root_index = manifest.roots[0].index;
|
||||
|
||||
var np = Parser.init(nodes_bytes);
|
||||
var payloads = try parseNodeSection(&np, allocator);
|
||||
defer payloads.deinit();
|
||||
const node_indices = try parseNodeSection(&np, arena);
|
||||
defer allocator.free(node_indices);
|
||||
|
||||
var cache = std.AutoHashMap(Hash, u32).init(allocator);
|
||||
defer cache.deinit();
|
||||
|
||||
return try loadNode(arena, payloads, &cache, root_hash);
|
||||
if (root_index >= node_indices.len) return error.InvalidNodePayload;
|
||||
return node_indices[root_index];
|
||||
}
|
||||
|
||||
@@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
845
ext/zig/src/io_driver.zig
Normal file
845
ext/zig/src/io_driver.zig
Normal file
@@ -0,0 +1,845 @@
|
||||
const std = @import("std");
|
||||
const Arena = @import("arena.zig").Arena;
|
||||
const codecs = @import("codecs.zig");
|
||||
const reduce = @import("reduce.zig");
|
||||
const tree = @import("tree.zig");
|
||||
|
||||
const c = @cImport({
|
||||
@cInclude("uv.h");
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Action tag constants (must match lib/io.tri and IODriver.hs)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub const ActionTag = enum(u8) {
|
||||
pure = 0,
|
||||
bind = 1,
|
||||
putStr = 10,
|
||||
getLine = 11,
|
||||
readFile = 20,
|
||||
writeFile = 21,
|
||||
ask = 30,
|
||||
local = 31,
|
||||
get = 40,
|
||||
put = 41,
|
||||
fork = 60,
|
||||
await = 61,
|
||||
yield = 62,
|
||||
sleep = 63,
|
||||
};
|
||||
|
||||
pub const Action = union(ActionTag) {
|
||||
pure: u32,
|
||||
bind: struct { left: u32, k: u32 },
|
||||
putStr: u32,
|
||||
getLine,
|
||||
readFile: u32,
|
||||
writeFile: struct { path: u32, contents: u32 },
|
||||
ask,
|
||||
local: struct { f: u32, action: u32 },
|
||||
get,
|
||||
put: u32,
|
||||
fork: u32,
|
||||
await: u32,
|
||||
yield,
|
||||
sleep: u32,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Error codes (must match IODriver.hs)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const ERR_DOES_NOT_EXIST: u64 = 1;
|
||||
const ERR_PERMISSION: u64 = 2;
|
||||
const ERR_ALREADY_EXISTS: u64 = 3;
|
||||
const ERR_IO_OTHER: u64 = 4;
|
||||
const ERR_POLICY_DENY: u64 = 20;
|
||||
const ERR_INVALID_ACTION: u64 = 40;
|
||||
const ERR_INVALID_STRING: u64 = 41;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Permissions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub const IOPerms = struct {
|
||||
allow_read_all: bool = false,
|
||||
allow_write_all: bool = false,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// IO sentinel detection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn isIOSentinel(arena: *Arena, root: u32) !?u32 {
|
||||
const node = arena.get(root);
|
||||
if (node.* != .fork) return null;
|
||||
|
||||
const sentinel = node.fork.left;
|
||||
const rest = node.fork.right;
|
||||
|
||||
const sentinel_str = try codecs.toString(arena, sentinel);
|
||||
defer {
|
||||
if (sentinel_str) |s| {
|
||||
arena.allocator.free(s);
|
||||
}
|
||||
}
|
||||
if (sentinel_str == null) return null;
|
||||
if (!std.mem.eql(u8, sentinel_str.?, "tricuIO")) return null;
|
||||
|
||||
const rest_node = arena.get(rest);
|
||||
if (rest_node.* != .fork) return null;
|
||||
|
||||
const version_num = try codecs.toNumber(arena, rest_node.fork.left);
|
||||
if (version_num == null or version_num.? != 1) return null;
|
||||
|
||||
return rest_node.fork.right;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Action decoding
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn decodeAction(arena: *Arena, root: u32) !?Action {
|
||||
const node = arena.get(root);
|
||||
if (node.* != .fork) return null;
|
||||
|
||||
const tag_num = try codecs.toNumber(arena, node.fork.left);
|
||||
if (tag_num == null) return null;
|
||||
|
||||
const tag: ActionTag = switch (tag_num.?) {
|
||||
0 => .pure,
|
||||
1 => .bind,
|
||||
10 => .putStr,
|
||||
11 => .getLine,
|
||||
20 => .readFile,
|
||||
21 => .writeFile,
|
||||
30 => .ask,
|
||||
31 => .local,
|
||||
40 => .get,
|
||||
41 => .put,
|
||||
60 => .fork,
|
||||
61 => .await,
|
||||
62 => .yield,
|
||||
63 => .sleep,
|
||||
else => return null,
|
||||
};
|
||||
|
||||
const payload = node.fork.right;
|
||||
|
||||
return switch (tag) {
|
||||
.pure => Action{ .pure = payload },
|
||||
.bind => blk: {
|
||||
const payload_node = arena.get(payload);
|
||||
if (payload_node.* != .fork) return null;
|
||||
break :blk Action{ .bind = .{ .left = payload_node.fork.left, .k = payload_node.fork.right } };
|
||||
},
|
||||
.putStr => Action{ .putStr = payload },
|
||||
.getLine => Action.getLine,
|
||||
.readFile => Action{ .readFile = payload },
|
||||
.writeFile => blk: {
|
||||
const payload_node = arena.get(payload);
|
||||
if (payload_node.* != .fork) return null;
|
||||
break :blk Action{ .writeFile = .{ .path = payload_node.fork.left, .contents = payload_node.fork.right } };
|
||||
},
|
||||
.ask => Action.ask,
|
||||
.local => blk: {
|
||||
const payload_node = arena.get(payload);
|
||||
if (payload_node.* != .fork) return null;
|
||||
break :blk Action{ .local = .{ .f = payload_node.fork.left, .action = payload_node.fork.right } };
|
||||
},
|
||||
.get => Action.get,
|
||||
.put => Action{ .put = payload },
|
||||
.fork => Action{ .fork = payload },
|
||||
.await => Action{ .await = payload },
|
||||
.yield => Action.yield,
|
||||
.sleep => Action{ .sleep = payload },
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Response tree constructors
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn makePure(arena: *Arena, val: u32) !u32 {
|
||||
const tag = try codecs.ofNumber(arena, 0);
|
||||
return try arena.alloc(.{ .fork = .{ .left = tag, .right = val } });
|
||||
}
|
||||
|
||||
pub fn makeOkResult(arena: *Arena, val: u32) !u32 {
|
||||
const ok_tag = try arena.alloc(.{ .stem = .{ .child = try arena.alloc(.leaf) } });
|
||||
const val_pair = try arena.alloc(.{ .fork = .{ .left = val, .right = try arena.alloc(.leaf) } });
|
||||
return try arena.alloc(.{ .fork = .{ .left = ok_tag, .right = val_pair } });
|
||||
}
|
||||
|
||||
pub fn makeErrResult(arena: *Arena, code: u64) !u32 {
|
||||
const code_tree = try codecs.ofNumber(arena, code);
|
||||
const code_pair = try arena.alloc(.{ .fork = .{ .left = code_tree, .right = try arena.alloc(.leaf) } });
|
||||
return try arena.alloc(.{ .fork = .{ .left = try arena.alloc(.leaf), .right = code_pair } });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Frame stack and runtime
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const Frame = union(enum) {
|
||||
bind: u32, // continuation k
|
||||
local: u32, // old env
|
||||
};
|
||||
|
||||
const Runtime = struct {
|
||||
env: u32,
|
||||
state: u32,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: reduce a term in a scratch arena and copy the result back
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn reduceInScratch(gpa: std.mem.Allocator, arena: *Arena, term: u32) !u32 {
|
||||
var scratch = Arena.init(gpa);
|
||||
defer scratch.deinit();
|
||||
const scratch_root = try tree.copyTree(arena.nodes.items, &scratch, term);
|
||||
const scratch_result = try reduce.reduce(scratch_root, &scratch, std.math.maxInt(u64));
|
||||
return try tree.copyTree(scratch.nodes.items, arena, scratch_result);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Task
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const Task = struct {
|
||||
id: u64,
|
||||
parent: ?*Task,
|
||||
frames: std.ArrayList(Frame),
|
||||
runtime: Runtime,
|
||||
current: u32,
|
||||
status: enum { runnable, blocked, completed },
|
||||
result: ?u32,
|
||||
waiting_for: ?u64,
|
||||
|
||||
fn init(gpa: std.mem.Allocator, id: u64, parent: ?*Task, env: u32, state: u32, current: u32) !*Task {
|
||||
const task = try gpa.create(Task);
|
||||
task.* = .{
|
||||
.id = id,
|
||||
.parent = parent,
|
||||
.frames = std.ArrayList(Frame).empty,
|
||||
.runtime = .{ .env = env, .state = state },
|
||||
.current = current,
|
||||
.status = .runnable,
|
||||
.result = null,
|
||||
.waiting_for = null,
|
||||
};
|
||||
return task;
|
||||
}
|
||||
|
||||
fn deinit(self: *Task, gpa: std.mem.Allocator) void {
|
||||
self.frames.deinit(gpa);
|
||||
gpa.destroy(self);
|
||||
}
|
||||
|
||||
// finishValue processes a value through the frame stack.
|
||||
// Returns true if the task has completed (no more frames).
|
||||
fn finishValue(self: *Task, arena: *Arena, value: u32) !bool {
|
||||
if (self.frames.pop()) |frame| {
|
||||
switch (frame) {
|
||||
.bind => |k| {
|
||||
self.current = try arena.alloc(.{ .app = .{ .func = k, .arg = value } });
|
||||
return false;
|
||||
},
|
||||
.local => |old_env| {
|
||||
self.runtime.env = old_env;
|
||||
self.current = try makePure(arena, value);
|
||||
return false;
|
||||
},
|
||||
}
|
||||
} else {
|
||||
self.current = value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Scheduler
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const Scheduler = struct {
|
||||
gpa: std.mem.Allocator,
|
||||
loop: *c.uv_loop_t,
|
||||
arena: *Arena,
|
||||
tasks: std.ArrayList(*Task),
|
||||
runnable: std.ArrayList(*Task),
|
||||
next_id: u64,
|
||||
perms: IOPerms,
|
||||
|
||||
fn init(gpa: std.mem.Allocator, loop: *c.uv_loop_t, arena: *Arena, perms: IOPerms) !Scheduler {
|
||||
const sched = Scheduler{
|
||||
.gpa = gpa,
|
||||
.loop = loop,
|
||||
.arena = arena,
|
||||
.tasks = std.ArrayList(*Task).empty,
|
||||
.runnable = std.ArrayList(*Task).empty,
|
||||
.next_id = 1,
|
||||
.perms = perms,
|
||||
};
|
||||
return sched;
|
||||
}
|
||||
|
||||
fn deinit(self: *Scheduler) void {
|
||||
for (self.tasks.items) |task| {
|
||||
task.deinit(self.gpa);
|
||||
}
|
||||
self.tasks.deinit(self.gpa);
|
||||
self.runnable.deinit(self.gpa);
|
||||
}
|
||||
|
||||
fn createTask(self: *Scheduler, parent: ?*Task, env: u32, state: u32, current: u32) !*Task {
|
||||
const id = self.next_id;
|
||||
self.next_id += 1;
|
||||
const task = try Task.init(self.gpa, id, parent, env, state, current);
|
||||
try self.tasks.append(self.gpa, task);
|
||||
return task;
|
||||
}
|
||||
|
||||
fn run(self: *Scheduler) !void {
|
||||
while (true) {
|
||||
if (self.runnable.items.len > 0) {
|
||||
const task = self.runnable.orderedRemove(0);
|
||||
try self.stepTask(task);
|
||||
} else if (self.hasPendingHandles()) {
|
||||
_ = c.uv_run(self.loop, c.UV_RUN_ONCE);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn hasPendingHandles(self: *Scheduler) bool {
|
||||
return c.uv_loop_alive(self.loop) != 0;
|
||||
}
|
||||
|
||||
fn completeTask(self: *Scheduler, task: *Task) !void {
|
||||
task.status = .completed;
|
||||
task.result = task.current;
|
||||
// Unblock any tasks waiting for this one
|
||||
for (self.tasks.items) |t| {
|
||||
if (t.status == .blocked and t.waiting_for == task.id) {
|
||||
t.status = .runnable;
|
||||
t.waiting_for = null;
|
||||
t.current = try makePure(self.arena, task.result.?);
|
||||
try self.runnable.append(self.gpa, t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn stepTask(self: *Scheduler, task: *Task) !void {
|
||||
const reduced = try reduceInScratch(self.gpa, self.arena, task.current);
|
||||
|
||||
const decoded = try decodeAction(self.arena, reduced);
|
||||
if (decoded == null) {
|
||||
// Not a recognized action — if no frames, it's the final result.
|
||||
// Otherwise treat as invalid.
|
||||
if (task.frames.items.len == 0) {
|
||||
task.current = reduced;
|
||||
try self.completeTask(task);
|
||||
return;
|
||||
}
|
||||
const err = try makeErrResult(self.arena, ERR_INVALID_ACTION);
|
||||
if (try task.finishValue(self.arena, err)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch (decoded.?) {
|
||||
.pure => |val| {
|
||||
if (try task.finishValue(self.arena, val)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
},
|
||||
|
||||
.bind => |b| {
|
||||
try task.frames.append(self.gpa, .{ .bind = b.k });
|
||||
task.current = b.left;
|
||||
try self.runnable.append(self.gpa, task);
|
||||
},
|
||||
|
||||
.putStr => |str_tree| {
|
||||
const str = try codecs.toString(self.arena, str_tree) orelse {
|
||||
const err = try makeErrResult(self.arena, ERR_INVALID_STRING);
|
||||
if (try task.finishValue(self.arena, err)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
return;
|
||||
};
|
||||
defer self.gpa.free(str);
|
||||
_ = std.c.write(1, str.ptr, str.len);
|
||||
const leaf = try self.arena.alloc(.leaf);
|
||||
if (try task.finishValue(self.arena, leaf)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
},
|
||||
|
||||
.getLine => {
|
||||
var buf: [4096]u8 = undefined;
|
||||
var len: usize = 0;
|
||||
while (len < buf.len) {
|
||||
const n = std.c.read(0, buf[len..].ptr, 1);
|
||||
if (n <= 0) break;
|
||||
if (buf[len] == '\n') break;
|
||||
len += 1;
|
||||
}
|
||||
const line = buf[0..len];
|
||||
const str_tree = try codecs.ofString(self.arena, line);
|
||||
if (try task.finishValue(self.arena, str_tree)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
},
|
||||
|
||||
.readFile => |path_tree| {
|
||||
const path = try codecs.toString(self.arena, path_tree) orelse {
|
||||
const err = try makeErrResult(self.arena, ERR_INVALID_STRING);
|
||||
if (try task.finishValue(self.arena, err)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
if (!self.perms.allow_read_all) {
|
||||
self.arena.allocator.free(path);
|
||||
const err = try makeErrResult(self.arena, ERR_POLICY_DENY);
|
||||
if (try task.finishValue(self.arena, err)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const ctx = try self.gpa.create(FileReadCtx);
|
||||
ctx.* = .{
|
||||
.scheduler = self,
|
||||
.task = task,
|
||||
.arena = self.arena,
|
||||
.gpa = self.gpa,
|
||||
.fd = -1,
|
||||
.buf = std.ArrayList(u8).empty,
|
||||
.path = path,
|
||||
.req = undefined,
|
||||
.read_buf = null,
|
||||
};
|
||||
ctx.req.data = ctx;
|
||||
_ = c.uv_fs_open(self.loop, &ctx.req, ctx.path.ptr, c.O_RDONLY, 0, file_open_cb);
|
||||
},
|
||||
|
||||
.writeFile => |wf| {
|
||||
const path = try codecs.toString(self.arena, wf.path) orelse {
|
||||
const err = try makeErrResult(self.arena, ERR_INVALID_STRING);
|
||||
if (try task.finishValue(self.arena, err)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
const contents = try codecs.toString(self.arena, wf.contents) orelse {
|
||||
self.arena.allocator.free(path);
|
||||
const err = try makeErrResult(self.arena, ERR_INVALID_STRING);
|
||||
if (try task.finishValue(self.arena, err)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
if (!self.perms.allow_write_all) {
|
||||
self.arena.allocator.free(path);
|
||||
self.arena.allocator.free(contents);
|
||||
const err = try makeErrResult(self.arena, ERR_POLICY_DENY);
|
||||
if (try task.finishValue(self.arena, err)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const ctx = try self.gpa.create(FileWriteCtx);
|
||||
ctx.* = .{
|
||||
.scheduler = self,
|
||||
.task = task,
|
||||
.arena = self.arena,
|
||||
.gpa = self.gpa,
|
||||
.fd = -1,
|
||||
.path = path,
|
||||
.contents = contents,
|
||||
.written = false,
|
||||
.req = undefined,
|
||||
};
|
||||
ctx.req.data = ctx;
|
||||
const flags = c.O_WRONLY | c.O_CREAT | c.O_TRUNC;
|
||||
_ = c.uv_fs_open(self.loop, &ctx.req, ctx.path.ptr, flags, 0o644, file_write_open_cb);
|
||||
},
|
||||
|
||||
.ask => {
|
||||
if (try task.finishValue(self.arena, task.runtime.env)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
},
|
||||
|
||||
.local => |loc| {
|
||||
const new_env = try reduceInScratch(self.gpa, self.arena, try self.arena.alloc(.{ .app = .{ .func = loc.f, .arg = task.runtime.env } }));
|
||||
try task.frames.append(self.gpa, .{ .local = task.runtime.env });
|
||||
task.runtime.env = new_env;
|
||||
task.current = loc.action;
|
||||
try self.runnable.append(self.gpa, task);
|
||||
},
|
||||
|
||||
.get => {
|
||||
if (try task.finishValue(self.arena, task.runtime.state)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
},
|
||||
|
||||
.put => |new_state| {
|
||||
task.runtime.state = new_state;
|
||||
const leaf = try self.arena.alloc(.leaf);
|
||||
if (try task.finishValue(self.arena, leaf)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
},
|
||||
|
||||
.fork => |action| {
|
||||
const child = try self.createTask(task, task.runtime.env, task.runtime.state, action);
|
||||
try self.runnable.append(self.gpa, child);
|
||||
const handle = try codecs.ofNumber(self.arena, child.id);
|
||||
if (try task.finishValue(self.arena, handle)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
},
|
||||
|
||||
.await => |handle_tree| {
|
||||
const handle = try codecs.toNumber(self.arena, handle_tree) orelse {
|
||||
const err = try makeErrResult(self.arena, ERR_INVALID_ACTION);
|
||||
if (try task.finishValue(self.arena, err)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
return;
|
||||
};
|
||||
var found: ?*Task = null;
|
||||
for (self.tasks.items) |t| {
|
||||
if (t.id == handle) {
|
||||
found = t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found == null) {
|
||||
const err = try makeErrResult(self.arena, ERR_INVALID_ACTION);
|
||||
if (try task.finishValue(self.arena, err)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (found.?.status == .completed) {
|
||||
const result = found.?.result.?;
|
||||
if (try task.finishValue(self.arena, result)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
} else {
|
||||
task.status = .blocked;
|
||||
task.waiting_for = handle;
|
||||
// Task remains out of runnable until child completes
|
||||
}
|
||||
},
|
||||
|
||||
.yield => {
|
||||
const leaf = try self.arena.alloc(.leaf);
|
||||
if (try task.finishValue(self.arena, leaf)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
},
|
||||
|
||||
.sleep => |ms_tree| {
|
||||
const ms = try codecs.toNumber(self.arena, ms_tree) orelse 0;
|
||||
const ctx = try self.gpa.create(SleepCtx);
|
||||
ctx.* = .{
|
||||
.scheduler = self,
|
||||
.task = task,
|
||||
.arena = self.arena,
|
||||
.timer = undefined,
|
||||
};
|
||||
ctx.timer.data = ctx;
|
||||
_ = c.uv_timer_init(self.loop, &ctx.timer);
|
||||
_ = c.uv_timer_start(&ctx.timer, sleep_cb, @intCast(ms), 0);
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Async file read
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const FileReadCtx = struct {
|
||||
scheduler: *Scheduler,
|
||||
task: *Task,
|
||||
arena: *Arena,
|
||||
gpa: std.mem.Allocator,
|
||||
fd: c_int,
|
||||
buf: std.ArrayList(u8),
|
||||
path: []const u8,
|
||||
req: c.uv_fs_t,
|
||||
read_buf: ?[]u8,
|
||||
};
|
||||
|
||||
fn mapUvErr(uv_err: c_int) u64 {
|
||||
return switch (uv_err) {
|
||||
c.UV_ENOENT => ERR_DOES_NOT_EXIST,
|
||||
c.UV_EACCES => ERR_PERMISSION,
|
||||
c.UV_EEXIST => ERR_ALREADY_EXISTS,
|
||||
else => ERR_IO_OTHER,
|
||||
};
|
||||
}
|
||||
|
||||
fn file_open_cb(req: [*c]c.uv_fs_t) callconv(.c) void {
|
||||
const ctx = @as(*FileReadCtx, @ptrCast(@alignCast(req.*.data)));
|
||||
const result = req.*.result;
|
||||
c.uv_fs_req_cleanup(req);
|
||||
if (result < 0) {
|
||||
const err = makeErrResult(ctx.arena, mapUvErr(@intCast(-result))) catch {
|
||||
ctx.gpa.destroy(ctx);
|
||||
return;
|
||||
};
|
||||
if (ctx.task.finishValue(ctx.arena, err) catch false) {
|
||||
ctx.scheduler.completeTask(ctx.task) catch {};
|
||||
} else {
|
||||
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
|
||||
}
|
||||
ctx.buf.deinit(ctx.gpa);
|
||||
ctx.gpa.free(ctx.path);
|
||||
ctx.gpa.destroy(ctx);
|
||||
return;
|
||||
}
|
||||
ctx.fd = @intCast(result);
|
||||
const read_buf = ctx.gpa.alloc(u8, 4096) catch unreachable;
|
||||
ctx.read_buf = read_buf;
|
||||
var uv_buf = c.uv_buf_init(@ptrCast(read_buf.ptr), @intCast(read_buf.len));
|
||||
_ = c.uv_fs_read(ctx.scheduler.loop, req, ctx.fd, &uv_buf, 1, -1, file_read_cb);
|
||||
}
|
||||
|
||||
fn file_read_cb(req: [*c]c.uv_fs_t) callconv(.c) void {
|
||||
const ctx = @as(*FileReadCtx, @ptrCast(@alignCast(req.*.data)));
|
||||
const nread = req.*.result;
|
||||
c.uv_fs_req_cleanup(req);
|
||||
if (nread < 0) {
|
||||
_ = c.uv_fs_close(ctx.scheduler.loop, req, ctx.fd, null);
|
||||
const err = makeErrResult(ctx.arena, mapUvErr(@intCast(-nread))) catch {
|
||||
ctx.gpa.destroy(ctx);
|
||||
return;
|
||||
};
|
||||
if (ctx.task.finishValue(ctx.arena, err) catch false) {
|
||||
ctx.scheduler.completeTask(ctx.task) catch {};
|
||||
} else {
|
||||
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
|
||||
}
|
||||
if (ctx.read_buf) |b| ctx.gpa.free(b);
|
||||
ctx.buf.deinit(ctx.gpa);
|
||||
ctx.gpa.free(ctx.path);
|
||||
ctx.gpa.destroy(ctx);
|
||||
return;
|
||||
}
|
||||
if (nread == 0) {
|
||||
// EOF
|
||||
_ = c.uv_fs_close(ctx.scheduler.loop, req, ctx.fd, null);
|
||||
const bytes_tree = codecs.ofBytes(ctx.arena, ctx.buf.items) catch {
|
||||
ctx.gpa.destroy(ctx);
|
||||
return;
|
||||
};
|
||||
const ok = makeOkResult(ctx.arena, bytes_tree) catch {
|
||||
ctx.gpa.destroy(ctx);
|
||||
return;
|
||||
};
|
||||
if (ctx.task.finishValue(ctx.arena, ok) catch false) {
|
||||
ctx.scheduler.completeTask(ctx.task) catch {};
|
||||
} else {
|
||||
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
|
||||
}
|
||||
if (ctx.read_buf) |b| ctx.gpa.free(b);
|
||||
ctx.buf.deinit(ctx.gpa);
|
||||
ctx.gpa.free(ctx.path);
|
||||
ctx.gpa.destroy(ctx);
|
||||
return;
|
||||
}
|
||||
const data = ctx.read_buf.?[0..@intCast(nread)];
|
||||
ctx.buf.appendSlice(ctx.gpa, data) catch unreachable;
|
||||
const read_buf = ctx.gpa.alloc(u8, 4096) catch unreachable;
|
||||
ctx.read_buf = read_buf;
|
||||
var uv_buf = c.uv_buf_init(@ptrCast(read_buf.ptr), @intCast(read_buf.len));
|
||||
_ = c.uv_fs_read(ctx.scheduler.loop, req, ctx.fd, &uv_buf, 1, -1, file_read_cb);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Async file write
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const FileWriteCtx = struct {
|
||||
scheduler: *Scheduler,
|
||||
task: *Task,
|
||||
arena: *Arena,
|
||||
gpa: std.mem.Allocator,
|
||||
fd: c_int,
|
||||
path: []const u8,
|
||||
contents: []const u8,
|
||||
written: bool,
|
||||
req: c.uv_fs_t,
|
||||
};
|
||||
|
||||
fn file_write_open_cb(req: [*c]c.uv_fs_t) callconv(.c) void {
|
||||
const ctx = @as(*FileWriteCtx, @ptrCast(@alignCast(req.*.data)));
|
||||
const result = req.*.result;
|
||||
c.uv_fs_req_cleanup(req);
|
||||
if (result < 0) {
|
||||
const err = makeErrResult(ctx.arena, mapUvErr(@intCast(-result))) catch {
|
||||
ctx.gpa.destroy(ctx);
|
||||
return;
|
||||
};
|
||||
if (ctx.task.finishValue(ctx.arena, err) catch false) {
|
||||
ctx.scheduler.completeTask(ctx.task) catch {};
|
||||
} else {
|
||||
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
|
||||
}
|
||||
ctx.gpa.free(ctx.path);
|
||||
ctx.gpa.free(ctx.contents);
|
||||
ctx.gpa.destroy(ctx);
|
||||
return;
|
||||
}
|
||||
ctx.fd = @intCast(result);
|
||||
var uv_buf = c.uv_buf_init(@ptrCast(@constCast(ctx.contents.ptr)), @intCast(ctx.contents.len));
|
||||
_ = c.uv_fs_write(ctx.scheduler.loop, req, ctx.fd, &uv_buf, 1, 0, file_write_cb);
|
||||
}
|
||||
|
||||
fn file_write_cb(req: [*c]c.uv_fs_t) callconv(.c) void {
|
||||
const ctx = @as(*FileWriteCtx, @ptrCast(@alignCast(req.*.data)));
|
||||
const nwrite = req.*.result;
|
||||
c.uv_fs_req_cleanup(req);
|
||||
if (nwrite < 0) {
|
||||
_ = c.uv_fs_close(ctx.scheduler.loop, req, ctx.fd, null);
|
||||
const err = makeErrResult(ctx.arena, mapUvErr(@intCast(-nwrite))) catch {
|
||||
ctx.gpa.destroy(ctx);
|
||||
return;
|
||||
};
|
||||
if (ctx.task.finishValue(ctx.arena, err) catch false) {
|
||||
ctx.scheduler.completeTask(ctx.task) catch {};
|
||||
} else {
|
||||
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
|
||||
}
|
||||
ctx.gpa.free(ctx.path);
|
||||
ctx.gpa.free(ctx.contents);
|
||||
ctx.gpa.destroy(ctx);
|
||||
return;
|
||||
}
|
||||
_ = c.uv_fs_close(ctx.scheduler.loop, req, ctx.fd, file_write_close_cb);
|
||||
}
|
||||
|
||||
fn file_write_close_cb(req: [*c]c.uv_fs_t) callconv(.c) void {
|
||||
const ctx = @as(*FileWriteCtx, @ptrCast(@alignCast(req.*.data)));
|
||||
c.uv_fs_req_cleanup(req);
|
||||
const leaf = ctx.arena.alloc(.leaf) catch {
|
||||
ctx.gpa.destroy(ctx);
|
||||
return;
|
||||
};
|
||||
const ok = makeOkResult(ctx.arena, leaf) catch {
|
||||
ctx.gpa.destroy(ctx);
|
||||
return;
|
||||
};
|
||||
if (ctx.task.finishValue(ctx.arena, ok) catch false) {
|
||||
ctx.scheduler.completeTask(ctx.task) catch {};
|
||||
} else {
|
||||
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
|
||||
}
|
||||
ctx.gpa.free(ctx.path);
|
||||
ctx.gpa.free(ctx.contents);
|
||||
ctx.gpa.destroy(ctx);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Async sleep
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const SleepCtx = struct {
|
||||
scheduler: *Scheduler,
|
||||
task: *Task,
|
||||
arena: *Arena,
|
||||
timer: c.uv_timer_t,
|
||||
};
|
||||
|
||||
fn sleep_cb(handle: [*c]c.uv_timer_t) callconv(.c) void {
|
||||
const ctx = @as(*SleepCtx, @ptrCast(@alignCast(handle.*.data)));
|
||||
defer ctx.scheduler.gpa.destroy(ctx);
|
||||
const leaf = ctx.arena.alloc(.leaf) catch {
|
||||
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
|
||||
return;
|
||||
};
|
||||
if (ctx.task.finishValue(ctx.arena, leaf) catch false) {
|
||||
ctx.scheduler.completeTask(ctx.task) catch {};
|
||||
} else {
|
||||
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public entry point
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn runIO(gpa: std.mem.Allocator, arena: *Arena, program: u32, perms: IOPerms) !u32 {
|
||||
const action_tree = try isIOSentinel(arena, program) orelse {
|
||||
return error.InvalidIOSentinel;
|
||||
};
|
||||
|
||||
var loop: c.uv_loop_t = undefined;
|
||||
const rc = c.uv_loop_init(&loop);
|
||||
if (rc != 0) return error.LoopInitFailed;
|
||||
defer _ = c.uv_loop_close(&loop);
|
||||
|
||||
var scheduler = try Scheduler.init(gpa, &loop, arena, perms);
|
||||
defer scheduler.deinit();
|
||||
|
||||
const main_task = try scheduler.createTask(null, try arena.alloc(.leaf), try arena.alloc(.leaf), action_tree);
|
||||
try scheduler.runnable.append(gpa, main_task);
|
||||
|
||||
try scheduler.run();
|
||||
|
||||
// Return the main task's result
|
||||
return main_task.result orelse program;
|
||||
}
|
||||
@@ -5,24 +5,15 @@ 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 runNative(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []const []const u8, 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, arg);
|
||||
current = try arena.alloc(.{ .app = .{ .func = current, .arg = arg_tree } });
|
||||
}
|
||||
|
||||
const result = try reduce.reduce(current, arena, 1_000_000_000);
|
||||
|
||||
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, result) orelse {
|
||||
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;
|
||||
@@ -32,11 +23,11 @@ fn runNative(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []cons
|
||||
try stdout.interface.writeAll("\n");
|
||||
},
|
||||
codecs.HOST_NUMBER_TAG => {
|
||||
const n = try codecs.toNumber(arena, result) orelse 0;
|
||||
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, result) orelse {
|
||||
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;
|
||||
@@ -44,19 +35,54 @@ fn runNative(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []cons
|
||||
try stdout.interface.writeAll(if (b) "true\n" else "false\n");
|
||||
},
|
||||
codecs.HOST_TREE_TAG => {
|
||||
try tree.formatTree(&stdout.interface, arena, result, 0);
|
||||
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, result, 0);
|
||||
try tree.formatTree(&stdout.interface, arena, node, 0);
|
||||
try stdout.interface.writeAll(")\n");
|
||||
},
|
||||
}
|
||||
try stdout.flush();
|
||||
}
|
||||
|
||||
fn runBundle(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []const []const u8, 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);
|
||||
|
||||
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 result = try reduce.reduce(current, arena, fuel);
|
||||
try printNode(arena, tag, result, io);
|
||||
}
|
||||
|
||||
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 {
|
||||
const kernel_root = try kernel.loadKernel(arena);
|
||||
|
||||
const tag_tree = try codecs.ofNumber(arena, tag);
|
||||
@@ -65,7 +91,7 @@ fn runBundle(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []cons
|
||||
var arg_items = try arena.allocator.alloc(u32, args_raw.len);
|
||||
defer arena.allocator.free(arg_items);
|
||||
for (args_raw, 0..) |arg, i| {
|
||||
arg_items[i] = try parseArg(arena, arg);
|
||||
arg_items[i] = try parseArg(arena, io, arg);
|
||||
}
|
||||
const args_tree = try codecs.ofList(arena, arg_items);
|
||||
|
||||
@@ -74,7 +100,7 @@ fn runBundle(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []cons
|
||||
const app1 = try arena.alloc(.{ .app = .{ .func = app0, .arg = bundle_tree } });
|
||||
const app2 = try arena.alloc(.{ .app = .{ .func = app1, .arg = args_tree } });
|
||||
|
||||
const result = try reduce.reduce(app2, arena, 1_000_000_000);
|
||||
const result = try reduce.reduce(app2, arena, fuel);
|
||||
|
||||
const unwrapped = try codecs.unwrapResult(arena, result) orelse {
|
||||
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
|
||||
@@ -98,46 +124,16 @@ 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, s: []const u8) !u32 {
|
||||
fn parseArg(arena: *Arena, io: std.Io, s: []const u8) !u32 {
|
||||
if (std.mem.endsWith(u8, s, ".arboricx")) {
|
||||
const bundle_bytes = try std.Io.Dir.cwd().readFileAlloc(io, s, arena.allocator, .limited(10 * 1024 * 1024));
|
||||
defer arena.allocator.free(bundle_bytes);
|
||||
return try bundle.loadBundleDefaultRoot(arena, bundle_bytes);
|
||||
}
|
||||
|
||||
if (std.fmt.parseInt(u64, s, 10)) |n| {
|
||||
return try codecs.ofNumber(arena, n);
|
||||
} else |_| {}
|
||||
@@ -156,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] <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();
|
||||
std.process.exit(1);
|
||||
}
|
||||
@@ -167,13 +163,16 @@ 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;
|
||||
while (i < args.len) : (i += 1) {
|
||||
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 <tree|number|bool|string|list|bytes> <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();
|
||||
std.process.exit(1);
|
||||
}
|
||||
@@ -194,6 +193,26 @@ 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 <N> [--io] [--unsafe-io] <bundle> [args...]\n");
|
||||
try stderr.flush();
|
||||
std.process.exit(1);
|
||||
}
|
||||
const n = std.fmt.parseInt(u64, args[i + 1], 10) catch {
|
||||
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
|
||||
try stderr.interface.print("Invalid fuel: {s}\n", .{args[i + 1]});
|
||||
try stderr.flush();
|
||||
std.process.exit(1);
|
||||
};
|
||||
fuel = std.math.mul(u64, n, 1_000_000) catch std.math.maxInt(u64);
|
||||
i += 1;
|
||||
} else {
|
||||
bundle_idx = i;
|
||||
arg_start = i + 1;
|
||||
@@ -203,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] <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();
|
||||
std.process.exit(1);
|
||||
}
|
||||
@@ -217,15 +236,22 @@ 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) {
|
||||
runBundle(&arena, tag, bundle_bytes, call_args, io) catch |err| {
|
||||
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)});
|
||||
try stderr.flush();
|
||||
std.process.exit(1);
|
||||
};
|
||||
} else {
|
||||
runNative(&arena, tag, bundle_bytes, call_args, io) catch |err| {
|
||||
runNative(&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)});
|
||||
try stderr.flush();
|
||||
|
||||
@@ -15,21 +15,21 @@ pub fn reduce(root: u32, arena: *Arena, fuel: u64) ReduceError!u32 {
|
||||
}
|
||||
|
||||
fn whnf(term: u32, arena: *Arena, fuel: *u64) ReduceError!u32 {
|
||||
if (fuel.* == 0) return error.FuelExhausted;
|
||||
var current = term;
|
||||
|
||||
while (true) {
|
||||
switch (arena.get(current).*) {
|
||||
.leaf, .stem, .fork => return current,
|
||||
.app => |app| {
|
||||
if (fuel.* == 0) return error.FuelExhausted;
|
||||
fuel.* -= 1;
|
||||
|
||||
const orig = current;
|
||||
const func_idx = app.func;
|
||||
const arg_idx = app.arg;
|
||||
|
||||
// Reduce function to WHNF
|
||||
const f = try whnf(func_idx, arena, fuel);
|
||||
if (fuel.* == 0) return error.FuelExhausted;
|
||||
fuel.* -= 1;
|
||||
|
||||
switch (arena.get(f).*) {
|
||||
// apply Leaf b = Stem b
|
||||
@@ -49,15 +49,11 @@ fn whnf(term: u32, arena: *Arena, fuel: *u64) ReduceError!u32 {
|
||||
|
||||
// Reduce left child of Fork
|
||||
const left = try whnf(left_idx, arena, fuel);
|
||||
if (fuel.* == 0) return error.FuelExhausted;
|
||||
fuel.* -= 1;
|
||||
|
||||
switch (arena.get(left).*) {
|
||||
// apply (Fork Leaf a) _ = a
|
||||
.leaf => {
|
||||
const result = try whnf(right_idx, arena, fuel);
|
||||
if (fuel.* == 0) return error.FuelExhausted;
|
||||
fuel.* -= 1;
|
||||
if (orig != result) {
|
||||
arena.get(orig).* = arena.get(result).*;
|
||||
}
|
||||
@@ -70,23 +66,17 @@ fn whnf(term: u32, arena: *Arena, fuel: *u64) ReduceError!u32 {
|
||||
const inner2 = try arena.alloc(.{ .app = .{ .func = right_idx, .arg = arg_idx } });
|
||||
arena.get(orig).* = .{ .app = .{ .func = inner1, .arg = inner2 } };
|
||||
current = orig;
|
||||
if (fuel.* == 0) return error.FuelExhausted;
|
||||
fuel.* -= 1;
|
||||
continue;
|
||||
},
|
||||
.fork => {
|
||||
// Reduce argument
|
||||
const arg = try whnf(arg_idx, arena, fuel);
|
||||
if (fuel.* == 0) return error.FuelExhausted;
|
||||
fuel.* -= 1;
|
||||
|
||||
switch (arena.get(arg).*) {
|
||||
// apply (Fork (Fork a b) c) Leaf = a
|
||||
.leaf => {
|
||||
const a_idx = arena.get(left).fork.left;
|
||||
const result = try whnf(a_idx, arena, fuel);
|
||||
if (fuel.* == 0) return error.FuelExhausted;
|
||||
fuel.* -= 1;
|
||||
if (orig != result) {
|
||||
arena.get(orig).* = arena.get(result).*;
|
||||
}
|
||||
@@ -98,8 +88,6 @@ fn whnf(term: u32, arena: *Arena, fuel: *u64) ReduceError!u32 {
|
||||
const u = s.child;
|
||||
arena.get(orig).* = .{ .app = .{ .func = b_idx, .arg = u } };
|
||||
current = orig;
|
||||
if (fuel.* == 0) return error.FuelExhausted;
|
||||
fuel.* -= 1;
|
||||
continue;
|
||||
},
|
||||
// apply (Fork (Fork a b) c) (Fork u v) = (c u) v
|
||||
@@ -110,8 +98,6 @@ fn whnf(term: u32, arena: *Arena, fuel: *u64) ReduceError!u32 {
|
||||
const inner = try arena.alloc(.{ .app = .{ .func = c_idx, .arg = u } });
|
||||
arena.get(orig).* = .{ .app = .{ .func = inner, .arg = v } };
|
||||
current = orig;
|
||||
if (fuel.* == 0) return error.FuelExhausted;
|
||||
fuel.* -= 1;
|
||||
continue;
|
||||
},
|
||||
.app => return error.InvalidApply,
|
||||
|
||||
@@ -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;
|
||||
|
||||
223
ext/zig/tests/io_protocol_test.c
Normal file
223
ext/zig/tests/io_protocol_test.c
Normal file
@@ -0,0 +1,223 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "arboricx.h"
|
||||
|
||||
int main(void) {
|
||||
arb_ctx_t* ctx = arboricx_init();
|
||||
if (!ctx) {
|
||||
fprintf(stderr, "Failed to initialize Arboricx context\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Test: construct and verify pure action = Fork 0 Leaf */
|
||||
uint32_t leaf = arb_leaf(ctx);
|
||||
uint32_t zero = arb_of_number(ctx, 0);
|
||||
uint32_t pure_action = arb_fork(ctx, zero, leaf);
|
||||
|
||||
if (!arb_is_fork(ctx, pure_action)) {
|
||||
fprintf(stderr, "FAIL: pure action should be fork\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t tag, payload;
|
||||
if (!arb_get_fork_children(ctx, pure_action, &tag, &payload) ||
|
||||
tag != zero || payload != leaf) {
|
||||
fprintf(stderr, "FAIL: pure action children mismatch\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint64_t tag_num;
|
||||
if (!arb_to_number(ctx, tag, &tag_num) || tag_num != 0) {
|
||||
fprintf(stderr, "FAIL: pure action tag should be 0\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
printf("PASS: pure action shape\n");
|
||||
|
||||
/* Test: construct and verify bind action = Fork 1 (Fork left k) */
|
||||
uint32_t one = arb_of_number(ctx, 1);
|
||||
uint32_t left = arb_fork(ctx, zero, leaf); /* pure Leaf */
|
||||
uint32_t k = arb_fork(ctx, leaf, leaf); /* identity as Fork Leaf Leaf */
|
||||
uint32_t bind_pair = arb_fork(ctx, left, k);
|
||||
uint32_t bind_action = arb_fork(ctx, one, bind_pair);
|
||||
|
||||
if (!arb_get_fork_children(ctx, bind_action, &tag, &payload) ||
|
||||
!arb_to_number(ctx, tag, &tag_num) || tag_num != 1) {
|
||||
fprintf(stderr, "FAIL: bind action tag should be 1\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t bind_left, bind_k;
|
||||
if (!arb_get_fork_children(ctx, payload, &bind_left, &bind_k) ||
|
||||
bind_left != left || bind_k != k) {
|
||||
fprintf(stderr, "FAIL: bind payload should be Fork left k\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
printf("PASS: bind action shape\n");
|
||||
|
||||
/* Test: construct and verify IO sentinel = Fork "tricuIO" (Fork 1 action) */
|
||||
uint32_t sentinel_str = arb_of_string(ctx, "tricuIO");
|
||||
uint32_t version = arb_of_number(ctx, 1);
|
||||
uint32_t version_action_pair = arb_fork(ctx, version, pure_action);
|
||||
uint32_t io_sentinel = arb_fork(ctx, sentinel_str, version_action_pair);
|
||||
|
||||
if (!arb_is_fork(ctx, io_sentinel)) {
|
||||
fprintf(stderr, "FAIL: IO sentinel should be fork\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t sent_left, sent_right;
|
||||
if (!arb_get_fork_children(ctx, io_sentinel, &sent_left, &sent_right)) {
|
||||
fprintf(stderr, "FAIL: get_fork_children on IO sentinel\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Verify sentinel string */
|
||||
uint8_t* decoded_sentinel;
|
||||
size_t decoded_len;
|
||||
if (!arb_to_string(ctx, sent_left, &decoded_sentinel, &decoded_len) ||
|
||||
decoded_len != 7 || memcmp(decoded_sentinel, "tricuIO", 7) != 0) {
|
||||
fprintf(stderr, "FAIL: IO sentinel string mismatch\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
arboricx_free_buf(ctx, decoded_sentinel, decoded_len);
|
||||
|
||||
/* Verify version = 1 and action = pure */
|
||||
uint32_t ver, act;
|
||||
if (!arb_get_fork_children(ctx, sent_right, &ver, &act) ||
|
||||
!arb_to_number(ctx, ver, &tag_num) || tag_num != 1 ||
|
||||
act != pure_action) {
|
||||
fprintf(stderr, "FAIL: IO sentinel version/action mismatch\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
printf("PASS: IO sentinel shape\n");
|
||||
|
||||
/* Test: putStr action = Fork 10 string */
|
||||
uint32_t ten = arb_of_number(ctx, 10);
|
||||
uint32_t msg = arb_of_string(ctx, "hello");
|
||||
uint32_t putStr_action = arb_fork(ctx, ten, msg);
|
||||
|
||||
if (!arb_get_fork_children(ctx, putStr_action, &tag, &payload) ||
|
||||
!arb_to_number(ctx, tag, &tag_num) || tag_num != 10) {
|
||||
fprintf(stderr, "FAIL: putStr tag should be 10\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
printf("PASS: putStr action shape\n");
|
||||
|
||||
/* Test: getLine action = Fork 11 Leaf */
|
||||
uint32_t eleven = arb_of_number(ctx, 11);
|
||||
uint32_t getLine_action = arb_fork(ctx, eleven, leaf);
|
||||
|
||||
if (!arb_get_fork_children(ctx, getLine_action, &tag, &payload) ||
|
||||
!arb_to_number(ctx, tag, &tag_num) || tag_num != 11 ||
|
||||
payload != leaf) {
|
||||
fprintf(stderr, "FAIL: getLine tag should be 11 with Leaf payload\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
printf("PASS: getLine action shape\n");
|
||||
|
||||
/* Test: readFile action = Fork 20 path */
|
||||
uint32_t twenty = arb_of_number(ctx, 20);
|
||||
uint32_t path = arb_of_string(ctx, "/tmp/test.txt");
|
||||
uint32_t readFile_action = arb_fork(ctx, twenty, path);
|
||||
|
||||
if (!arb_get_fork_children(ctx, readFile_action, &tag, &payload) ||
|
||||
!arb_to_number(ctx, tag, &tag_num) || tag_num != 20) {
|
||||
fprintf(stderr, "FAIL: readFile tag should be 20\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
printf("PASS: readFile action shape\n");
|
||||
|
||||
/* Test: writeFile action = Fork 21 (Fork path contents) */
|
||||
uint32_t twenty_one = arb_of_number(ctx, 21);
|
||||
uint32_t contents = arb_of_string(ctx, "data");
|
||||
uint32_t write_pair = arb_fork(ctx, path, contents);
|
||||
uint32_t writeFile_action = arb_fork(ctx, twenty_one, write_pair);
|
||||
|
||||
if (!arb_get_fork_children(ctx, writeFile_action, &tag, &payload) ||
|
||||
!arb_to_number(ctx, tag, &tag_num) || tag_num != 21) {
|
||||
fprintf(stderr, "FAIL: writeFile tag should be 21\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t wf_path, wf_contents;
|
||||
if (!arb_get_fork_children(ctx, payload, &wf_path, &wf_contents) ||
|
||||
wf_path != path || wf_contents != contents) {
|
||||
fprintf(stderr, "FAIL: writeFile payload should be Fork path contents\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
printf("PASS: writeFile action shape\n");
|
||||
|
||||
/* Test: ok result = Fork (Stem Leaf) (Fork val Leaf) */
|
||||
uint32_t stem_leaf = arb_stem(ctx, leaf);
|
||||
uint32_t val_pair = arb_fork(ctx, msg, leaf);
|
||||
uint32_t ok_result = arb_fork(ctx, stem_leaf, val_pair);
|
||||
|
||||
if (!arb_is_fork(ctx, ok_result)) {
|
||||
fprintf(stderr, "FAIL: ok result should be fork\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t ok_tag, ok_rest;
|
||||
if (!arb_get_fork_children(ctx, ok_result, &ok_tag, &ok_rest) ||
|
||||
!arb_is_stem(ctx, ok_tag)) {
|
||||
fprintf(stderr, "FAIL: ok result left should be stem\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t ok_val, ok_leaf;
|
||||
if (!arb_get_fork_children(ctx, ok_rest, &ok_val, &ok_leaf) ||
|
||||
ok_val != msg || ok_leaf != leaf) {
|
||||
fprintf(stderr, "FAIL: ok result right should be Fork val Leaf\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
printf("PASS: ok result shape\n");
|
||||
|
||||
/* Test: err result = Fork Leaf (Fork code Leaf) */
|
||||
uint32_t err_code = arb_of_number(ctx, 42);
|
||||
uint32_t err_pair = arb_fork(ctx, err_code, leaf);
|
||||
uint32_t err_result = arb_fork(ctx, leaf, err_pair);
|
||||
|
||||
if (!arb_is_fork(ctx, err_result)) {
|
||||
fprintf(stderr, "FAIL: err result should be fork\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t err_tag, err_rest;
|
||||
if (!arb_get_fork_children(ctx, err_result, &err_tag, &err_rest) ||
|
||||
!arb_is_leaf(ctx, err_tag)) {
|
||||
fprintf(stderr, "FAIL: err result left should be leaf\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t err_c, err_l;
|
||||
if (!arb_get_fork_children(ctx, err_rest, &err_c, &err_l) ||
|
||||
err_c != err_code || err_l != leaf) {
|
||||
fprintf(stderr, "FAIL: err result right should be Fork code Leaf\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
printf("PASS: err result shape\n");
|
||||
|
||||
arboricx_free(ctx);
|
||||
printf("\nAll IO protocol tests passed.\n");
|
||||
return 0;
|
||||
}
|
||||
217
ext/zig/tests/io_run_test.c
Normal file
217
ext/zig/tests/io_run_test.c
Normal file
@@ -0,0 +1,217 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "arboricx.h"
|
||||
|
||||
static uint32_t make_pure(arb_ctx_t* ctx, uint32_t val) {
|
||||
uint32_t zero = arb_of_number(ctx, 0);
|
||||
return arb_fork(ctx, zero, val);
|
||||
}
|
||||
|
||||
static uint32_t make_io_sentinel(arb_ctx_t* ctx, uint32_t action) {
|
||||
uint32_t sentinel = arb_of_string(ctx, "tricuIO");
|
||||
uint32_t version = arb_of_number(ctx, 1);
|
||||
uint32_t version_action = arb_fork(ctx, version, action);
|
||||
return arb_fork(ctx, sentinel, version_action);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
arb_ctx_t* ctx = arboricx_init();
|
||||
if (!ctx) {
|
||||
fprintf(stderr, "Failed to initialize Arboricx context\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
arb_io_perms_t perms = { 0, 0 };
|
||||
|
||||
/* Test 1: pure "hello" wrapped in IO sentinel */
|
||||
{
|
||||
uint32_t hello = arb_of_string(ctx, "hello");
|
||||
uint32_t pure_hello = make_pure(ctx, hello);
|
||||
uint32_t program = make_io_sentinel(ctx, pure_hello);
|
||||
|
||||
uint32_t result = arb_run_io(ctx, program, &perms);
|
||||
if (result == 0) {
|
||||
fprintf(stderr, "FAIL: pure hello returned 0\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint8_t* decoded;
|
||||
size_t decoded_len;
|
||||
if (!arb_to_string(ctx, result, &decoded, &decoded_len) ||
|
||||
decoded_len != 5 || memcmp(decoded, "hello", 5) != 0) {
|
||||
fprintf(stderr, "FAIL: pure hello result mismatch\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
arboricx_free_buf(ctx, decoded, decoded_len);
|
||||
printf("PASS: pure hello\n");
|
||||
}
|
||||
|
||||
/* Test 2: bind (pure "a") (\_ : pure "done") */
|
||||
{
|
||||
uint32_t a = arb_of_string(ctx, "a");
|
||||
uint32_t done = arb_of_string(ctx, "done");
|
||||
uint32_t pure_a = make_pure(ctx, a);
|
||||
uint32_t pure_done = make_pure(ctx, done);
|
||||
|
||||
/* K pure_done = Fork Leaf pure_done */
|
||||
uint32_t k = arb_fork(ctx, arb_leaf(ctx), pure_done);
|
||||
uint32_t bind_pair = arb_fork(ctx, pure_a, k);
|
||||
uint32_t one = arb_of_number(ctx, 1);
|
||||
uint32_t bind_action = arb_fork(ctx, one, bind_pair);
|
||||
uint32_t program = make_io_sentinel(ctx, bind_action);
|
||||
|
||||
uint32_t result = arb_run_io(ctx, program, &perms);
|
||||
if (result == 0) {
|
||||
fprintf(stderr, "FAIL: bind returned 0\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint8_t* decoded;
|
||||
size_t decoded_len;
|
||||
if (!arb_to_string(ctx, result, &decoded, &decoded_len) ||
|
||||
decoded_len != 4 || memcmp(decoded, "done", 4) != 0) {
|
||||
fprintf(stderr, "FAIL: bind result mismatch\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
arboricx_free_buf(ctx, decoded, decoded_len);
|
||||
printf("PASS: bind pure\n");
|
||||
}
|
||||
|
||||
/* Test 3: putStr "test" (no permissions needed) */
|
||||
{
|
||||
uint32_t test = arb_of_string(ctx, "test");
|
||||
uint32_t ten = arb_of_number(ctx, 10);
|
||||
uint32_t putStr_action = arb_fork(ctx, ten, test);
|
||||
uint32_t program = make_io_sentinel(ctx, putStr_action);
|
||||
|
||||
printf("EXPECT: test\n");
|
||||
uint32_t result = arb_run_io(ctx, program, &perms);
|
||||
if (result == 0) {
|
||||
fprintf(stderr, "FAIL: putStr returned 0\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
if (!arb_is_leaf(ctx, result)) {
|
||||
fprintf(stderr, "FAIL: putStr should return Leaf\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
printf("PASS: putStr\n");
|
||||
}
|
||||
|
||||
/* Test 4: readFile without permission returns err */
|
||||
{
|
||||
uint32_t path = arb_of_string(ctx, "/etc/passwd");
|
||||
uint32_t twenty = arb_of_number(ctx, 20);
|
||||
uint32_t readFile_action = arb_fork(ctx, twenty, path);
|
||||
uint32_t program = make_io_sentinel(ctx, readFile_action);
|
||||
|
||||
uint32_t result = arb_run_io(ctx, program, &perms);
|
||||
if (result == 0) {
|
||||
fprintf(stderr, "FAIL: readFile denied returned 0\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Should be an err result: Fork Leaf (Fork code Leaf) */
|
||||
uint32_t left, right;
|
||||
if (!arb_get_fork_children(ctx, result, &left, &right) ||
|
||||
!arb_is_leaf(ctx, left)) {
|
||||
fprintf(stderr, "FAIL: readFile denied should be err result\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t code, rest;
|
||||
if (!arb_get_fork_children(ctx, right, &code, &rest) ||
|
||||
!arb_is_leaf(ctx, rest)) {
|
||||
fprintf(stderr, "FAIL: readFile denied err shape mismatch\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint64_t code_num;
|
||||
if (!arb_to_number(ctx, code, &code_num) || code_num != 20) {
|
||||
fprintf(stderr, "FAIL: readFile denied code should be 20, got %llu\n",
|
||||
(unsigned long long)code_num);
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
printf("PASS: readFile denied\n");
|
||||
}
|
||||
|
||||
/* Test 5: readFile with permission succeeds */
|
||||
{
|
||||
/* Create a temp file first */
|
||||
const char* tmp = "/tmp/tricu_io_test.txt";
|
||||
FILE* f = fopen(tmp, "w");
|
||||
if (!f) {
|
||||
fprintf(stderr, "FAIL: could not create temp file\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
fprintf(f, "hi");
|
||||
fclose(f);
|
||||
|
||||
arb_io_perms_t unsafe_perms = { 1, 0 };
|
||||
uint32_t path = arb_of_string(ctx, tmp);
|
||||
uint32_t twenty = arb_of_number(ctx, 20);
|
||||
uint32_t readFile_action = arb_fork(ctx, twenty, path);
|
||||
uint32_t program = make_io_sentinel(ctx, readFile_action);
|
||||
|
||||
uint32_t result = arb_run_io(ctx, program, &unsafe_perms);
|
||||
if (result == 0) {
|
||||
fprintf(stderr, "FAIL: readFile allowed returned 0\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Should be ok result: Fork (Stem Leaf) (Fork val Leaf) */
|
||||
uint32_t ok_tag, ok_rest;
|
||||
if (!arb_get_fork_children(ctx, result, &ok_tag, &ok_rest) ||
|
||||
!arb_is_stem(ctx, ok_tag)) {
|
||||
fprintf(stderr, "FAIL: readFile allowed should be ok result\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t val, leaf;
|
||||
if (!arb_get_fork_children(ctx, ok_rest, &val, &leaf) ||
|
||||
!arb_is_leaf(ctx, leaf)) {
|
||||
fprintf(stderr, "FAIL: readFile allowed ok shape mismatch\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint8_t* decoded;
|
||||
size_t decoded_len;
|
||||
if (!arb_to_string(ctx, val, &decoded, &decoded_len) ||
|
||||
decoded_len != 2 || memcmp(decoded, "hi", 2) != 0) {
|
||||
fprintf(stderr, "FAIL: readFile allowed contents mismatch\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
arboricx_free_buf(ctx, decoded, decoded_len);
|
||||
printf("PASS: readFile allowed\n");
|
||||
}
|
||||
|
||||
/* Test 6: invalid sentinel returns 0 */
|
||||
{
|
||||
uint32_t bad = arb_fork(ctx, arb_leaf(ctx), arb_leaf(ctx));
|
||||
uint32_t result = arb_run_io(ctx, bad, &perms);
|
||||
if (result != 0) {
|
||||
fprintf(stderr, "FAIL: invalid sentinel should return 0\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
printf("PASS: invalid sentinel\n");
|
||||
}
|
||||
|
||||
arboricx_free(ctx);
|
||||
printf("\nAll IO run tests passed.\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -27,7 +27,7 @@ int main() {
|
||||
printf("bundle size=%zu\n", bundle_len);
|
||||
|
||||
clock_t t0 = clock();
|
||||
uint32_t term = arb_load_bundle(ctx, bundle, bundle_len, "root");
|
||||
uint32_t term = arb_load_bundle(ctx, bundle, bundle_len, "append");
|
||||
clock_t t1 = clock();
|
||||
printf("load_bundle took %.3f ms, term=%u\n", (double)(t1 - t0) * 1000.0 / CLOCKS_PER_SEC, term);
|
||||
if (term == 0) {
|
||||
|
||||
@@ -16,12 +16,12 @@ static uint8_t *read_file(const char *path, size_t *out_len) {
|
||||
return buf;
|
||||
}
|
||||
|
||||
int test_bundle(arb_ctx_t *ctx, const char *path, int expect_val) {
|
||||
int test_bundle(arb_ctx_t *ctx, const char *path, const char *name, int expect_val) {
|
||||
size_t bundle_len;
|
||||
uint8_t *bundle = read_file(path, &bundle_len);
|
||||
if (!bundle) { printf("bundle not found: %s\n", path); return 1; }
|
||||
|
||||
uint32_t term = arb_load_bundle(ctx, bundle, bundle_len, "root");
|
||||
uint32_t term = arb_load_bundle(ctx, bundle, bundle_len, name);
|
||||
if (term == 0) {
|
||||
printf("load_bundle failed for %s\n", path);
|
||||
free(bundle);
|
||||
@@ -51,8 +51,8 @@ int main() {
|
||||
arb_ctx_t *ctx = arboricx_init();
|
||||
if (!ctx) { printf("init failed\n"); return 1; }
|
||||
|
||||
if (test_bundle(ctx, "../../test/fixtures/true.arboricx", 1) != 0) return 1;
|
||||
if (test_bundle(ctx, "../../test/fixtures/false.arboricx", 0) != 0) return 1;
|
||||
if (test_bundle(ctx, "../../test/fixtures/true.arboricx", "true", 1) != 0) return 1;
|
||||
if (test_bundle(ctx, "../../test/fixtures/false.arboricx", "false", 0) != 0) return 1;
|
||||
|
||||
arboricx_free(ctx);
|
||||
printf("All bool tests passed.\n");
|
||||
|
||||
@@ -26,7 +26,7 @@ int main() {
|
||||
printf("bundle size=%zu\n", bundle_len);
|
||||
|
||||
clock_t t0 = clock();
|
||||
uint32_t term = arb_load_bundle(ctx, bundle, bundle_len, "root");
|
||||
uint32_t term = arb_load_bundle(ctx, bundle, bundle_len, "id");
|
||||
clock_t t1 = clock();
|
||||
printf("load_bundle took %.3f ms, term=%u\n", (double)(t1 - t0) * 1000.0 / CLOCKS_PER_SEC, term);
|
||||
if (term == 0) {
|
||||
|
||||
@@ -217,7 +217,7 @@ print(f" time: {(t1 - t0) * 1000:.1f} ms")
|
||||
# Test 5: append via native named export
|
||||
print("\n--- Test 5: append via named export 'root' ---")
|
||||
t0 = time.time()
|
||||
result = native_run_named(bundle, "root", ["Hello, ", "world!"])
|
||||
result = native_run_named(bundle, "append", ["Hello, ", "world!"])
|
||||
t1 = time.time()
|
||||
check("append named", result, "Hello, world!")
|
||||
print(f" time: {(t1 - t0) * 1000:.1f} ms")
|
||||
|
||||
135
flake.nix
135
flake.nix
@@ -16,7 +16,29 @@
|
||||
haskellPackages = pkgs.haskellPackages;
|
||||
hsLib = pkgs.haskell.lib;
|
||||
|
||||
tricuStatic = hsLib.justStaticExecutables self.packages.${system}.default;
|
||||
staticPkgs = pkgs.pkgsStatic;
|
||||
staticHaskellPackages = staticPkgs.haskellPackages;
|
||||
staticHsLib = staticPkgs.haskell.lib;
|
||||
|
||||
tricuMuslStatic =
|
||||
staticHsLib.justStaticExecutables (
|
||||
staticHsLib.dontCheck (
|
||||
staticHaskellPackages.callCabal2nix packageName self {}
|
||||
)
|
||||
);
|
||||
|
||||
tricuStatic = pkgs.runCommand "${packageName}-static-upx" {
|
||||
nativeBuildInputs = [ pkgs.upx ];
|
||||
} ''
|
||||
mkdir -p $out/bin
|
||||
cp ${tricuMuslStatic}/bin/tricu $out/bin/tricu
|
||||
chmod +w $out/bin/tricu
|
||||
|
||||
# Good compression, slower build.
|
||||
upx --best --lzma $out/bin/tricu
|
||||
|
||||
chmod 755 $out/bin/tricu
|
||||
'';
|
||||
|
||||
tricuPackageTests =
|
||||
haskellPackages.callCabal2nix packageName self {};
|
||||
@@ -26,6 +48,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 +71,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 +90,7 @@
|
||||
version = "0.1.0";
|
||||
src = ./.;
|
||||
nativeBuildInputs = [ pkgs.gcc pkgs.python3 tricuZig ];
|
||||
buildInputs = [ pkgs.libuv ];
|
||||
buildPhase = "true";
|
||||
doCheck = true;
|
||||
checkPhase = ''
|
||||
@@ -69,6 +105,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 \
|
||||
@@ -111,7 +159,7 @@
|
||||
buildPhase = "true";
|
||||
installPhase = ''
|
||||
mkdir -p $out/share/tricu-php $out/lib $out/bin
|
||||
cp -r src run.php $out/share/tricu-php/
|
||||
cp -r src public run.php $out/share/tricu-php/
|
||||
cp ${tricuZig}/lib/libarboricx.so $out/lib/
|
||||
cp ${tricuZig}/include/arboricx.h $out/share/tricu-php/
|
||||
|
||||
@@ -122,6 +170,48 @@
|
||||
'';
|
||||
};
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# JS FFI host
|
||||
# ------------------------------------------------------------------
|
||||
tricuJs = pkgs.buildNpmPackage {
|
||||
pname = "tricu-js";
|
||||
version = "0.1.0";
|
||||
src = ./ext/js;
|
||||
npmDepsHash = "sha256-81C7tsNcbyZVhm3uqiWdDQxp5LAXXO9aueHdMDztCfM=";
|
||||
nativeBuildInputs = [ pkgs.nodejs tricuZig ];
|
||||
dontNpmBuild = true;
|
||||
installPhase = ''
|
||||
mkdir -p $out/lib/
|
||||
cp -r . $out/lib/
|
||||
cp ${tricuZig}/lib/libarboricx.so $out/lib/src
|
||||
'';
|
||||
};
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# JS FFI host tests (separate target)
|
||||
# ------------------------------------------------------------------
|
||||
tricuJsTests = pkgs.stdenv.mkDerivation {
|
||||
pname = "tricu-js-tests";
|
||||
version = "0.1.0";
|
||||
src = ./.;
|
||||
nativeBuildInputs = [ pkgs.nodejs tricuZig ];
|
||||
buildPhase = "true";
|
||||
doCheck = true;
|
||||
checkPhase = ''
|
||||
export ARBORICX_LIB=${tricuZig}/lib/libarboricx.so
|
||||
export LD_LIBRARY_PATH=${tricuZig}/lib:$LD_LIBRARY_PATH
|
||||
ulimit -s 32768
|
||||
|
||||
cd ext/js
|
||||
# node_modules are pre-fetched by buildNpmPackage; copy them in
|
||||
cp -r ${tricuJs}/lib/tricu-js/node_modules .
|
||||
npm test
|
||||
|
||||
mkdir -p $out
|
||||
echo "All JS tests passed" > $out/result
|
||||
'';
|
||||
};
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# PHP FFI tests (separate target)
|
||||
# ------------------------------------------------------------------
|
||||
@@ -153,10 +243,15 @@
|
||||
in {
|
||||
packages.${packageName} = tricuPackage;
|
||||
packages.default = tricuPackage;
|
||||
packages.tricu-static = tricuMuslStatic;
|
||||
packages.tricu-static-upx = tricuStatic;
|
||||
packages.tricu-bench = tricuBench;
|
||||
packages.tricu-zig = tricuZig;
|
||||
packages.tricu-zig-tests = tricuZigTests;
|
||||
packages.tricu-php = tricuPhp;
|
||||
packages.tricu-php-tests = tricuPhpTests;
|
||||
packages.tricu-js = tricuJs;
|
||||
packages.tricu-js-tests = tricuJsTests;
|
||||
|
||||
checks.${packageName} = tricuPackageTests;
|
||||
checks.default = tricuPackageTests;
|
||||
@@ -168,7 +263,6 @@
|
||||
haskellPackages.ghcid
|
||||
customGHC
|
||||
upx
|
||||
zig
|
||||
gcc
|
||||
python3
|
||||
];
|
||||
@@ -182,26 +276,35 @@
|
||||
|
||||
packages.${containerPackageName} = pkgs.dockerTools.buildImage {
|
||||
name = "tricu";
|
||||
tag = "latest";
|
||||
|
||||
copyToRoot = pkgs.buildEnv {
|
||||
name = "image-root";
|
||||
paths = [ tricuStatic ];
|
||||
pathsToLink = [ "/bin" ];
|
||||
};
|
||||
tag = "latest";
|
||||
|
||||
config = {
|
||||
Cmd = [
|
||||
"/bin/tricu"
|
||||
"server"
|
||||
"-h" "0.0.0.0"
|
||||
"-p" "8787"
|
||||
];
|
||||
Cmd = [ "/bin/tricu" ];
|
||||
WorkingDir = "/app";
|
||||
ExposedPorts = {
|
||||
"8787/tcp" = {};
|
||||
};
|
||||
extraCommands = ''
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
packages.arboricxServer = pkgs.dockerTools.buildImage {
|
||||
name = "arboricxServer";
|
||||
tag = "latest";
|
||||
|
||||
copyToRoot = pkgs.runCommand "arboricxServer" {} ''
|
||||
mkdir -p $out/app/bin $out/app/lib $out/app/tricu-apps $out/app/store
|
||||
cp ${tricuStatic}/bin/tricu $out/app/bin/
|
||||
cp -r ${./lib}/* $out/app/lib/
|
||||
cp ${./tricu-apps/arboricxServer.tri} $out/app/tricu-apps/arboricxServer.tri
|
||||
'';
|
||||
|
||||
config = {
|
||||
Entrypoint = [ "/app/bin/tricu" "eval" "tricu-apps/arboricxServer.tri" "--io" "--allow-read" "./store" "--allow-write" "./store" "-f" "decode" ];
|
||||
WorkingDir = "/app";
|
||||
ExposedPorts = { "8080/tcp" = {}; };
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
!import "arboricx.tri" !Local
|
||||
!import "patterns.tri" !Local
|
||||
|
||||
-- Multi-purpose kernel dispatch.
|
||||
--
|
||||
-- runArboricxTyped tag bundleBytes args
|
||||
-- tag 0 → hostTree (runArboricxToTree)
|
||||
-- tag 1 → hostString (runArboricxToString)
|
||||
-- tag 2 → hostNumber (runArboricxToNumber)
|
||||
-- tag 3 → hostBool (runArboricxToBool)
|
||||
-- tag 4 → hostList (runArboricxToList)
|
||||
-- tag 5 → hostBytes (runArboricxToBytes)
|
||||
-- otherwise → err 99 bundleBytes
|
||||
|
||||
runArboricxTyped = (tag bs args :
|
||||
match tag
|
||||
[[(equal? hostTreeTag) (_ : runArboricxToTree bs args)]
|
||||
[(equal? hostStringTag) (_ : runArboricxToString bs args)]
|
||||
[(equal? hostNumberTag) (_ : runArboricxToNumber bs args)]
|
||||
[(equal? hostBoolTag) (_ : runArboricxToBool bs args)]
|
||||
[(equal? hostListTag) (_ : runArboricxToList bs args)]
|
||||
[(equal? hostBytesTag) (_ : runArboricxToBytes bs args)]
|
||||
[otherwise (_ : err 99 bs)]])
|
||||
@@ -1,232 +0,0 @@
|
||||
!import "arboricx-common.tri" !Local
|
||||
|
||||
readNodeRecord = (bs :
|
||||
bindResult (readBytes 32 bs)
|
||||
(nodeHash afterNodeHash :
|
||||
bindResult (readBytes 4 afterNodeHash)
|
||||
(payloadLength afterPayloadLength :
|
||||
bindResult (readBytes (u32BEBytesToNat payloadLength) afterPayloadLength)
|
||||
(payload afterPayload :
|
||||
ok
|
||||
(pair nodeHash
|
||||
(pair payloadLength payload))
|
||||
afterPayload))))
|
||||
|
||||
nodeRecordHash = (nodeRecord :
|
||||
matchPair
|
||||
(nodeHash _ : nodeHash)
|
||||
nodeRecord)
|
||||
|
||||
nodeRecordPayloadLength = (nodeRecord :
|
||||
matchPair
|
||||
(_ payload :
|
||||
matchPair
|
||||
(payloadLength _ : payloadLength)
|
||||
payload)
|
||||
nodeRecord)
|
||||
|
||||
nodeRecordPayload = (nodeRecord :
|
||||
matchPair
|
||||
(_ payload :
|
||||
matchPair
|
||||
(_ nodePayload : nodePayload)
|
||||
payload)
|
||||
nodeRecord)
|
||||
|
||||
nodePayloadKind = (nodePayload : bytesHead nodePayload)
|
||||
|
||||
nodePayloadHasTag? = (tag nodePayload :
|
||||
triage
|
||||
false
|
||||
(actualTag : byteEq? actualTag tag)
|
||||
(_ _ : false)
|
||||
(nodePayloadKind nodePayload))
|
||||
|
||||
nodePayloadLeaf? = (nodePayload : bytesEq? [(0)] nodePayload)
|
||||
|
||||
nodePayloadStem? = (nodePayload :
|
||||
and?
|
||||
(nodePayloadHasTag? nodePayloadStemTag nodePayload)
|
||||
(equal? (bytesLength nodePayload) 33))
|
||||
|
||||
nodePayloadFork? = (nodePayload :
|
||||
and?
|
||||
(nodePayloadHasTag? nodePayloadForkTag nodePayload)
|
||||
(equal? (bytesLength nodePayload) 65))
|
||||
|
||||
nodePayloadValid? = (nodePayload :
|
||||
or?
|
||||
(nodePayloadLeaf? nodePayload)
|
||||
(or?
|
||||
(nodePayloadStem? nodePayload)
|
||||
(nodePayloadFork? nodePayload)))
|
||||
|
||||
nodePayloadStemChildHash = (nodePayload : bytesTake 32 (bytesDrop 1 nodePayload))
|
||||
nodePayloadForkLeftHash = (nodePayload : bytesTake 32 (bytesDrop 1 nodePayload))
|
||||
nodePayloadForkRightHash = (nodePayload : bytesTake 32 (bytesDrop 33 nodePayload))
|
||||
|
||||
nodeRecordPayloadValid? = (nodeRecord : nodePayloadValid? (nodeRecordPayload nodeRecord))
|
||||
|
||||
nodeRecordsHaveInvalidPayload? = y (self nodeRecords :
|
||||
matchList
|
||||
false
|
||||
(nodeRecord rest :
|
||||
or?
|
||||
(not? (nodeRecordPayloadValid? nodeRecord))
|
||||
(self rest))
|
||||
nodeRecords)
|
||||
|
||||
nodeRecordsHaveHash? = y (self nodeRecords nodeHash :
|
||||
matchList
|
||||
false
|
||||
(nodeRecord rest :
|
||||
or?
|
||||
(bytesEq? nodeHash (nodeRecordHash nodeRecord))
|
||||
(self rest nodeHash))
|
||||
nodeRecords)
|
||||
|
||||
nodeRecordsHaveDuplicateHashes? = y (self nodeRecords :
|
||||
matchList
|
||||
false
|
||||
(nodeRecord rest :
|
||||
or?
|
||||
(nodeRecordsHaveHash? rest (nodeRecordHash nodeRecord))
|
||||
(self rest))
|
||||
nodeRecords)
|
||||
|
||||
lookupNodeRecord_ = y (self nodeRecords nodeHash :
|
||||
matchList
|
||||
nothing
|
||||
(nodeRecord rest :
|
||||
matchBool
|
||||
(just nodeRecord)
|
||||
(self rest nodeHash)
|
||||
(bytesEq? nodeHash (nodeRecordHash nodeRecord)))
|
||||
nodeRecords)
|
||||
|
||||
lookupNodeRecord = (nodeHash nodeRecords : lookupNodeRecord_ nodeRecords nodeHash)
|
||||
|
||||
nodeRecordChildHashes = (nodeRecord :
|
||||
(nodePayload :
|
||||
matchBool
|
||||
t
|
||||
(matchBool
|
||||
(pair (nodePayloadStemChildHash nodePayload) t)
|
||||
(pair (nodePayloadForkLeftHash nodePayload)
|
||||
(pair (nodePayloadForkRightHash nodePayload) t))
|
||||
(nodePayloadStem? nodePayload))
|
||||
(nodePayloadLeaf? nodePayload))
|
||||
(nodeRecordPayload nodeRecord))
|
||||
|
||||
nodeHashPresent? = (nodeHash nodeRecords : nodeRecordsHaveHash? nodeRecords nodeHash)
|
||||
|
||||
nodeChildHashesPresent? = y (self childHashes nodeRecords :
|
||||
matchList
|
||||
true
|
||||
(childHash rest :
|
||||
and?
|
||||
(nodeHashPresent? childHash nodeRecords)
|
||||
(self rest nodeRecords))
|
||||
childHashes)
|
||||
|
||||
nodeRecordChildrenPresent? = (nodeRecord nodeRecords :
|
||||
nodeChildHashesPresent? (nodeRecordChildHashes nodeRecord) nodeRecords)
|
||||
|
||||
nodeRecordsClosed? = y (self nodeRecords allNodeRecords :
|
||||
matchList
|
||||
true
|
||||
(nodeRecord rest :
|
||||
and?
|
||||
(nodeRecordChildrenPresent? nodeRecord allNodeRecords)
|
||||
(self rest allNodeRecords))
|
||||
nodeRecords)
|
||||
|
||||
validateNodeRecords = (nodeRecords rest :
|
||||
matchBool
|
||||
(err errInvalidNodePayload rest)
|
||||
(matchBool
|
||||
(err errDuplicateNode rest)
|
||||
(matchBool
|
||||
(ok nodeRecords rest)
|
||||
(err errMissingNode rest)
|
||||
(nodeRecordsClosed? nodeRecords nodeRecords))
|
||||
(nodeRecordsHaveDuplicateHashes? nodeRecords))
|
||||
(nodeRecordsHaveInvalidPayload? nodeRecords))
|
||||
|
||||
readNodeRecords_ = y (self bs nodeCount i acc :
|
||||
matchBool
|
||||
(ok (reverse acc) bs)
|
||||
(bindResult (readNodeRecord bs)
|
||||
(nodeRecord afterNodeRecord :
|
||||
self afterNodeRecord nodeCount (succ i) (pair nodeRecord acc)))
|
||||
(equal? i nodeCount))
|
||||
|
||||
readNodeRecords = (nodeCount bs : readNodeRecords_ bs nodeCount 0 t)
|
||||
|
||||
readNodesSection = (bs :
|
||||
bindResult (readBytes 8 bs)
|
||||
(nodeCount afterNodeCount :
|
||||
bindResult (readNodeRecords (u64BEBytesToNat nodeCount) afterNodeCount)
|
||||
(nodeRecords afterNodeRecords :
|
||||
bindResult (validateNodeRecords nodeRecords afterNodeRecords)
|
||||
(validNodeRecords afterValidNodeRecords :
|
||||
ok (pair nodeCount validNodeRecords) afterValidNodeRecords))))
|
||||
|
||||
readNodesSectionComplete = (bs :
|
||||
bindResult (readNodesSection bs)
|
||||
(nodesSection afterNodesSection :
|
||||
matchBool
|
||||
(ok nodesSection afterNodesSection)
|
||||
(err errUnexpectedBytes afterNodesSection)
|
||||
(bytesNil? afterNodesSection)))
|
||||
|
||||
readArboricxNodesSection = (bs :
|
||||
bindResult (readArboricxContainer bs)
|
||||
(container afterContainer :
|
||||
matchPair
|
||||
(_ directory :
|
||||
bindResult (sectionBytesOrErr arboricxNodesSectionId directory bs afterContainer)
|
||||
(nodesBytes _ :
|
||||
bindResult (readNodesSectionComplete nodesBytes)
|
||||
(nodesSection _ : ok nodesSection afterContainer)))
|
||||
container))
|
||||
|
||||
nodesSectionCount = (nodesSection :
|
||||
matchPair
|
||||
(nodeCount _ : nodeCount)
|
||||
nodesSection)
|
||||
|
||||
nodesSectionRecords = (nodesSection :
|
||||
matchPair
|
||||
(_ nodeRecords : nodeRecords)
|
||||
nodesSection)
|
||||
|
||||
nodeRecordToTreeWith = (self nodeRecords nodeRecord :
|
||||
(nodePayload :
|
||||
matchBool
|
||||
(ok t t)
|
||||
(matchBool
|
||||
(bindResult (self (nodePayloadStemChildHash nodePayload) nodeRecords)
|
||||
(child _ : ok (t child) t))
|
||||
(bindResult (self (nodePayloadForkLeftHash nodePayload) nodeRecords)
|
||||
(left _ :
|
||||
bindResult (self (nodePayloadForkRightHash nodePayload) nodeRecords)
|
||||
(right _ : ok (pair left right) t)))
|
||||
(nodePayloadStem? nodePayload))
|
||||
(nodePayloadLeaf? nodePayload))
|
||||
(nodeRecordPayload nodeRecord))
|
||||
|
||||
nodeHashToTree = y (self nodeHash nodeRecords :
|
||||
triage
|
||||
(err errMissingNode t)
|
||||
(nodeRecord : nodeRecordToTreeWith self nodeRecords nodeRecord)
|
||||
(_ _ : err errMissingNode t)
|
||||
(lookupNodeRecord nodeHash nodeRecords))
|
||||
|
||||
readArboricxTreeFromHash = (rootHash bs :
|
||||
bindResult (readArboricxNodesSection bs)
|
||||
(nodesSection afterContainer :
|
||||
bindResult (nodeHashToTree rootHash (nodesSectionRecords nodesSection))
|
||||
(tree _ : ok tree afterContainer)))
|
||||
|
||||
readArboricxExecutableFromHash = readArboricxTreeFromHash
|
||||
@@ -1,4 +1,7 @@
|
||||
!import "arboricx-manifest.tri" !Local
|
||||
!import "prelude" !Local
|
||||
!import "arboricx.common" !Local
|
||||
!import "arboricx.manifest" !Local
|
||||
!import "arboricx.nodes" !Local
|
||||
|
||||
-- Read and validate a full Arboricx bundle.
|
||||
-- Returns (pair validManifest afterContainer).
|
||||
@@ -26,7 +29,7 @@ readArboricxExecutableByName = (nameBytes bs :
|
||||
(validCore _ :
|
||||
bindResult (selectExport (manifestExports validCore) nameBytes)
|
||||
(selectedExport _ :
|
||||
readArboricxTreeFromHash (exportRoot selectedExport) bs))
|
||||
readArboricxTreeFromIndex (exportRoot selectedExport) bs))
|
||||
bundleResult))
|
||||
|
||||
readArboricxExecutable = (bs :
|
||||
@@ -104,33 +107,52 @@ wrapHostValue = (validator wrapper resultValue rest :
|
||||
(err errHostCodecFailed resultValue)
|
||||
(validator resultValue))
|
||||
|
||||
runArboricxByNameToTree = (nameBytes bs args :
|
||||
wrapHostValueByTag = (tag value rest :
|
||||
matchBool
|
||||
(ok (hostTree value) rest)
|
||||
(matchBool
|
||||
(wrapHostValue hostString? hostString value rest)
|
||||
(matchBool
|
||||
(wrapHostValue hostNumber? hostNumber value rest)
|
||||
(matchBool
|
||||
(wrapHostValue hostBool? hostBool value rest)
|
||||
(matchBool
|
||||
(wrapHostValue hostList? hostList value rest)
|
||||
(matchBool
|
||||
(wrapHostValue hostBytes? hostBytes value rest)
|
||||
(err errHostCodecFailed value)
|
||||
(equal? tag hostBytesTag))
|
||||
(equal? tag hostListTag))
|
||||
(equal? tag hostBoolTag))
|
||||
(equal? tag hostNumberTag))
|
||||
(equal? tag hostStringTag))
|
||||
(equal? tag hostTreeTag))
|
||||
|
||||
runArboricxByNameToTyped = (tag nameBytes bs args :
|
||||
bindResult (runArboricxArgsByName nameBytes bs args)
|
||||
(value rest : ok (hostTree value) rest))
|
||||
(value rest : wrapHostValueByTag tag value rest))
|
||||
|
||||
runArboricxByNameToTree = (nameBytes bs args :
|
||||
runArboricxByNameToTyped hostTreeTag nameBytes bs args)
|
||||
|
||||
runArboricxByNameToString = (nameBytes bs args :
|
||||
bindResult (runArboricxArgsByName nameBytes bs args)
|
||||
(value rest : wrapHostValue hostString? hostString value rest))
|
||||
runArboricxByNameToTyped hostStringTag nameBytes bs args)
|
||||
|
||||
runArboricxByNameToNumber = (nameBytes bs args :
|
||||
bindResult (runArboricxArgsByName nameBytes bs args)
|
||||
(value rest : wrapHostValue hostNumber? hostNumber value rest))
|
||||
runArboricxByNameToTyped hostNumberTag nameBytes bs args)
|
||||
|
||||
runArboricxByNameToBool = (nameBytes bs args :
|
||||
bindResult (runArboricxArgsByName nameBytes bs args)
|
||||
(value rest : wrapHostValue hostBool? hostBool value rest))
|
||||
runArboricxByNameToTyped hostBoolTag nameBytes bs args)
|
||||
|
||||
runArboricxByNameToList = (nameBytes bs args :
|
||||
bindResult (runArboricxArgsByName nameBytes bs args)
|
||||
(value rest : wrapHostValue hostList? hostList value rest))
|
||||
runArboricxByNameToTyped hostListTag nameBytes bs args)
|
||||
|
||||
runArboricxByNameToBytes = (nameBytes bs args :
|
||||
bindResult (runArboricxArgsByName nameBytes bs args)
|
||||
(value rest : wrapHostValue hostBytes? hostBytes value rest))
|
||||
runArboricxByNameToTyped hostBytesTag nameBytes bs args)
|
||||
|
||||
runArboricxToTree = (bs args : runArboricxByNameToTree [] bs args)
|
||||
runArboricxToString = (bs args : runArboricxByNameToString [] bs args)
|
||||
runArboricxToNumber = (bs args : runArboricxByNameToNumber [] bs args)
|
||||
runArboricxToBool = (bs args : runArboricxByNameToBool [] bs args)
|
||||
runArboricxToList = (bs args : runArboricxByNameToList [] bs args)
|
||||
runArboricxToBytes = (bs args : runArboricxByNameToBytes [] bs args)
|
||||
runArboricxToTree = (bs args : runArboricxByNameToTyped hostTreeTag [] bs args)
|
||||
runArboricxToString = (bs args : runArboricxByNameToTyped hostStringTag [] bs args)
|
||||
runArboricxToNumber = (bs args : runArboricxByNameToTyped hostNumberTag [] bs args)
|
||||
runArboricxToBool = (bs args : runArboricxByNameToTyped hostBoolTag [] bs args)
|
||||
runArboricxToList = (bs args : runArboricxByNameToTyped hostListTag [] bs args)
|
||||
runArboricxToBytes = (bs args : runArboricxByNameToTyped hostBytesTag [] bs args)
|
||||
@@ -1,7 +1,6 @@
|
||||
!import "base.tri" !Local
|
||||
!import "list.tri" !Local
|
||||
!import "bytes.tri" !Local
|
||||
!import "binary.tri" !Local
|
||||
!import "prelude" !Local
|
||||
!import "binary" !Local
|
||||
|
||||
|
||||
arboricxMagic = [(65) (82) (66) (79) (82) (73) (67) (88)]
|
||||
arboricxMajorVersion = [(0) (1)]
|
||||
@@ -61,22 +60,22 @@ readSectionRecord = (bs :
|
||||
bindResult (readBytes 2 afterSectionFlags)
|
||||
(compression afterCompression :
|
||||
bindResult (readBytes 2 afterCompression)
|
||||
(digestAlgorithm afterDigestAlgorithm :
|
||||
bindResult (readBytes 8 afterDigestAlgorithm)
|
||||
(reserved1 afterReserved1 :
|
||||
bindResult (readBytes 8 afterReserved1)
|
||||
(offset afterOffset :
|
||||
bindResult (readBytes 8 afterOffset)
|
||||
(length afterLength :
|
||||
bindResult (readBytes 32 afterLength)
|
||||
(digest afterDigest :
|
||||
bindResult (readBytes 4 afterLength)
|
||||
(reserved2 afterReserved2 :
|
||||
ok
|
||||
(pair sectionId
|
||||
(pair sectionVersion
|
||||
(pair sectionFlags
|
||||
(pair compression
|
||||
(pair digestAlgorithm
|
||||
(pair reserved1
|
||||
(pair offset
|
||||
(pair length digest)))))))
|
||||
afterDigest)))))))))
|
||||
(pair length reserved2)))))))
|
||||
afterReserved2)))))))))
|
||||
|
||||
readSectionDirectory_ = y (self bs sectionCount i acc :
|
||||
matchBool
|
||||
@@ -126,7 +125,7 @@ sectionRecordCompression = (sectionRecord :
|
||||
payload)
|
||||
sectionRecord)
|
||||
|
||||
sectionRecordDigestAlgorithm = (sectionRecord :
|
||||
sectionRecordReserved1 = (sectionRecord :
|
||||
matchPair
|
||||
(_ payload :
|
||||
matchPair
|
||||
@@ -136,7 +135,7 @@ sectionRecordDigestAlgorithm = (sectionRecord :
|
||||
matchPair
|
||||
(_ payload4 :
|
||||
matchPair
|
||||
(digestAlgorithm _ : digestAlgorithm)
|
||||
(reserved1 _ : reserved1)
|
||||
payload4)
|
||||
payload3)
|
||||
payload2)
|
||||
@@ -186,7 +185,7 @@ sectionRecordLength = (sectionRecord :
|
||||
payload)
|
||||
sectionRecord)
|
||||
|
||||
sectionRecordDigest = (sectionRecord :
|
||||
sectionRecordReserved2 = (sectionRecord :
|
||||
matchPair
|
||||
(_ payload :
|
||||
matchPair
|
||||
@@ -200,7 +199,7 @@ sectionRecordDigest = (sectionRecord :
|
||||
matchPair
|
||||
(_ payload6 :
|
||||
matchPair
|
||||
(_ digest : digest)
|
||||
(_ reserved2 : reserved2)
|
||||
payload6)
|
||||
payload5)
|
||||
payload4)
|
||||
7
lib/arboricx/dispatch.tri
Normal file
7
lib/arboricx/dispatch.tri
Normal file
@@ -0,0 +1,7 @@
|
||||
!import "prelude" !Local
|
||||
!import "arboricx" !Local
|
||||
|
||||
-- Multi-purpose kernel dispatch.
|
||||
-- runArboricxTyped tag bundleBytes args
|
||||
runArboricxTyped = (tag bs args :
|
||||
runArboricxByNameToTyped tag [] bs args)
|
||||
@@ -1,4 +1,7 @@
|
||||
!import "arboricx-nodes.tri" !Local
|
||||
!import "prelude" !Local
|
||||
!import "binary" !Local
|
||||
!import "arboricx.common" !Local
|
||||
!import "arboricx.nodes" !Local
|
||||
|
||||
readManifestMagic = (bs :
|
||||
expectBytes arboricxManifestMagic bs)
|
||||
@@ -29,13 +32,13 @@ readCapabilities_ = y (self bs count i acc :
|
||||
readCapabilities = (count bs :
|
||||
readCapabilities_ bs count 0 t)
|
||||
|
||||
-- Helper: read a single root entry (32-byte raw hash + length-prefixed role)
|
||||
-- Helper: read a single root entry (4-byte u32 BE index + length-prefixed role)
|
||||
readRootEntry = (bs :
|
||||
bindResult (readBytes 32 bs)
|
||||
(hashRaw afterHash :
|
||||
bindResult (readLengthPrefixedString afterHash)
|
||||
bindResult (readBytes 4 bs)
|
||||
(indexRaw afterIndex :
|
||||
bindResult (readLengthPrefixedString afterIndex)
|
||||
(role afterRole :
|
||||
ok (pair hashRaw role) afterRole)))
|
||||
ok (pair indexRaw role) afterRole)))
|
||||
|
||||
-- Helper worker: read N root entries (counts up from 0)
|
||||
readRoots_ = y (self bs count i acc :
|
||||
@@ -54,13 +57,13 @@ readRoots = (count bs :
|
||||
readExportEntry = (bs :
|
||||
bindResult (readLengthPrefixedString bs)
|
||||
(name afterName :
|
||||
bindResult (readBytes 32 afterName)
|
||||
(rootHashRaw afterRootHash :
|
||||
bindResult (readLengthPrefixedString afterRootHash)
|
||||
bindResult (readBytes 4 afterName)
|
||||
(rootIndexRaw afterRootIndex :
|
||||
bindResult (readLengthPrefixedString afterRootIndex)
|
||||
(kind afterKind :
|
||||
bindResult (readLengthPrefixedString afterKind)
|
||||
(abi afterAbi :
|
||||
ok (pair name (pair rootHashRaw (pair kind abi))) afterAbi)))))
|
||||
ok (pair name (pair rootIndexRaw (pair kind abi))) afterAbi)))))
|
||||
|
||||
-- Helper worker: read N export entries (counts up from 0)
|
||||
readExports_ = y (self bs count i acc :
|
||||
@@ -200,7 +203,7 @@ lookupMetadata_ = y (self tlvs tag :
|
||||
lookupMetadata = (tlvs tag :
|
||||
lookupMetadata_ tlvs tag)
|
||||
|
||||
-- Get export name from an export entry (pair name (pair rootHash (pair kind abi)))
|
||||
-- Get export name from an export entry (pair name (pair rootIndex (pair kind abi)))
|
||||
exportName = (exp :
|
||||
matchPair
|
||||
(name _ : name)
|
||||
@@ -284,9 +287,9 @@ selectExportOpt = (exports optNameBytes :
|
||||
expectedSchema = "arboricx.bundle.manifest.v1"
|
||||
expectedBundleType = "tree-calculus-executable-object"
|
||||
expectedTreeCalculus = "tree-calculus.v1"
|
||||
expectedTreeHashAlgorithm = "sha256"
|
||||
expectedTreeHashDomain = "arboricx.merkle.node.v1"
|
||||
expectedTreeNodePayload = "arboricx.merkle.payload.v1"
|
||||
expectedTreeHashAlgorithm = "indexed"
|
||||
expectedTreeHashDomain = "arboricx.indexed.node.v1"
|
||||
expectedTreeNodePayload = "arboricx.indexed.payload.v1"
|
||||
expectedRuntimeSemantics = "tree-calculus.v1"
|
||||
expectedRuntimeEvaluation = "normal-order"
|
||||
expectedRuntimeAbi = "arboricx.abi.tree.v1"
|
||||
374
lib/arboricx/nodes.tri
Normal file
374
lib/arboricx/nodes.tri
Normal file
@@ -0,0 +1,374 @@
|
||||
!import "prelude" !Local
|
||||
!import "binary" !Local
|
||||
!import "arboricx.common" !Local
|
||||
|
||||
-- Indexed Arboricx node section reader.
|
||||
--
|
||||
-- Node records in the indexed format are just length-prefixed payloads:
|
||||
-- u32 payloadLength || payload
|
||||
-- A payload is one of:
|
||||
-- 0x00
|
||||
-- 0x01 || childIndex:u32be
|
||||
-- 0x02 || leftIndex:u32be || rightIndex:u32be
|
||||
-- Child indices must point strictly backward in the node array.
|
||||
|
||||
readNodeRecord = (bs :
|
||||
bindResult (readBytes 4 bs)
|
||||
(payloadLength afterPayloadLength :
|
||||
bindResult (readBytes (u32BEBytesToNat payloadLength) afterPayloadLength)
|
||||
(payload afterPayload :
|
||||
ok payload afterPayload)))
|
||||
|
||||
nodePayloadKind = (nodePayload : bytesHead nodePayload)
|
||||
|
||||
nodePayloadHasTag? = (tag nodePayload :
|
||||
triage
|
||||
false
|
||||
(actualTag : equal? actualTag tag)
|
||||
(_ _ : false)
|
||||
(nodePayloadKind nodePayload))
|
||||
|
||||
nodePayloadLeaf? = (nodePayload :
|
||||
bytesEq? [(0)] nodePayload)
|
||||
|
||||
nodePayloadStem? = (nodePayload :
|
||||
and?
|
||||
(nodePayloadHasTag? nodePayloadStemTag nodePayload)
|
||||
(equal? (bytesLength nodePayload) 5))
|
||||
|
||||
nodePayloadFork? = (nodePayload :
|
||||
and?
|
||||
(nodePayloadHasTag? nodePayloadForkTag nodePayload)
|
||||
(equal? (bytesLength nodePayload) 9))
|
||||
|
||||
nodePayloadValid? = (nodePayload :
|
||||
or?
|
||||
(nodePayloadLeaf? nodePayload)
|
||||
(or?
|
||||
(nodePayloadStem? nodePayload)
|
||||
(nodePayloadFork? nodePayload)))
|
||||
|
||||
nodeU32FromBytes4 = (b0 b1 b2 b3 :
|
||||
u32BEBytesToNat
|
||||
(pair b0
|
||||
(pair b1
|
||||
(pair b2
|
||||
(pair b3 t)))))
|
||||
|
||||
withNodePayloadStemIndex = (nodePayload shortK indexK :
|
||||
matchList
|
||||
(shortK t)
|
||||
(tag r0 :
|
||||
matchList
|
||||
(shortK t)
|
||||
(b0 r1 :
|
||||
matchList
|
||||
(shortK t)
|
||||
(b1 r2 :
|
||||
matchList
|
||||
(shortK t)
|
||||
(b2 r3 :
|
||||
matchList
|
||||
(shortK t)
|
||||
(b3 _ :
|
||||
indexK (nodeU32FromBytes4 b0 b1 b2 b3))
|
||||
r3) r2) r1) r0) nodePayload)
|
||||
|
||||
withNodePayloadForkIndices = (nodePayload shortK indicesK :
|
||||
matchList
|
||||
(shortK t)
|
||||
(tag r0 :
|
||||
matchList
|
||||
(shortK t)
|
||||
(l0 r1 :
|
||||
matchList
|
||||
(shortK t)
|
||||
(l1 r2 :
|
||||
matchList
|
||||
(shortK t)
|
||||
(l2 r3 :
|
||||
matchList
|
||||
(shortK t)
|
||||
(l3 r4 :
|
||||
matchList
|
||||
(shortK t)
|
||||
(r0b r5 :
|
||||
matchList
|
||||
(shortK t)
|
||||
(r1b r6 :
|
||||
matchList
|
||||
(shortK t)
|
||||
(r2b r7 :
|
||||
matchList
|
||||
(shortK t)
|
||||
(r3b _ :
|
||||
indicesK
|
||||
(nodeU32FromBytes4 l0 l1 l2 l3)
|
||||
(nodeU32FromBytes4 r0b r1b r2b r3b)) r7) r6) r5) r4) r3) r2) r1) r0) nodePayload)
|
||||
|
||||
nodePayloadStemChildIndex = (nodePayload :
|
||||
withNodePayloadStemIndex nodePayload (_ : 0) (index : index))
|
||||
|
||||
nodePayloadForkLeftIndex = (nodePayload :
|
||||
withNodePayloadForkIndices nodePayload (_ : 0) (left right : left))
|
||||
|
||||
nodePayloadForkRightIndex = (nodePayload :
|
||||
withNodePayloadForkIndices nodePayload (_ : 0) (left right : right))
|
||||
|
||||
nodeRecordsHaveInvalidPayload? = y (self nodeRecords :
|
||||
matchList
|
||||
false
|
||||
(nodePayload rest :
|
||||
or?
|
||||
(not? (nodePayloadValid? nodePayload))
|
||||
(self rest))
|
||||
nodeRecords)
|
||||
|
||||
nodePayloadChildIndices = (nodePayload :
|
||||
matchList
|
||||
t
|
||||
(tag rest :
|
||||
lazyBool
|
||||
(_ :
|
||||
withNodePayloadStemIndex
|
||||
nodePayload
|
||||
(_ : t)
|
||||
(childIndex : pair childIndex t))
|
||||
(_ :
|
||||
lazyBool
|
||||
(_ :
|
||||
withNodePayloadForkIndices
|
||||
nodePayload
|
||||
(_ : t)
|
||||
(leftIndex rightIndex :
|
||||
pair leftIndex (pair rightIndex t)))
|
||||
(_ : t)
|
||||
(equal? tag nodePayloadForkTag))
|
||||
(equal? tag nodePayloadStemTag))
|
||||
nodePayload)
|
||||
|
||||
-- True iff index n names an element before limit in records.
|
||||
-- For topologically sorted indexed bundles, every child of record i must
|
||||
-- satisfy childIndex < i, so searching only the prefix [0, i) validates both
|
||||
-- bounds and acyclicity.
|
||||
nodeIndexInPrefix? = y (self records n i limit :
|
||||
matchList
|
||||
false
|
||||
(_ rest :
|
||||
matchBool
|
||||
false
|
||||
(matchBool
|
||||
true
|
||||
(self rest n (succ i) limit)
|
||||
(equal? i n))
|
||||
(equal? i limit))
|
||||
records)
|
||||
|
||||
nodeChildIndicesInPrefix? = y (self childIndices records limit :
|
||||
matchList
|
||||
true
|
||||
(childIndex rest :
|
||||
matchBool
|
||||
(self rest records limit)
|
||||
false
|
||||
(nodeIndexInPrefix? records childIndex 0 limit))
|
||||
childIndices)
|
||||
|
||||
nodePayloadIndicesValid? = (nodePayload i records :
|
||||
nodeChildIndicesInPrefix?
|
||||
(nodePayloadChildIndices nodePayload)
|
||||
records
|
||||
i)
|
||||
|
||||
nodeRecordsValidIndicesFrom? = y (self allRecords remainingRecords i :
|
||||
matchList
|
||||
true
|
||||
(nodePayload rest :
|
||||
matchBool
|
||||
(self allRecords rest (succ i))
|
||||
false
|
||||
(nodePayloadIndicesValid? nodePayload i allRecords))
|
||||
remainingRecords)
|
||||
|
||||
nodeRecordsValidIndices? = (nodeRecords i :
|
||||
nodeRecordsValidIndicesFrom? nodeRecords nodeRecords i)
|
||||
|
||||
validateNodeRecords = (nodeRecords rest :
|
||||
matchBool
|
||||
(err errInvalidNodePayload rest)
|
||||
(matchBool
|
||||
(ok nodeRecords rest)
|
||||
(err errMissingNode rest)
|
||||
(nodeRecordsValidIndices? nodeRecords 0))
|
||||
(nodeRecordsHaveInvalidPayload? nodeRecords))
|
||||
|
||||
readNodeRecords_ = y (self bs nodeCount i acc :
|
||||
matchBool
|
||||
(ok (reverse acc) bs)
|
||||
(bindResult (readNodeRecord bs)
|
||||
(nodeRecord afterNodeRecord :
|
||||
self afterNodeRecord nodeCount (succ i) (pair nodeRecord acc)))
|
||||
(equal? i nodeCount))
|
||||
|
||||
readNodeRecords = (nodeCount bs :
|
||||
readNodeRecords_ bs nodeCount 0 t)
|
||||
|
||||
readNodesSection = (bs :
|
||||
bindResult (readBytes 8 bs)
|
||||
(nodeCount afterNodeCount :
|
||||
bindResult (readNodeRecords (u64BEBytesToNat nodeCount) afterNodeCount)
|
||||
(nodeRecords afterNodeRecords :
|
||||
bindResult (validateNodeRecords nodeRecords afterNodeRecords)
|
||||
(validNodeRecords afterValidNodeRecords :
|
||||
ok (pair nodeCount validNodeRecords) afterValidNodeRecords))))
|
||||
|
||||
readNodesSectionComplete = (bs :
|
||||
bindResult (readNodesSection bs)
|
||||
(nodesSection afterNodesSection :
|
||||
matchBool
|
||||
(ok nodesSection afterNodesSection)
|
||||
(err errUnexpectedBytes afterNodesSection)
|
||||
(bytesNil? afterNodesSection)))
|
||||
|
||||
readArboricxNodesSection = (bs :
|
||||
bindResult (readArboricxContainer bs)
|
||||
(container afterContainer :
|
||||
matchPair
|
||||
(_ directory :
|
||||
bindResult (sectionBytesOrErr arboricxNodesSectionId directory bs afterContainer)
|
||||
(nodesBytes _ :
|
||||
bindResult (readNodesSectionComplete nodesBytes)
|
||||
(nodesSection _ : ok nodesSection afterContainer)))
|
||||
container))
|
||||
|
||||
nodesSectionCount = (nodesSection :
|
||||
matchPair
|
||||
(nodeCount _ : nodeCount)
|
||||
nodesSection)
|
||||
|
||||
nodesSectionRecords = (nodesSection :
|
||||
matchPair
|
||||
(_ nodeRecords : nodeRecords)
|
||||
nodesSection)
|
||||
|
||||
nodeBuiltTreeIndex = (entry :
|
||||
matchPair
|
||||
(index _ : index)
|
||||
entry)
|
||||
|
||||
nodeBuiltTreeValue = (entry :
|
||||
matchPair
|
||||
(_ tree : tree)
|
||||
entry)
|
||||
|
||||
nodeTreeByIndex_ = (self builtTrees targetIndex :
|
||||
lazyList
|
||||
(_ : err errMissingNode t)
|
||||
(entry rest :
|
||||
lazyBool
|
||||
(_ : ok (nodeBuiltTreeValue entry) t)
|
||||
(_ : self rest targetIndex)
|
||||
(equal? (nodeBuiltTreeIndex entry) targetIndex))
|
||||
builtTrees)
|
||||
|
||||
nodeTreeByIndex = (builtTrees targetIndex :
|
||||
y nodeTreeByIndex_ builtTrees targetIndex)
|
||||
|
||||
nodePayloadToTreeFromBuilt = (builtTrees nodePayload :
|
||||
matchList
|
||||
(err errInvalidNodePayload t)
|
||||
(tag rest :
|
||||
lazyBool
|
||||
(_ : ok t t)
|
||||
(_ :
|
||||
lazyBool
|
||||
(_ :
|
||||
withNodePayloadStemIndex
|
||||
nodePayload
|
||||
(_ : err errInvalidNodePayload t)
|
||||
(childIndex :
|
||||
lazyResult
|
||||
(code after : err code after)
|
||||
(child _ : ok (t child) t)
|
||||
(nodeTreeByIndex builtTrees childIndex)))
|
||||
(_ :
|
||||
lazyBool
|
||||
(_ :
|
||||
withNodePayloadForkIndices
|
||||
nodePayload
|
||||
(_ : err errInvalidNodePayload t)
|
||||
(leftIndex rightIndex :
|
||||
lazyResult
|
||||
(code after : err code after)
|
||||
(left _ :
|
||||
lazyResult
|
||||
(code after : err code after)
|
||||
(right _ : ok (pair left right) t)
|
||||
(nodeTreeByIndex builtTrees rightIndex))
|
||||
(nodeTreeByIndex builtTrees leftIndex)))
|
||||
(_ : err errInvalidNodePayload t)
|
||||
(equal? tag nodePayloadForkTag))
|
||||
(equal? tag nodePayloadStemTag))
|
||||
(equal? tag 0))
|
||||
nodePayload)
|
||||
|
||||
nodeBuildState = (targetIndex i builtTrees :
|
||||
pair targetIndex (pair i builtTrees))
|
||||
|
||||
nodeBuildStateTargetIndex = (state :
|
||||
matchPair
|
||||
(targetIndex _ : targetIndex)
|
||||
state)
|
||||
|
||||
nodeBuildStateI = (state :
|
||||
matchPair
|
||||
(_ rest :
|
||||
matchPair
|
||||
(i _ : i)
|
||||
rest)
|
||||
state)
|
||||
|
||||
nodeBuildStateBuiltTrees = (state :
|
||||
matchPair
|
||||
(_ rest :
|
||||
matchPair
|
||||
(_ builtTrees : builtTrees)
|
||||
rest)
|
||||
state)
|
||||
|
||||
nodeIndexToTree_ = (self remainingRecords state :
|
||||
((nodeIndex :
|
||||
((i :
|
||||
((builtTrees :
|
||||
lazyList
|
||||
(_ : err errMissingNode t)
|
||||
(nodePayload rest :
|
||||
lazyResult
|
||||
(code after : err code after)
|
||||
(tree _ :
|
||||
lazyBool
|
||||
(_ : ok tree t)
|
||||
(_ :
|
||||
self
|
||||
rest
|
||||
(nodeBuildState
|
||||
nodeIndex
|
||||
(succ i)
|
||||
(pair (pair i tree) builtTrees)))
|
||||
(equal? i nodeIndex))
|
||||
(nodePayloadToTreeFromBuilt builtTrees nodePayload))
|
||||
remainingRecords)
|
||||
(nodeBuildStateBuiltTrees state)))
|
||||
(nodeBuildStateI state)))
|
||||
(nodeBuildStateTargetIndex state)))
|
||||
|
||||
nodeIndexToTree = (nodeRecords nodeIndex :
|
||||
y nodeIndexToTree_ nodeRecords (nodeBuildState nodeIndex 0 t))
|
||||
|
||||
readArboricxTreeFromIndex = (rootIndexBytes bs :
|
||||
bindResult (readArboricxNodesSection bs)
|
||||
(nodesSection afterContainer :
|
||||
bindResult (nodeIndexToTree (nodesSectionRecords nodesSection) (u32BEBytesToNat rootIndexBytes))
|
||||
(tree _ : ok tree afterContainer)))
|
||||
|
||||
readArboricxExecutableFromIndex = readArboricxTreeFromIndex
|
||||
206
lib/arboricx/server.tri
Normal file
206
lib/arboricx/server.tri
Normal file
@@ -0,0 +1,206 @@
|
||||
!import "prelude" !Local
|
||||
!import "io" !Local
|
||||
!import "http" !Local
|
||||
!import "socket" !Local
|
||||
!import "patterns" !Local
|
||||
!import "arboricx" !Local
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Store layout helpers
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
pathJoin a b = append a (append "/" b)
|
||||
|
||||
objectDir root shard =
|
||||
pathJoin (pathJoin root "objects") shard
|
||||
|
||||
hashShard hash =
|
||||
matchList
|
||||
t
|
||||
(h0 r0 :
|
||||
matchList
|
||||
(pair h0 t)
|
||||
(h1 r1 :
|
||||
matchList
|
||||
(pair h0 (pair h1 t))
|
||||
(h2 _ :
|
||||
pair h0 (pair h1 (pair h2 t)))
|
||||
r1)
|
||||
r0)
|
||||
hash
|
||||
|
||||
bundleObjectPath root hash =
|
||||
pathJoin
|
||||
(objectDir root (hashShard hash))
|
||||
(append hash ".arboricx")
|
||||
|
||||
bundleTmpPath root hash time =
|
||||
pathJoin
|
||||
(pathJoin root "tmp")
|
||||
(append hash ".tmp")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Store initialization
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
ensureDir path =
|
||||
void (createDirectory path)
|
||||
|
||||
ensureStore root =
|
||||
foldl
|
||||
thenIO
|
||||
(pure (ok t t))
|
||||
[(ensureDir root)
|
||||
(ensureDir (pathJoin root "tmp"))
|
||||
(ensureDir (pathJoin root "objects"))
|
||||
(ensureDir (pathJoin root "aliases"))
|
||||
(ensureDir (pathJoin (pathJoin root "aliases") "names"))
|
||||
(ensureDir (pathJoin (pathJoin root "aliases") "packages"))
|
||||
(ensureDir (pathJoin root "manifests"))]
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Bundle object write
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
putBundleWrite root bundleBytes hash shard tmpPath finalPath =
|
||||
do onOk_
|
||||
_ <- mapErrIO "createDirectory: " (createDirectory (objectDir root shard))
|
||||
_ <- mapErrIO "writeBytes: " (writeBytes tmpPath bundleBytes)
|
||||
_ <- mapErrIO "renameFile: " (renameFile tmpPath finalPath)
|
||||
pure (ok hash t)
|
||||
|
||||
putBundleWithHash root bundleBytes time hash =
|
||||
let shard = hashShard hash in
|
||||
let tmpPath = bundleTmpPath root hash time in
|
||||
let finalPath = bundleObjectPath root hash in
|
||||
putBundleWrite root bundleBytes hash shard tmpPath finalPath
|
||||
|
||||
putBundle root bundleBytes =
|
||||
do onOk_
|
||||
time <- mapErrIO "currentTime: " currentTime
|
||||
hash <- mapErrIO "sha256Hex: " (sha256Hex bundleBytes)
|
||||
savedHash <- mapErrIO "withHash: " (putBundleWithHash root bundleBytes time hash)
|
||||
pure (ok savedHash t)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Bundle object fetch
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
getBundleByHash root hash =
|
||||
onResult_ (readFile (bundleObjectPath root hash))
|
||||
(errMsg : pure (err errMsg t))
|
||||
(bytes : pure (ok bytes t))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Route prefix helper
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
stripPrefix_ self input prefix =
|
||||
lazyList
|
||||
(_ :
|
||||
lazyList
|
||||
(_ : just t)
|
||||
(_ _ : nothing)
|
||||
prefix)
|
||||
(ih ir :
|
||||
lazyList
|
||||
(_ : just input)
|
||||
(ph pr :
|
||||
lazyBool
|
||||
(_ : self ir pr)
|
||||
(_ : nothing)
|
||||
(equal? ih ph))
|
||||
prefix)
|
||||
input
|
||||
|
||||
stripPrefix prefix input =
|
||||
y stripPrefix_ input prefix
|
||||
|
||||
bundleHashPrefix = "/_arboricx/bundle/hash/"
|
||||
bundlePath = "/_arboricx/bundle"
|
||||
healthPath = "/_arboricx/health"
|
||||
bundleContentType = "application/vnd.arboricx.bundle"
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Landing page
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- TODO: Let's replace in-line HTML with the ability to read and serve files
|
||||
-- from a public/ folder.
|
||||
|
||||
htmlLandingPage = "<!DOCTYPE html><html><head><meta name='viewport' content='width=device-width, initial-scale=1'><title>Arboricx Server</title></head><body><h1>Arboricx Server</h1><p>Bundle registry</p><p><a href='https://git.eversole.co/James/tricu'>Made with Love (and trees, lots of trees)</a></p></body></html>"
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Registry routes
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
bundleResponse bytes = response 200 bundleContentType bytes
|
||||
|
||||
serveBundleHash root hash =
|
||||
onResult_ (getBundleByHash root hash)
|
||||
(errMsg : pure (errorResponse 404 errMsg))
|
||||
(bytes : pure (bundleResponse bytes))
|
||||
|
||||
healthRoute method target =
|
||||
cond
|
||||
[(guard (_ : equal? method "GET") (_ : getHealth))
|
||||
(guard (_ : true) (_ : pure notFoundResponse))]
|
||||
where getHealth =
|
||||
cond
|
||||
[(guard (_ : equal? target healthPath) (_ : pure (okResponse "OK\n")))
|
||||
(guard (_ : true) (_ : pure notFoundResponse))]
|
||||
|
||||
putBundleRoute root method target body =
|
||||
cond
|
||||
[(guard (_ : equal? method "POST") (_ : postBundle))
|
||||
(guard (_ : true) (_ : pure notFoundResponse))]
|
||||
where postBundle =
|
||||
cond
|
||||
[(guard (_ : equal? target bundlePath) (_ : handleUpload))
|
||||
(guard (_ : true) (_ : pure notFoundResponse))]
|
||||
where handleUpload =
|
||||
onResult_ (putBundle root body)
|
||||
(err : pure (badRequestResponse (append "Upload failed: " err)))
|
||||
(hash : pure (createdResponse hash))
|
||||
|
||||
getBundleRoute root method target =
|
||||
cond
|
||||
[(guard (_ : equal? method "GET") (_ : getBundle))
|
||||
(guard (_ : true) (_ : pure notFoundResponse))]
|
||||
where getBundle =
|
||||
lazyMaybe
|
||||
(_ : pure notFoundResponse)
|
||||
(hash : serveBundleHash root hash)
|
||||
(stripPrefix bundleHashPrefix target)
|
||||
|
||||
arboricxRouter root method target headers body =
|
||||
cond
|
||||
[(guard (_ : equal? method "GET") (_ : getRoutes))
|
||||
(guard (_ : equal? method "POST") (_ : putBundleRoute root method target body))
|
||||
(guard (_ : true) (_ : pure notFoundResponse))]
|
||||
where getRoutes =
|
||||
cond
|
||||
[(guard (_ : equal? target "/") (_ : pure (htmlResponse htmlLandingPage)))
|
||||
(guard (_ : true) (_ : getBundleOrHealth))]
|
||||
where getBundleOrHealth =
|
||||
lazyMaybe
|
||||
(_ : healthRoute method target)
|
||||
(hash : serveBundleHash root hash)
|
||||
(stripPrefix bundleHashPrefix target)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Server entrypoint
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
arboricxHandler root = (client peer :
|
||||
httpHandlerIO
|
||||
(method target headers body :
|
||||
arboricxRouter root method target headers body)
|
||||
client
|
||||
peer)
|
||||
|
||||
arboricxServer root addr port =
|
||||
onResult_ (listenSocket addr port 128)
|
||||
(errMsg : pure (err errMsg t))
|
||||
(server :
|
||||
serveForever server (arboricxHandler root))
|
||||
323
lib/base.tri
323
lib/base.tri
@@ -1,18 +1,18 @@
|
||||
false = t
|
||||
_ = t
|
||||
true = t t
|
||||
id = a : a
|
||||
const = a b : a
|
||||
id a@_a =@_a a
|
||||
const a@_a b@_b =@_a a
|
||||
pair = t
|
||||
if = cond then else : t (t else (t t then)) t cond
|
||||
if cond then else = t (t else (t t then)) t cond
|
||||
|
||||
y = ((mut wait fun : wait mut (x : fun (wait mut x)))
|
||||
(x : x x)
|
||||
(a0 a1 a2 : t (t a0) (t t a2) a1))
|
||||
|
||||
compose = f g x : f (g x)
|
||||
compose f@(Fn [_b] _c) g@(Fn [_a] _b) x@_a =@_c f (g x)
|
||||
|
||||
triage = leaf stem fork : t (t leaf stem) fork
|
||||
triage leaf stem fork = t (t leaf stem) fork
|
||||
test = triage "Leaf" (_ : "Stem") (_ _ : "Fork")
|
||||
|
||||
matchBool = (ot of : triage
|
||||
@@ -31,7 +31,18 @@ lOr = (triage
|
||||
(_ _ : true)
|
||||
(_ _ _ : true))
|
||||
|
||||
matchPair = a : triage _ _ a
|
||||
matchPair a = triage _ _ a
|
||||
|
||||
fst p = matchPair takeFirst p
|
||||
where takeFirst a b = a
|
||||
snd p = matchPair takeSecond p
|
||||
where takeSecond a b = b
|
||||
|
||||
resultIsOk result =
|
||||
matchResult (err rest : false) (val rest : true) result
|
||||
|
||||
resultIsErr result =
|
||||
matchResult (err rest : true) (val rest : false) result
|
||||
|
||||
not? = matchBool false true
|
||||
and? = matchBool id (_ : false)
|
||||
@@ -72,3 +83,303 @@ succ = y (self :
|
||||
(t (t t))
|
||||
(_ tail : t t (self tail))
|
||||
t))
|
||||
|
||||
ok value rest = pair true (pair value rest)
|
||||
err msg rest = pair false (pair msg rest)
|
||||
|
||||
matchResult errCase okCase result =
|
||||
matchPair
|
||||
(tag payload :
|
||||
matchPair
|
||||
(value rest :
|
||||
matchBool
|
||||
(okCase value rest)
|
||||
(errCase value rest)
|
||||
tag)
|
||||
payload)
|
||||
result
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Maybe / Option type
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
nothing = t
|
||||
just x = t x
|
||||
|
||||
matchMaybe nothingCase justCase maybe =
|
||||
triage
|
||||
nothingCase
|
||||
justCase
|
||||
(_ _ : nothingCase)
|
||||
maybe
|
||||
|
||||
maybe default f m = matchMaybe default f m
|
||||
maybeMap f@(Fn [_a] _b) m@(Maybe _a) =@(Maybe _b) matchMaybe nothing (compose just f) m
|
||||
maybeBind m@(Maybe _a) f@(Fn [_a] (Maybe _b)) =@(Maybe _b) matchMaybe nothing f m
|
||||
maybeOr default@_a m@(Maybe _a) =@_a matchMaybe default id m
|
||||
maybe? = matchMaybe false (_ : true)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Basic arithmetic
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
ifLazy = (cond thenK elseK :
|
||||
matchBool
|
||||
(thenK t)
|
||||
(elseK t)
|
||||
cond)
|
||||
|
||||
andLazy? = (a bK :
|
||||
ifLazy
|
||||
a
|
||||
bK
|
||||
(_ : false))
|
||||
|
||||
pred = y (self : triage
|
||||
0
|
||||
(_ : 0)
|
||||
(bit rest :
|
||||
matchBool
|
||||
(matchBool
|
||||
0
|
||||
(pair 0 rest)
|
||||
(equal? rest 0))
|
||||
(matchBool
|
||||
0
|
||||
(pair 1 (self rest))
|
||||
(equal? rest 0))
|
||||
bit))
|
||||
|
||||
isZero? = triage true (_ : false) (_ _ : false)
|
||||
|
||||
add = y (self x y :
|
||||
triage
|
||||
y
|
||||
(_ : succ y)
|
||||
(_ _ : succ (self (pred x) y))
|
||||
x)
|
||||
|
||||
sub = y (self a b :
|
||||
ifLazy
|
||||
(isZero? b)
|
||||
(_ : a)
|
||||
(_ : self (pred a) (pred b)))
|
||||
|
||||
lte? = y (self a b :
|
||||
ifLazy
|
||||
(isZero? a)
|
||||
(_ : true)
|
||||
(_ :
|
||||
ifLazy
|
||||
(isZero? b)
|
||||
(_ : false)
|
||||
(_ : self (pred a) (pred b))))
|
||||
|
||||
gte? = a b :
|
||||
lte? b a
|
||||
|
||||
lt? = a b :
|
||||
and? (lte? a b) (not? (equal? a b))
|
||||
|
||||
gt? = a b :
|
||||
lt? b a
|
||||
|
||||
mul = y (self a b :
|
||||
ifLazy
|
||||
(isZero? b)
|
||||
(_ : 0)
|
||||
(_ : add a (self a (pred b))))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Result combinators
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
mapResult = (f result :
|
||||
matchResult
|
||||
(code rest : err code rest)
|
||||
(value rest : ok (f value) rest)
|
||||
result)
|
||||
|
||||
bindResult = (result f :
|
||||
matchResult
|
||||
(code rest : err code rest)
|
||||
(value rest : f value rest)
|
||||
result)
|
||||
|
||||
resultOr = (default result :
|
||||
matchResult
|
||||
(_ _ : default)
|
||||
(value _ : value)
|
||||
result)
|
||||
|
||||
resultMapErr = (f result :
|
||||
matchResult
|
||||
(code rest : err (f code) rest)
|
||||
(value rest : ok value rest)
|
||||
result)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- View facts
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
factsFact name provenance view = pair name (pair provenance view)
|
||||
factsChecked = 0
|
||||
factsTrusted = 1
|
||||
factsUnchecked = 2
|
||||
factsField tag value = pair tag value
|
||||
factsRecord tag fields = pair tag fields
|
||||
factsVar id = factsRecord 8 [(factsField 10 id)]
|
||||
factsForall binders body =
|
||||
factsRecord 9 [(factsField 11 binders) (factsField 12 body)]
|
||||
factsFn args result =
|
||||
factsRecord 1 [(factsField 0 args) (factsField 1 result)]
|
||||
factsAny = factsRecord 0 []
|
||||
factsRef symbol = factsRecord 2 [(factsField 2 symbol)]
|
||||
factsBool = factsRef 0
|
||||
factsString = factsRef 1
|
||||
factsByte = factsRef 2
|
||||
factsUnit = factsRef 3
|
||||
factsMaybe elem = factsRecord 4 [(factsField 3 elem)]
|
||||
factsList elem = factsRecord 3 [(factsField 3 elem)]
|
||||
factsPair left right = factsRecord 5 [(factsField 4 left) (factsField 5 right)]
|
||||
factsResult err ok = factsRecord 6 [(factsField 6 err) (factsField 7 ok)]
|
||||
|
||||
viewFacts =
|
||||
[ (factsFact "pair" factsTrusted
|
||||
(factsForall [0]
|
||||
(factsFn
|
||||
[(factsVar 0) (factsList (factsVar 0))]
|
||||
(factsList (factsVar 0)))))
|
||||
(factsFact "nothing" factsTrusted
|
||||
(factsForall [0]
|
||||
(factsMaybe (factsVar 0))))
|
||||
(factsFact "just" factsTrusted
|
||||
(factsForall [0]
|
||||
(factsFn [(factsVar 0)] (factsMaybe (factsVar 0)))))
|
||||
(factsFact "false" factsTrusted factsBool)
|
||||
(factsFact "true" factsTrusted factsBool)
|
||||
(factsFact "if" factsTrusted
|
||||
(factsForall [0]
|
||||
(factsFn [factsBool (factsVar 0) (factsVar 0)] (factsVar 0))))
|
||||
(factsFact "triage" factsTrusted
|
||||
(factsForall [0]
|
||||
(factsFn [factsAny factsAny factsAny factsAny] (factsVar 0))))
|
||||
(factsFact "test" factsTrusted factsString)
|
||||
(factsFact "matchBool" factsTrusted
|
||||
(factsForall [0]
|
||||
(factsFn
|
||||
[(factsVar 0) (factsVar 0) factsBool]
|
||||
(factsVar 0))))
|
||||
(factsFact "lAnd" factsTrusted
|
||||
(factsFn [factsBool factsBool] factsBool))
|
||||
(factsFact "lOr" factsTrusted
|
||||
(factsFn [factsBool factsBool] factsBool))
|
||||
(factsFact "matchPair" factsTrusted
|
||||
(factsForall [0 1 2]
|
||||
(factsFn
|
||||
[(factsFn [(factsVar 0) (factsVar 1)] (factsVar 2))
|
||||
(factsPair (factsVar 0) (factsVar 1))]
|
||||
(factsVar 2))))
|
||||
(factsFact "fst" factsTrusted
|
||||
(factsForall [0 1]
|
||||
(factsFn [(factsPair (factsVar 0) (factsVar 1))] (factsVar 0))))
|
||||
(factsFact "snd" factsTrusted
|
||||
(factsForall [0 1]
|
||||
(factsFn [(factsPair (factsVar 0) (factsVar 1))] (factsVar 1))))
|
||||
(factsFact "not?" factsTrusted
|
||||
(factsFn [factsBool] factsBool))
|
||||
(factsFact "and?" factsTrusted
|
||||
(factsFn [factsBool factsBool] factsBool))
|
||||
(factsFact "or?" factsTrusted
|
||||
(factsFn [factsBool factsBool] factsBool))
|
||||
(factsFact "xor?" factsTrusted
|
||||
(factsFn [factsBool factsBool] factsBool))
|
||||
(factsFact "equal?" factsTrusted
|
||||
(factsForall [0]
|
||||
(factsFn [(factsVar 0) (factsVar 0)] factsBool)))
|
||||
(factsFact "succ" factsTrusted
|
||||
(factsFn [factsByte] factsByte))
|
||||
(factsFact "pred" factsTrusted
|
||||
(factsFn [factsByte] factsByte))
|
||||
(factsFact "isZero?" factsTrusted
|
||||
(factsFn [factsByte] factsBool))
|
||||
(factsFact "add" factsTrusted
|
||||
(factsFn [factsByte factsByte] factsByte))
|
||||
(factsFact "sub" factsTrusted
|
||||
(factsFn [factsByte factsByte] factsByte))
|
||||
(factsFact "lte?" factsTrusted
|
||||
(factsFn [factsByte factsByte] factsBool))
|
||||
(factsFact "gte?" factsTrusted
|
||||
(factsFn [factsByte factsByte] factsBool))
|
||||
(factsFact "lt?" factsTrusted
|
||||
(factsFn [factsByte factsByte] factsBool))
|
||||
(factsFact "gt?" factsTrusted
|
||||
(factsFn [factsByte factsByte] factsBool))
|
||||
(factsFact "mul" factsTrusted
|
||||
(factsFn [factsByte factsByte] factsByte))
|
||||
(factsFact "matchMaybe" factsTrusted
|
||||
(factsForall [0 1]
|
||||
(factsFn
|
||||
[(factsVar 1)
|
||||
(factsFn [(factsVar 0)] (factsVar 1))
|
||||
(factsMaybe (factsVar 0))]
|
||||
(factsVar 1))))
|
||||
(factsFact "maybe" factsTrusted
|
||||
(factsForall [0 1]
|
||||
(factsFn
|
||||
[(factsVar 1)
|
||||
(factsFn [(factsVar 0)] (factsVar 1))
|
||||
(factsMaybe (factsVar 0))]
|
||||
(factsVar 1))))
|
||||
(factsFact "maybe?" factsTrusted
|
||||
(factsForall [0]
|
||||
(factsFn [(factsMaybe (factsVar 0))] factsBool)))
|
||||
(factsFact "ifLazy" factsTrusted
|
||||
(factsForall [0]
|
||||
(factsFn
|
||||
[factsBool
|
||||
(factsFn [factsUnit] (factsVar 0))
|
||||
(factsFn [factsUnit] (factsVar 0))]
|
||||
(factsVar 0))))
|
||||
(factsFact "andLazy?" factsTrusted
|
||||
(factsFn [factsBool (factsFn [factsUnit] factsBool)] factsBool))
|
||||
(factsFact "ok" factsTrusted
|
||||
(factsForall [0 1]
|
||||
(factsFn [(factsVar 1) factsAny] (factsResult (factsVar 0) (factsVar 1)))))
|
||||
(factsFact "err" factsTrusted
|
||||
(factsForall [0 1]
|
||||
(factsFn [(factsVar 0) factsAny] (factsResult (factsVar 0) (factsVar 1)))))
|
||||
(factsFact "matchResult" factsTrusted
|
||||
(factsForall [0 1 2]
|
||||
(factsFn
|
||||
[(factsFn [(factsVar 0) factsAny] (factsVar 2))
|
||||
(factsFn [(factsVar 1) factsAny] (factsVar 2))
|
||||
(factsResult (factsVar 0) (factsVar 1))]
|
||||
(factsVar 2))))
|
||||
(factsFact "resultIsOk" factsTrusted
|
||||
(factsForall [0 1]
|
||||
(factsFn [(factsResult (factsVar 0) (factsVar 1))] factsBool)))
|
||||
(factsFact "resultIsErr" factsTrusted
|
||||
(factsForall [0 1]
|
||||
(factsFn [(factsResult (factsVar 0) (factsVar 1))] factsBool)))
|
||||
(factsFact "mapResult" factsTrusted
|
||||
(factsForall [0 1 2]
|
||||
(factsFn
|
||||
[(factsFn [(factsVar 1)] (factsVar 2))
|
||||
(factsResult (factsVar 0) (factsVar 1))]
|
||||
(factsResult (factsVar 0) (factsVar 2)))))
|
||||
(factsFact "bindResult" factsTrusted
|
||||
(factsForall [0 1 2]
|
||||
(factsFn
|
||||
[(factsResult (factsVar 0) (factsVar 1))
|
||||
(factsFn [(factsVar 1)] (factsResult (factsVar 0) (factsVar 2)))]
|
||||
(factsResult (factsVar 0) (factsVar 2)))))
|
||||
(factsFact "resultOr" factsTrusted
|
||||
(factsForall [0 1]
|
||||
(factsFn [(factsVar 1) (factsResult (factsVar 0) (factsVar 1))] (factsVar 1))))
|
||||
(factsFact "resultMapErr" factsTrusted
|
||||
(factsForall [0 1 2]
|
||||
(factsFn
|
||||
[(factsFn [(factsVar 0)] (factsVar 2))
|
||||
(factsResult (factsVar 0) (factsVar 1))]
|
||||
(factsResult (factsVar 2) (factsVar 1)))))]
|
||||
|
||||
110
lib/binary.tri
110
lib/binary.tri
@@ -1,32 +1,18 @@
|
||||
!import "base.tri" !Local
|
||||
!import "list.tri" !Local
|
||||
!import "bytes.tri" !Local
|
||||
!import "prelude" !Local
|
||||
|
||||
errUnexpectedEof = 1
|
||||
errUnexpectedBytes = 2
|
||||
errUnexpectedByte = 3
|
||||
|
||||
ok = value rest : pair true (pair value rest)
|
||||
err = code rest : pair false (pair code rest)
|
||||
unit = t
|
||||
|
||||
matchResult = (errCase okCase result :
|
||||
matchPair
|
||||
(tag payload :
|
||||
matchPair
|
||||
(value rest :
|
||||
matchBool
|
||||
(okCase value rest)
|
||||
(errCase value rest)
|
||||
tag)
|
||||
payload)
|
||||
result)
|
||||
readU8 = (bytes :
|
||||
matchList
|
||||
(err errUnexpectedEof t)
|
||||
(h r : ok h r)
|
||||
bytes)
|
||||
|
||||
readU8 = (bytes : matchList
|
||||
(err errUnexpectedEof t)
|
||||
(h r : ok h r)
|
||||
bytes)
|
||||
|
||||
readBytes_ = y (self bs n i original acc :
|
||||
readBytes_ self bs n i original acc =
|
||||
matchList
|
||||
(matchBool
|
||||
(ok (reverse acc) bs)
|
||||
@@ -37,13 +23,12 @@ readBytes_ = y (self bs n i original acc :
|
||||
(ok (reverse acc) bs)
|
||||
(self r n (succ i) original (pair h acc))
|
||||
(equal? i n))
|
||||
bs)
|
||||
bs
|
||||
|
||||
readBytes = (n bs : readBytes_ bs n 0 bs t)
|
||||
readBytes = (n bs :
|
||||
y readBytes_ bs n 0 bs t)
|
||||
|
||||
unit = t
|
||||
|
||||
expectBytes_ = y (self expected bs original :
|
||||
expectBytes_ self expected bs original =
|
||||
matchList
|
||||
(ok unit bs)
|
||||
(expectedByte expectedRest :
|
||||
@@ -53,11 +38,12 @@ expectBytes_ = y (self expected bs original :
|
||||
matchBool
|
||||
(self expectedRest rest original)
|
||||
(err errUnexpectedBytes original)
|
||||
(byteEq? actual expectedByte))
|
||||
(equal? actual expectedByte))
|
||||
(readU8 bs))
|
||||
expected)
|
||||
expected
|
||||
|
||||
expectBytes = (expected bs : expectBytes_ expected bs bs)
|
||||
expectBytes = (expected bs :
|
||||
y expectBytes_ expected bs bs)
|
||||
|
||||
expectU8 = (expected bs :
|
||||
matchResult
|
||||
@@ -66,22 +52,58 @@ expectU8 = (expected bs :
|
||||
matchBool
|
||||
(ok unit rest)
|
||||
(err errUnexpectedByte bs)
|
||||
(byteEq? actual expected))
|
||||
(equal? actual expected))
|
||||
(readU8 bs))
|
||||
|
||||
mapResult = (f result :
|
||||
matchResult
|
||||
(code rest : err code rest)
|
||||
(value rest : ok (f value) rest)
|
||||
result)
|
||||
|
||||
bindResult = (result f :
|
||||
matchResult
|
||||
(code rest : err code rest)
|
||||
(value rest : f value rest)
|
||||
result)
|
||||
|
||||
read2 = (bs : readBytes 2 bs)
|
||||
read4 = (bs : readBytes 4 bs)
|
||||
readU16BEBytes = (bs : read2 bs)
|
||||
readU32BEBytes = (bs : read4 bs)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Parser combinators
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
pureParser = value bs : ok value bs
|
||||
failParser = code bs : err code bs
|
||||
|
||||
mapParser = f p bs : mapResult f (p bs)
|
||||
bindParser = p f bs : bindResult (p bs) f
|
||||
thenParser = p q bs : bindResult (p bs) (_ : q)
|
||||
|
||||
orParser = (p q bs :
|
||||
matchResult
|
||||
(_ _ : q bs)
|
||||
(value rest : ok value rest)
|
||||
(p bs))
|
||||
|
||||
readWhile_ self pred bs acc =
|
||||
matchResult
|
||||
(code rest : ok (reverse acc) bs)
|
||||
(value rest :
|
||||
matchBool
|
||||
(self pred rest (pair value acc))
|
||||
(ok (reverse acc) (pair value rest))
|
||||
(pred value))
|
||||
(readU8 bs)
|
||||
|
||||
readWhile = pred bs :
|
||||
y readWhile_ pred bs t
|
||||
|
||||
readUntil = pred :
|
||||
readWhile (x : not? (pred x))
|
||||
|
||||
readRemaining = bs : ok bs t
|
||||
|
||||
peekU8 = (bs :
|
||||
matchResult
|
||||
(code rest : err code bs)
|
||||
(value rest : ok value bs)
|
||||
(readU8 bs))
|
||||
|
||||
eof? = (bs :
|
||||
matchBool
|
||||
(ok t bs)
|
||||
(err errUnexpectedEof bs)
|
||||
(emptyList? bs))
|
||||
|
||||
expectAscii = expectBytes
|
||||
|
||||
@@ -1,51 +1,17 @@
|
||||
!import "base.tri" !Local
|
||||
!import "list.tri" !Local
|
||||
|
||||
nothing = t
|
||||
just = x : t x
|
||||
!import "base" !Local
|
||||
!import "list" !Local
|
||||
|
||||
bytesNil? = emptyList?
|
||||
|
||||
bytesHead = matchList nothing (h _ : just h)
|
||||
bytesHead =
|
||||
matchList nothing (h _ : just h)
|
||||
|
||||
bytesTail = matchList nothing (_ r : just r)
|
||||
bytesTail =
|
||||
matchList nothing (_ r : just r)
|
||||
|
||||
byteEq? = equal?
|
||||
bytesLength = length
|
||||
bytesAppend = append
|
||||
|
||||
bytesTake_ = y (self remaining n i :
|
||||
matchList
|
||||
t
|
||||
(h r :
|
||||
matchBool
|
||||
t
|
||||
(pair h (self r n (succ i)))
|
||||
(equal? i n))
|
||||
remaining)
|
||||
|
||||
bytesTake = n bytes : bytesTake_ bytes n 0
|
||||
|
||||
bytesDrop_ = y (self remaining n i :
|
||||
matchList
|
||||
t
|
||||
(_ r :
|
||||
matchBool
|
||||
remaining
|
||||
(self r n (succ i))
|
||||
(equal? i n))
|
||||
remaining)
|
||||
|
||||
bytesDrop = n bytes : bytesDrop_ bytes n 0
|
||||
|
||||
bytesSplitAt = n bytes : pair (bytesTake n bytes) (bytesDrop n bytes)
|
||||
|
||||
bytesEq? = y (self xs ys :
|
||||
matchList
|
||||
(matchList true (_ _ : false) ys)
|
||||
(xh xt :
|
||||
matchList
|
||||
false
|
||||
(yh yt : and? (byteEq? xh yh) (self xt yt))
|
||||
ys)
|
||||
xs)
|
||||
bytesTake = take
|
||||
bytesDrop = drop
|
||||
bytesSplitAt = splitAt
|
||||
bytesEq? = equal?
|
||||
|
||||
22
lib/conversions.tri
Normal file
22
lib/conversions.tri
Normal file
@@ -0,0 +1,22 @@
|
||||
!import "base" !Local
|
||||
!import "list" !Local
|
||||
|
||||
incDecRev = y (self : matchList
|
||||
"1"
|
||||
(digit rest :
|
||||
matchBool
|
||||
(pair 48 (self rest))
|
||||
(pair (succ digit) rest)
|
||||
(equal? digit 57)))
|
||||
|
||||
showNumberRev_ = y (self n acc :
|
||||
matchBool
|
||||
acc
|
||||
(self (pred n) (incDecRev acc))
|
||||
(equal? n 0))
|
||||
|
||||
showNumber = (n :
|
||||
matchBool
|
||||
"0"
|
||||
(reverse (showNumberRev_ n t))
|
||||
(equal? n 0))
|
||||
849
lib/http.tri
Normal file
849
lib/http.tri
Normal file
@@ -0,0 +1,849 @@
|
||||
!import "prelude" !Local
|
||||
!import "io" !Local
|
||||
!import "patterns" !Local
|
||||
!import "socket" !Local
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Constants
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
maxHeaderBytes = 65536
|
||||
maxBodyBytes = 1048576
|
||||
maxUriBytes = 8192
|
||||
|
||||
crlf = pair 13 (pair 10 t)
|
||||
crlfcrlf = pair 13 (pair 10 (pair 13 (pair 10 t)))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Small byte/list helpers
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
chomp = (xs :
|
||||
lazyList
|
||||
(_ : t)
|
||||
(h r :
|
||||
lazyBool
|
||||
(_ : reverse r)
|
||||
(_ : xs)
|
||||
(equal? h 13))
|
||||
(reverse xs))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Response construction
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
statusPhrases =
|
||||
[(pair 200 "OK")
|
||||
(pair 201 "Created")
|
||||
(pair 204 "No Content")
|
||||
(pair 400 "Bad Request")
|
||||
(pair 404 "Not Found")
|
||||
(pair 405 "Method Not Allowed")
|
||||
(pair 431 "Request Header Fields Too Large")
|
||||
(pair 501 "Not Implemented")
|
||||
(pair 505 "HTTP Version Not Supported")]
|
||||
|
||||
lookupStatusPhrase_ self code phrases =
|
||||
lazyList
|
||||
(_ : "Internal Server Error")
|
||||
(h r :
|
||||
lazyBool
|
||||
(_ : snd h)
|
||||
(_ : self code r)
|
||||
(equal? code (fst h)))
|
||||
phrases
|
||||
|
||||
statusPhrase = (code :
|
||||
y lookupStatusPhrase_ code statusPhrases)
|
||||
|
||||
statusLine = (code phrase :
|
||||
append "HTTP/1.1 " (append (showNumber code) (append " " (append phrase "\r\n"))))
|
||||
|
||||
headerLine = (key value :
|
||||
append key (append ": " (append value "\r\n")))
|
||||
|
||||
buildResponse = (status headers body :
|
||||
append
|
||||
(statusLine status (statusPhrase status))
|
||||
(append
|
||||
(foldl (acc h : append acc (headerLine (fst h) (snd h))) "" headers)
|
||||
(append "\r\n" body)))
|
||||
|
||||
response = (status contentType body :
|
||||
buildResponse status
|
||||
[(pair "Content-Type" contentType)
|
||||
(pair "Content-Length" (showNumber (length body)))
|
||||
(pair "Connection" "close")]
|
||||
body)
|
||||
|
||||
emptyResponse = (status :
|
||||
buildResponse status
|
||||
[(pair "Content-Length" "0")
|
||||
(pair "Connection" "close")]
|
||||
"")
|
||||
|
||||
okResponse = (body :
|
||||
response 200 "text/plain; charset=utf-8" body)
|
||||
|
||||
textResponse = (body :
|
||||
response 200 "text/plain; charset=utf-8" body)
|
||||
|
||||
jsonResponse = (body :
|
||||
response 200 "application/json" body)
|
||||
|
||||
htmlResponse = (body :
|
||||
response 200 "text/html; charset=utf-8" body)
|
||||
|
||||
createdResponse = (body :
|
||||
response 201 "text/plain; charset=utf-8" body)
|
||||
|
||||
notFoundResponse = (
|
||||
response 404 "text/plain; charset=utf-8" "Not found\n")
|
||||
|
||||
badRequestResponse = (msg :
|
||||
response 400 "text/plain; charset=utf-8" msg)
|
||||
|
||||
errorResponse = (status msg :
|
||||
response status "text/plain; charset=utf-8" msg)
|
||||
|
||||
headerEndState state h =
|
||||
lazyBool
|
||||
(_ :
|
||||
lazyBool
|
||||
(_ : 3)
|
||||
(_ : 1)
|
||||
(equal? state 2))
|
||||
(_ :
|
||||
lazyBool
|
||||
(_ :
|
||||
lazyBool
|
||||
(_ : 4)
|
||||
(_ : 2)
|
||||
(equal? state 3))
|
||||
(_ : 0)
|
||||
(boolAnd?
|
||||
(equal? h 10)
|
||||
(boolOr? (equal? state 1) (equal? state 3))))
|
||||
(equal? h 13)
|
||||
|
||||
headersOnly_ self bs state acc =
|
||||
lazyList
|
||||
(_ : reverse acc)
|
||||
(h r :
|
||||
let nextAcc = pair h acc in
|
||||
let nextState = headerEndState state h in
|
||||
lazyBool
|
||||
(_ : reverse nextAcc)
|
||||
(_ : self r nextState nextAcc)
|
||||
(equal? nextState 4))
|
||||
bs
|
||||
|
||||
headersOnly = (response :
|
||||
y headersOnly_ response 0 t)
|
||||
|
||||
responseForMethod = (method resp :
|
||||
lazyBool
|
||||
(_ : headersOnly resp)
|
||||
(_ : resp)
|
||||
(equal? method "HEAD"))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Header receive / framing
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
recvUntilMax_ = (y (self sock pattern maxBytes acc accLen :
|
||||
onResult_ (recv sock 1)
|
||||
(err :
|
||||
pure (err 400 acc))
|
||||
(chunk :
|
||||
lazyBool
|
||||
(_ : pure (err 400 acc))
|
||||
(_ :
|
||||
let chunkLen = length chunk in
|
||||
let nextLen = add accLen chunkLen in
|
||||
let next = append acc chunk in
|
||||
lazyBool
|
||||
(_ :
|
||||
lazyBool
|
||||
(_ : pure (ok next t))
|
||||
(_ : self sock pattern maxBytes next nextLen)
|
||||
(contains? pattern next))
|
||||
(_ : pure (err 431 next))
|
||||
(lte? nextLen maxBytes))
|
||||
(emptyList? chunk))))
|
||||
|
||||
recvUntilMax = (sock pattern maxBytes :
|
||||
recvUntilMax_ sock pattern maxBytes t 0)
|
||||
|
||||
recvUntil = (sock pattern :
|
||||
recvUntilMax sock pattern maxHeaderBytes)
|
||||
|
||||
recvHeaders = (sock :
|
||||
recvUntilMax sock crlfcrlf maxHeaderBytes)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Request line parsing
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
readLineBytes_ = (y (self bs acc :
|
||||
lazyList
|
||||
(_ : pair (reverse acc) t)
|
||||
(h r :
|
||||
lazyBool
|
||||
(_ : pair (reverse acc) r)
|
||||
(_ :
|
||||
lazyBool
|
||||
(_ : self r acc)
|
||||
(_ : self r (pair h acc))
|
||||
(equal? h 13))
|
||||
(equal? h 10))
|
||||
bs))
|
||||
|
||||
readLineBytes = (bs :
|
||||
((result :
|
||||
pair (chomp (fst result)) (snd result))
|
||||
(readLineBytes_ bs t)))
|
||||
|
||||
parseThreeWords_ = (y (self bs phase acc w1 w2 :
|
||||
lazyList
|
||||
(_ :
|
||||
lazyBool
|
||||
(_ : ok (pair w1 (pair w2 (reverse acc))) t)
|
||||
(_ : err 400 "Bad Request\n")
|
||||
(equal? phase 2))
|
||||
(h r :
|
||||
lazyBool
|
||||
(_ :
|
||||
lazyBool
|
||||
(_ : self r 1 t (reverse acc) w2)
|
||||
(_ :
|
||||
lazyBool
|
||||
(_ : self r 2 t w1 (reverse acc))
|
||||
(_ : err 400 "Bad Request\n")
|
||||
(equal? phase 1))
|
||||
(equal? phase 0))
|
||||
(_ : self r phase (pair h acc) w1 w2)
|
||||
(equal? h 32))
|
||||
bs))
|
||||
|
||||
parseThreeWords = (bs :
|
||||
parseThreeWords_ bs 0 t t t)
|
||||
|
||||
parseRequestLine = (bs :
|
||||
((lineRest :
|
||||
lazyResult
|
||||
(code bad : err 400 "Bad Request\n")
|
||||
(req ignored : ok req (snd lineRest))
|
||||
(parseThreeWords (fst lineRest)))
|
||||
(readLineBytes bs)))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Header parsing
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
-- ASCII byte helpers below are structural on the Tree Calculus numeral
|
||||
-- spine. Do not replace them with lte?/sub based checks: these names are
|
||||
-- normalized at import time under abstract byte inputs.
|
||||
boolNot? = (b :
|
||||
matchBool false true b)
|
||||
|
||||
boolOr? = (a b :
|
||||
matchBool true b a)
|
||||
|
||||
boolAnd? = (a b :
|
||||
matchBool b false a)
|
||||
|
||||
low5NonZero? = (b0 b1 b2 b3 b4 :
|
||||
boolOr?
|
||||
(bit1? b0)
|
||||
(boolOr?
|
||||
(bit1? b1)
|
||||
(boolOr?
|
||||
(bit1? b2)
|
||||
(boolOr?
|
||||
(bit1? b3)
|
||||
(bit1? b4)))))
|
||||
|
||||
low5TooHighForUpper? = (b0 b1 b2 b3 b4 :
|
||||
boolAnd?
|
||||
(bit1? b4)
|
||||
(boolAnd?
|
||||
(bit1? b3)
|
||||
(boolOr?
|
||||
(bit1? b2)
|
||||
(boolAnd?
|
||||
(bit1? b1)
|
||||
(bit1? b0)))))
|
||||
|
||||
upperLow5? = (b0 b1 b2 b3 b4 :
|
||||
boolAnd?
|
||||
(low5NonZero? b0 b1 b2 b3 b4)
|
||||
(boolNot?
|
||||
(low5TooHighForUpper? b0 b1 b2 b3 b4)))
|
||||
|
||||
lowerAsciiBits = (b0 b1 b2 b3 b4 :
|
||||
pair b0
|
||||
(pair b1
|
||||
(pair b2
|
||||
(pair b3
|
||||
(pair b4
|
||||
(pair true
|
||||
(pair true 0)))))))
|
||||
|
||||
byte7BitsOr default c k =
|
||||
let noStem _ = default in
|
||||
let bit6 b0 b1 b2 b3 b4 b5 b6 r6 =
|
||||
k b0 b1 b2 b3 b4 b5 b6 r6 in
|
||||
let bit5 b0 b1 b2 b3 b4 b5 r5 =
|
||||
triage default noStem (bit6 b0 b1 b2 b3 b4 b5) r5 in
|
||||
let bit4 b0 b1 b2 b3 b4 r4 =
|
||||
triage default noStem (bit5 b0 b1 b2 b3 b4) r4 in
|
||||
let bit3 b0 b1 b2 b3 r3 =
|
||||
triage default noStem (bit4 b0 b1 b2 b3) r3 in
|
||||
let bit2 b0 b1 b2 r2 =
|
||||
triage default noStem (bit3 b0 b1 b2) r2 in
|
||||
let bit1 b0 b1 r1 =
|
||||
triage default noStem (bit2 b0 b1) r1 in
|
||||
let bit0 b0 r0 =
|
||||
triage default noStem (bit1 b0) r0 in
|
||||
triage default noStem bit0 c
|
||||
|
||||
toLowerAsciiByte = (c :
|
||||
byte7BitsOr c c (b0 b1 b2 b3 b4 b5 b6 rest :
|
||||
lazyBool
|
||||
(_ : lowerAsciiBits b0 b1 b2 b3 b4)
|
||||
(_ : c)
|
||||
(boolAnd?
|
||||
(isZero? rest)
|
||||
(boolAnd?
|
||||
(bit1? b6)
|
||||
(boolAnd?
|
||||
(bit0? b5)
|
||||
(upperLow5? b0 b1 b2 b3 b4))))))
|
||||
|
||||
finishHeaderLine = (self r headers key value seenColon :
|
||||
matchBool
|
||||
(matchBool
|
||||
(err 400 "Bad Request\n")
|
||||
(ok (reverse headers) r)
|
||||
seenColon)
|
||||
(matchBool
|
||||
(self r
|
||||
(pair (pair (reverse key) (reverse value)) headers)
|
||||
t
|
||||
t
|
||||
false
|
||||
true)
|
||||
(err 400 "Bad Request\n")
|
||||
seenColon)
|
||||
(emptyList? key))
|
||||
|
||||
finishHeaderEOF = (headers key value seenColon :
|
||||
matchBool
|
||||
(ok (reverse headers) t)
|
||||
(matchBool
|
||||
(ok (reverse (pair (pair (reverse key) (reverse value)) headers)) t)
|
||||
(err 400 "Bad Request\n")
|
||||
seenColon)
|
||||
(emptyList? key))
|
||||
|
||||
parseHeaders_ = (self bs headers key value seenColon trimValue :
|
||||
matchList
|
||||
(finishHeaderEOF headers key value seenColon)
|
||||
(h r :
|
||||
matchBool
|
||||
(finishHeaderLine self r headers key value seenColon)
|
||||
(matchBool
|
||||
(self r headers key value seenColon trimValue)
|
||||
(matchBool
|
||||
(matchBool
|
||||
(self r headers key value true true)
|
||||
(self r headers key (pair h value) true false)
|
||||
(boolAnd? trimValue (equal? h 32)))
|
||||
(matchBool
|
||||
(self r headers key value true true)
|
||||
(self r headers (pair (toLowerAsciiByte h) key) value false true)
|
||||
(equal? h 58))
|
||||
seenColon)
|
||||
(equal? h 13))
|
||||
(equal? h 10))
|
||||
bs)
|
||||
|
||||
parseHeaders = (bs :
|
||||
y parseHeaders_ bs t t t false true)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Content-Length parsing
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
bit0? = (x :
|
||||
isZero? x)
|
||||
|
||||
bit1? = (x :
|
||||
triage
|
||||
false
|
||||
(a : isZero? a)
|
||||
(_ _ : false)
|
||||
x)
|
||||
|
||||
low3 = (b0 b1 b2 :
|
||||
matchBool
|
||||
(matchBool
|
||||
(matchBool 7 6 (bit1? b0))
|
||||
(matchBool 5 4 (bit1? b0))
|
||||
(bit1? b1))
|
||||
(matchBool
|
||||
(matchBool 3 2 (bit1? b0))
|
||||
(matchBool 1 0 (bit1? b0))
|
||||
(bit1? b1))
|
||||
(bit1? b2))
|
||||
|
||||
decimalDigit = (c :
|
||||
triage
|
||||
nothing
|
||||
(_ : nothing)
|
||||
(b0 r0 :
|
||||
triage
|
||||
nothing
|
||||
(_ : nothing)
|
||||
(b1 r1 :
|
||||
triage
|
||||
nothing
|
||||
(_ : nothing)
|
||||
(b2 r2 :
|
||||
triage
|
||||
nothing
|
||||
(_ : nothing)
|
||||
(b3 r3 :
|
||||
triage
|
||||
nothing
|
||||
(_ : nothing)
|
||||
(b4 r4 :
|
||||
triage
|
||||
nothing
|
||||
(_ : nothing)
|
||||
(b5 r5 :
|
||||
matchBool
|
||||
(matchBool
|
||||
(matchBool
|
||||
(matchBool
|
||||
(matchBool
|
||||
(just (low3 b0 b1 b2))
|
||||
(matchBool
|
||||
(matchBool
|
||||
(just (matchBool 9 8 (bit1? b0)))
|
||||
nothing
|
||||
(bit0? b2))
|
||||
nothing
|
||||
(bit0? b1))
|
||||
(bit0? b3))
|
||||
nothing
|
||||
(bit1? b5))
|
||||
nothing
|
||||
(bit1? b4))
|
||||
nothing
|
||||
(isZero? r5))
|
||||
nothing
|
||||
true)
|
||||
r4)
|
||||
r3)
|
||||
r2)
|
||||
r1)
|
||||
r0)
|
||||
c)
|
||||
|
||||
readDecimal_ = (self bytes acc :
|
||||
matchList
|
||||
(just acc)
|
||||
(h r :
|
||||
matchMaybe
|
||||
nothing
|
||||
(d : self r (add (mul acc 10) d))
|
||||
(decimalDigit h))
|
||||
bytes)
|
||||
|
||||
readDecimal = (bytes :
|
||||
matchBool
|
||||
nothing
|
||||
(y readDecimal_ bytes 0)
|
||||
(emptyList? bytes))
|
||||
|
||||
maxBodyBytesDecimal = "1048576"
|
||||
|
||||
byte0? b = equal? b 48
|
||||
digitLtMax? maxDigit digit = lt? digit maxDigit
|
||||
|
||||
stripLeadingZeros_ self raw =
|
||||
lazyList
|
||||
(_ : t)
|
||||
(c r :
|
||||
lazyBool
|
||||
(_ : self r)
|
||||
(_ : raw)
|
||||
(byte0? c))
|
||||
raw
|
||||
|
||||
decimalLengthLte_ self max raw =
|
||||
lazyList
|
||||
(_ : true)
|
||||
(_ rest :
|
||||
lazyList
|
||||
(_ : false)
|
||||
(_ maxRest : self maxRest rest)
|
||||
max)
|
||||
raw
|
||||
|
||||
decimalSameLength_ self max raw =
|
||||
lazyList
|
||||
(_ :
|
||||
lazyList
|
||||
(_ : true)
|
||||
(_ _ : false)
|
||||
max)
|
||||
(_ rest :
|
||||
lazyList
|
||||
(_ : false)
|
||||
(_ maxRest : self maxRest rest)
|
||||
max)
|
||||
raw
|
||||
|
||||
sameLengthDecimalLte_ self max raw less =
|
||||
lazyList
|
||||
(_ : true)
|
||||
(digit rest :
|
||||
lazyList
|
||||
(_ : false)
|
||||
(maxDigit maxRest :
|
||||
lazyBool
|
||||
(_ : self maxRest rest true)
|
||||
(_ :
|
||||
lazyBool
|
||||
(_ : self maxRest rest true)
|
||||
(_ :
|
||||
lazyBool
|
||||
(_ : self maxRest rest false)
|
||||
(_ : false)
|
||||
(equal? digit maxDigit))
|
||||
(digitLtMax? maxDigit digit))
|
||||
less)
|
||||
max)
|
||||
raw
|
||||
|
||||
decimalLengthLte? max raw = y decimalLengthLte_ max raw
|
||||
|
||||
decimalSameLength? max raw = y decimalSameLength_ max raw
|
||||
|
||||
decimalBytesLte? max raw =
|
||||
let trimmed = y stripLeadingZeros_ raw in
|
||||
lazyBool
|
||||
(_ : y sameLengthDecimalLte_ max trimmed false)
|
||||
(_ : decimalLengthLte? max trimmed)
|
||||
(decimalSameLength? max trimmed)
|
||||
|
||||
parseContentLengthValue = (raw :
|
||||
matchMaybe
|
||||
(err 400 "Bad Request\n")
|
||||
(n :
|
||||
lazyBool
|
||||
(_ : ok (just n) t)
|
||||
(_ : err 413 "Request body too large\n")
|
||||
(decimalBytesLte? maxBodyBytesDecimal raw))
|
||||
(readDecimal raw))
|
||||
|
||||
contentLength_ = (self headers :
|
||||
matchList
|
||||
(ok nothing t)
|
||||
(h r :
|
||||
matchBool
|
||||
(parseContentLengthValue (snd h))
|
||||
(self r)
|
||||
(equal? "content-length" (fst h)))
|
||||
headers)
|
||||
|
||||
contentLength = (headers :
|
||||
y contentLength_ headers)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Body reading
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
bodyReadState = (remaining accRev rest :
|
||||
pair remaining (pair accRev rest))
|
||||
|
||||
bodyReadRemaining = (state :
|
||||
fst state)
|
||||
|
||||
bodyReadAccRev = (state :
|
||||
fst (snd state))
|
||||
|
||||
bodyReadRest = (state :
|
||||
snd (snd state))
|
||||
|
||||
takeBodyBytes_ = (self bytes remaining accRev :
|
||||
lazyBool
|
||||
(_ : bodyReadState 0 accRev bytes)
|
||||
(_ :
|
||||
lazyList
|
||||
(_ : bodyReadState remaining accRev t)
|
||||
(h r :
|
||||
self r (pred remaining) (pair h accRev))
|
||||
bytes)
|
||||
(isZero? remaining))
|
||||
|
||||
takeBodyBytes = (bytes remaining accRev :
|
||||
y takeBodyBytes_ bytes remaining accRev)
|
||||
|
||||
shiftRight1 n = triage 0 (_ : 0) (_ rest : rest) n
|
||||
|
||||
shiftRight2 n = shiftRight1 (shiftRight1 n)
|
||||
shiftRight4 n = shiftRight2 (shiftRight2 n)
|
||||
shiftRight8 n = shiftRight4 (shiftRight4 n)
|
||||
shiftRight12 n = shiftRight4 (shiftRight8 n)
|
||||
|
||||
shiftRight6 n = shiftRight2 (shiftRight4 n)
|
||||
|
||||
atLeast16? n = not? (isZero? (shiftRight4 n))
|
||||
atLeast64? n = not? (isZero? (shiftRight6 n))
|
||||
atLeast256? n = not? (isZero? (shiftRight8 n))
|
||||
atLeast1024? n = not? (isZero? (shiftRight2 (shiftRight8 n)))
|
||||
atLeast4096? n = not? (isZero? (shiftRight12 n))
|
||||
|
||||
recvChunkMax4096 remaining =
|
||||
lazyBool
|
||||
(_ : 4096)
|
||||
(_ :
|
||||
lazyBool
|
||||
(_ : 1024)
|
||||
(_ :
|
||||
lazyBool
|
||||
(_ : 256)
|
||||
(_ :
|
||||
lazyBool
|
||||
(_ : 64)
|
||||
(_ :
|
||||
lazyBool
|
||||
(_ : 16)
|
||||
(_ : 1)
|
||||
(atLeast16? remaining))
|
||||
(atLeast64? remaining))
|
||||
(atLeast256? remaining))
|
||||
(atLeast1024? remaining))
|
||||
(atLeast4096? remaining)
|
||||
|
||||
readBodyRecv = (self sock remaining accRev recvBytes :
|
||||
onResult_ (recv sock recvBytes)
|
||||
(errMsg :
|
||||
pure
|
||||
(err
|
||||
400
|
||||
(append "recv failed while reading body: " errMsg)))
|
||||
(chunk :
|
||||
let state = takeBodyBytes chunk remaining accRev in
|
||||
let nextRemaining = bodyReadRemaining state in
|
||||
let nextAccRev = bodyReadAccRev state in
|
||||
lazyBool
|
||||
(_ : pure (ok (reverse nextAccRev) (bodyReadRest state)))
|
||||
(_ : self sock nextRemaining nextAccRev)
|
||||
(isZero? nextRemaining)))
|
||||
|
||||
readBodyMore_ = (self sock remaining accRev :
|
||||
lazyBool
|
||||
(_ : pure (ok (reverse accRev) t))
|
||||
(_ : readBodyRecv self sock remaining accRev (recvChunkMax4096 remaining))
|
||||
(isZero? remaining))
|
||||
|
||||
readBodyMore = (sock remaining accRev :
|
||||
y readBodyMore_ sock remaining accRev)
|
||||
|
||||
readBodyExact = (sock expected initialBytes :
|
||||
let state = takeBodyBytes initialBytes expected t in
|
||||
let remaining = bodyReadRemaining state in
|
||||
let accRev = bodyReadAccRev state in
|
||||
lazyBool
|
||||
(_ : pure (ok (reverse accRev) (bodyReadRest state)))
|
||||
(_ : readBodyMore sock remaining accRev)
|
||||
(isZero? remaining))
|
||||
|
||||
validateBodyLength = (expected body rest :
|
||||
let actual = length body in
|
||||
lazyBool
|
||||
(_ : pure (ok body rest))
|
||||
(_ :
|
||||
pure
|
||||
(err
|
||||
400
|
||||
(append
|
||||
"body length mismatch expected="
|
||||
(append
|
||||
(showNumber expected)
|
||||
(append
|
||||
" actual="
|
||||
(showNumber actual))))))
|
||||
(equal? actual expected))
|
||||
|
||||
readBody = (sock headers initialBytes :
|
||||
matchResult
|
||||
(status msg :
|
||||
pure (err status msg))
|
||||
(maybeLen rest :
|
||||
lazyMaybe
|
||||
(_ : pure (ok t initialBytes))
|
||||
(n :
|
||||
onOk (readBodyExact sock n initialBytes)
|
||||
(body rest :
|
||||
validateBodyLength n body rest))
|
||||
maybeLen)
|
||||
(contentLength headers))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Request validation
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
validMethod? = (method :
|
||||
lazyBool
|
||||
(_ : true)
|
||||
(_ :
|
||||
lazyBool
|
||||
(_ : true)
|
||||
(_ :
|
||||
lazyBool
|
||||
(_ : true)
|
||||
(_ : false)
|
||||
(equal? method "HEAD"))
|
||||
(equal? method "POST"))
|
||||
(equal? method "GET"))
|
||||
|
||||
validVersion? = (version :
|
||||
lazyBool
|
||||
(_ : true)
|
||||
(_ : equal? version "HTTP/1.0")
|
||||
(equal? version "HTTP/1.1"))
|
||||
|
||||
validTarget? = (target :
|
||||
startsWith? "/" target)
|
||||
|
||||
validateRequest = (method target version headers :
|
||||
lazyBool
|
||||
(_ :
|
||||
lazyBool
|
||||
(_ :
|
||||
lazyBool
|
||||
(_ : ok t t)
|
||||
(_ : err 400 "Bad Request\n")
|
||||
(validTarget? target))
|
||||
(_ : err 505 "HTTP Version Not Supported\n")
|
||||
(validVersion? version))
|
||||
(_ : err 400 "Bad Request\n")
|
||||
(validMethod? method))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 11. Handler pipeline
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
routerMethod = (method :
|
||||
lazyBool
|
||||
(_ : "GET")
|
||||
(_ : method)
|
||||
(equal? method "HEAD"))
|
||||
|
||||
respondAndClose = (sock resp :
|
||||
onOk_ (finally (send sock resp) (closeSocket_ sock)) (_ :
|
||||
pure (ok t t)))
|
||||
|
||||
handleReadableRequest = (router client method target headers rest3 :
|
||||
onResult (readBody client headers rest3)
|
||||
(status msg :
|
||||
respondAndClose client
|
||||
(responseForMethod method
|
||||
(errorResponse status msg)))
|
||||
(body rest :
|
||||
respondAndClose client
|
||||
(responseForMethod method
|
||||
(router (routerMethod method) target headers body))))
|
||||
|
||||
handleParsedHeaders = (router client method target version rest2 :
|
||||
matchResult
|
||||
(code bad :
|
||||
respondAndClose client (badRequestResponse "Bad Request\n"))
|
||||
(headers rest3 :
|
||||
matchResult
|
||||
(status msg :
|
||||
respondAndClose client
|
||||
(responseForMethod method (errorResponse status msg)))
|
||||
(ignored rest :
|
||||
handleReadableRequest router client method target headers rest3)
|
||||
(validateRequest method target version headers))
|
||||
(parseHeaders rest2))
|
||||
|
||||
handleParsedRequest = (router client req rest2 :
|
||||
((method :
|
||||
((target :
|
||||
((version :
|
||||
handleParsedHeaders router client method target version rest2)
|
||||
(snd (snd req))))
|
||||
(fst (snd req))))
|
||||
(fst req)))
|
||||
|
||||
httpHandler = (router client peer :
|
||||
onResult_ (recvHeaders client)
|
||||
(status :
|
||||
respondAndClose client
|
||||
(badRequestResponse "Bad Request\n"))
|
||||
(raw :
|
||||
matchResult
|
||||
(code bad :
|
||||
respondAndClose client (badRequestResponse "Bad Request\n"))
|
||||
(req rest2 :
|
||||
handleParsedRequest router client req rest2)
|
||||
(parseRequestLine raw)))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 12. IO-aware handler pipeline
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
handleReadableRequestIO = (routerIO client method target headers rest3 :
|
||||
onResult (readBody client headers rest3)
|
||||
(status msg :
|
||||
respondAndClose client
|
||||
(responseForMethod method
|
||||
(errorResponse status msg)))
|
||||
(body rest :
|
||||
bind (routerIO (routerMethod method) target headers body) (resp :
|
||||
respondAndClose client (responseForMethod method resp))))
|
||||
|
||||
handleParsedHeadersIO = (routerIO client method target version rest2 :
|
||||
matchResult
|
||||
(code bad :
|
||||
respondAndClose client (badRequestResponse "Bad Request\n"))
|
||||
(headers rest3 :
|
||||
matchResult
|
||||
(status msg :
|
||||
respondAndClose client
|
||||
(responseForMethod method (errorResponse status msg)))
|
||||
(ignored rest :
|
||||
handleReadableRequestIO routerIO client method target headers rest3)
|
||||
(validateRequest method target version headers))
|
||||
(parseHeaders rest2))
|
||||
|
||||
handleParsedRequestIO = (routerIO client req rest2 :
|
||||
((method :
|
||||
((target :
|
||||
((version :
|
||||
handleParsedHeadersIO routerIO client method target version rest2)
|
||||
(snd (snd req))))
|
||||
(fst (snd req))))
|
||||
(fst req)))
|
||||
|
||||
httpHandlerIO = (routerIO client peer :
|
||||
onResult_ (recvHeaders client)
|
||||
(status :
|
||||
respondAndClose client
|
||||
(badRequestResponse "Bad Request\n"))
|
||||
(raw :
|
||||
matchResult
|
||||
(code bad :
|
||||
respondAndClose client (badRequestResponse "Bad Request\n"))
|
||||
(req rest2 :
|
||||
handleParsedRequestIO routerIO client req rest2)
|
||||
(parseRequestLine raw)))
|
||||
161
lib/io.tri
Normal file
161
lib/io.tri
Normal file
@@ -0,0 +1,161 @@
|
||||
!import "prelude" !Local
|
||||
!import "patterns" !Local
|
||||
|
||||
-- IO constructors for host-interpreted interaction trees.
|
||||
-- Free-monad style: Bind is the single sequencing mechanism.
|
||||
|
||||
version = 1
|
||||
|
||||
io = action : pair "tricuIO" (pair version action)
|
||||
|
||||
pure = x : pair 0 x
|
||||
bind = action k : pair 1 (pair action k)
|
||||
|
||||
putStr = s : pair 10 s
|
||||
getLine = pair 11 t
|
||||
|
||||
readFile = p : pair 20 p
|
||||
writeFile = p c : pair 21 (pair p c)
|
||||
putBytes = bs : pair 12 bs
|
||||
writeBytes = p c : pair 22 (pair p c)
|
||||
|
||||
listDirectory = p : pair 23 p
|
||||
renameFile = old new : pair 24 (pair old new)
|
||||
createDirectory = p : pair 25 p
|
||||
deleteFile = p : pair 26 p
|
||||
fileExists = p : pair 27 p
|
||||
|
||||
sha256Hex = bs : pair 28 bs
|
||||
currentTime = pair 29 t
|
||||
|
||||
ask = pair 30 t
|
||||
local = f action : pair 31 (pair f action)
|
||||
|
||||
get = pair 40 t
|
||||
put = s : pair 41 s
|
||||
|
||||
fork = action : pair 60 action
|
||||
await = handle : pair 61 handle
|
||||
yield = pair 62 t
|
||||
sleep = ms : pair 63 ms
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Generic sequencing combinators
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
thenIO = a b : bind a (_ : b)
|
||||
mapIO = action f : bind action (x : pure (f x))
|
||||
void = action : bind action (_ : pure t)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Conditional execution
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
when = cond action : matchBool action (pure t) cond
|
||||
unless = cond action : matchBool (pure t) action cond
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Infinite loop
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
forever = y (self : action :
|
||||
bind action (_ :
|
||||
self action))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Result-aware combinators
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Propagate driver Result on error; run okCase on success.
|
||||
onOk = action okCase :
|
||||
bind action (result :
|
||||
matchResult
|
||||
(err rest : pure result)
|
||||
okCase
|
||||
result)
|
||||
|
||||
-- Same as onOk, but the okCase only receives the value (rest is dropped).
|
||||
onOk_ = action okCase :
|
||||
bind action (result :
|
||||
matchResult
|
||||
(err rest : pure result)
|
||||
(val _ : okCase val)
|
||||
result)
|
||||
|
||||
-- Generalized Result handler with explicit branches.
|
||||
onResult = action errCase okCase :
|
||||
bind action (result :
|
||||
matchResult errCase okCase result)
|
||||
|
||||
-- Same as onResult, but handlers only receive the value/msg (rest is dropped).
|
||||
onResult_ = action errCase okCase :
|
||||
bind action (result :
|
||||
matchResult
|
||||
(err _ : errCase err)
|
||||
(val _ : okCase val)
|
||||
result)
|
||||
|
||||
mapErrIO prefix action =
|
||||
onResult_ action
|
||||
(e : pure (err (append prefix e) t))
|
||||
(v : pure (ok v t))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Convenience helpers
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
print = s : void (putStr s)
|
||||
putStrLn = s : void (putStr (append s "\n"))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Result-aware file helpers
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
onReadFile = path : onResult (readFile path)
|
||||
|
||||
onWriteFile = path contents : onResult (writeFile path contents)
|
||||
|
||||
onListDirectory = path : onResult (listDirectory path)
|
||||
onRenameFile = old new : onResult (renameFile old new)
|
||||
onCreateDirectory = path : onResult (createDirectory path)
|
||||
onDeleteFile = path : onResult (deleteFile path)
|
||||
onFileExists = path : onResult (fileExists path)
|
||||
onSha256Hex = bs : onResult (sha256Hex bs)
|
||||
onCurrentTime = onResult currentTime
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Convenience helpers for the common cases
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
readFileOrPrintError = (path okCase :
|
||||
onReadFile path
|
||||
(err rest : putStrLn (append "Read failed: " err))
|
||||
okCase)
|
||||
|
||||
writeFileOrPrintError = (path contents okCase :
|
||||
onWriteFile path contents
|
||||
(err rest : putStrLn (append "Write failed: " err))
|
||||
okCase)
|
||||
|
||||
copyFile = (src dst :
|
||||
onResult (readFile src)
|
||||
(err rest : putStrLn (append "Read failed: " err))
|
||||
(contents rest :
|
||||
onResult (writeFile dst contents)
|
||||
(err rest : putStrLn (append "Write failed: " err))
|
||||
(_ _ : pure t)))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Resource-safe combinators
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
finally = action cleanup :
|
||||
bind action (result :
|
||||
bind cleanup (_ :
|
||||
pure result))
|
||||
|
||||
bracket = acquire release use :
|
||||
bind acquire (resource :
|
||||
bind (use resource) (result :
|
||||
bind (release resource) (_ :
|
||||
pure result)))
|
||||
30
lib/lazy.tri
Normal file
30
lib/lazy.tri
Normal file
@@ -0,0 +1,30 @@
|
||||
!import "base" !Local
|
||||
!import "list" !Local
|
||||
|
||||
lazyBool = (thenK elseK cond :
|
||||
((chosen : chosen t)
|
||||
(matchBool
|
||||
thenK
|
||||
elseK
|
||||
cond)))
|
||||
|
||||
lazyList = (nilK consK xs :
|
||||
((chosen : chosen t)
|
||||
(matchList
|
||||
nilK
|
||||
(h r : (_ : consK h r))
|
||||
xs)))
|
||||
|
||||
lazyMaybe = (noneK someK m :
|
||||
((chosen : chosen t)
|
||||
(matchMaybe
|
||||
noneK
|
||||
(x : (_ : someK x))
|
||||
m)))
|
||||
|
||||
lazyResult = (errK okK result :
|
||||
((chosen : chosen t)
|
||||
(matchResult
|
||||
(code rest : (_ : errK code rest))
|
||||
(value rest : (_ : okK value rest))
|
||||
result)))
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user