tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

modules.md (32341B)


Modules and packages

A native tricu module declares dependencies and exports, then defines its values. Each exported value is a closed executable tree. A module manifest names those trees, and an Arboricx bundle carries them for distribution.

Write a module

!import prelude *
!import math as M
!export main

main = M.add 20 22

The CLI discovers the nearest tricu.workspace in the input file's directory or its ancestors, independently of the caller's working directory. Without a declaration, dependencies remain sibling NAME.tri files. Module names start with an ASCII letter or underscore and continue with letters, digits, underscores, or hyphens.

Source locations without a build directory

A workspace declaration maps import names to authored source paths. For example, place this at your project root (columns are separated by an actual tab):

tricu.workspace.v1
math<TAB>libraries/math.tri
prelude<TAB>libraries/prelude.tri

Then apps/main.tri can import math and prelude without copying files beside it. Paths are relative to the declaration, must end in .tri, and can include .. for explicitly shared sources. LF endings and a final newline are required; every line must contain the header or a mapping row. Declarations are limited to 64 KiB; names to 128 ASCII characters. All mapped files must exist. Direct symlinks and nonregular input files are rejected; use stable, trusted directories.

The repository declaration provides its native library locations, including native_math and arboricx_server. Compile the application directly:

result/bin/tricu arboricx compile --file tricu-apps/arboricxServer.tri \
  --store "$WORK/application" --output "$WORK/server.arboricx" \
  --compile-timeout 900

Sibling modules remain available. A sibling and mapping with the same name must refer to the same file; conflicting and case-colliding names fail. The nearest declaration supplies the complete mapping. A nearer header-only tricu.workspace establishes an isolated flat workspace; the self-contained module demos use this boundary.

The host copies the selected source locations into a temporary flat workspace and removes it after compilation, including on failure. Source text is unchanged; the portable compiler still reads headers, discovers the reached dependency closure, rejects cycles, and owns compilation and cache semantics. Only reached modules are compiled, though all mapped and validly named sibling files are copied. Source discovery is limited to the declared mappings and siblings; registry fetching is a separate preparation step. Keep sources stable during the copy/build. Full-hash imports and package-generated binding modules work as before.

eval, arboricx compile, and provenance capture share these location rules. Capture retains reached source bytes under their module names; replay uses that flat layout. Executable and receipt identities depend on content, independently of the workspace paths. Use Build and run to select the stable compiler.

Headers

A directive header precedes definitions. Blank lines and -- comments may appear throughout it. Words are separated by ASCII spaces, with LF line layout.

Declaration Effect
!import NAME Acquire NAME.tri and qualify its public exports with NAME.
!import NAME as Alias Acquire the dependency under a chosen qualifier.
!import NAME * Open its public exports.
!import HASH as Alias or !import HASH * Select an immutable manifest by full hash.
!export name Publish a local definition.
!export * Publish all local definitions.
!export * from REF Acquire and re-export that dependency's public interface.
!source NAME Declare a build-only dependency.

Qualifiers use simple ASCII identifiers. Full-hash imports require as Alias or *. Public interfaces contain at least one export; eval selects exported main. A header-only re-export module is valid. !export * selects local definitions, while imported values are re-exported with !export * from REF.

The workspace explicitly supplies every dependency, including the prelude. Repeated imports under different qualifiers share one dependency build. Local edges are deduplicated in first header-occurrence order. Duplicate build-only edges, redundant build-only/import edges, name collisions, missing dependencies, and cycles are errors. Bodies contain definitions; expose the entrypoint through an export.

Resolution and identity

Workspace names resolve through the selected workspace. A source map assigns paths, aliases, frontends, and ordered dependencies for a larger build. Full hashes select immutable manifests. Graph builds resolve workspace names through that map. Store aliases are managed separately.

Imports hydrate and verify the complete public interface. This includes unused exports: namespace validation and cache keys depend on the interface as a whole. Locks pin that interface. Re-exports preserve the exact executable tree hashes.

Different labels can name the same tree. Source or receipt changes can leave an executable identity unchanged. Conversely, functions with the same observed results may have different trees; tricu programs can observe that difference.

Native module objects use arboricx.indexed-tree.v1, and their manifests use arboricx.module-manifest.v1. The CLI's default indexed import supplies native imports, stored execution, and compiler selection without flat expansion. Storage defines the representations, hashes, conversion budget, receipts, and cache rules.

Publication

The tricu object layer verifies content digests on reads. It publishes immutable objects with no-replace writes and publishes individual aliases, locks, and cache indices atomically. File and parent-directory syncing support crash durability on the tested local filesystems. A post-publication sync failure may leave the new name visible; retry and verify it.

A build spans several objects, so an interrupted build can leave completed objects behind. Stabilize source files throughout a build, and use trusted stores whose contents remain stable during verified reads. A receipt records input identities and output references; preserving the corresponding source bytes is part of maintaining a reproducible build.

The lower-level sourceModulePinAlias API explicitly updates a module alias in the neutral alias-file format. This is separate from graph workspace resolution.

Implementations

lib/source/{module,object,lock,build,map,bundle}.tri owns native module policy. The Haskell host supplies primitive IO and typed tree transport. Its content store supports CLI artifact operations; source compilation uses the portable compiler. Compiler architecture describes the build boundary; Testing lists the module and graph probes.

Exact-version packages from trusted registries

The initial package profile is one compiled native module per release. Versions are matched as exact opaque labels. Names and versions contain 1–128 ASCII letters, digits, dots, underscores or hyphens, starting with a letter or digit. Registry policy lives in the Haskell CLI; the native server serves the index and bundles, and native compilation continues to consume local CAS objects.

Publish

Compile a library normally, then publish from an administrator account with direct access to the server's trusted store directory:

result/bin/tricu arboricx compile --file library.tri \
  --store "$WORK/producer" --output "$WORK/library.arboricx"
result/bin/tricu package publish --root "$REGISTRY_ROOT" \
  --name library --version 0.1.0 --file "$WORK/library.arboricx"

Publication validates the bundle and its native interface, writes the bundle before the index record, and refuses to replace an existing name/version with different bytes. Identical retries are allowed, including concurrent publishers. Failure may leave an unreferenced bundle. Publication requires local administrator access to the registry store.

The current lib/arboricx/server.tri serves GET /_arboricx/package/NAME/VERSION from that same store. Compile the updated server source and deploy the resulting bundle before using this route. Use an HTTPS reverse proxy for non-loopback service. A read-only registry deployment should restrict public methods/routes and grant the server only read access; the older bundle-upload route still exists.

Resolve, fetch, bind

Create an explicit registry configuration. This loopback example assumes the native server is listening on port 9050:

printf 'arboricx.registries.v1\nhome\thttp://127.0.0.1:9050\n' > "$WORK/registries"
result/bin/tricu package resolve --registries "$WORK/registries" \
  --registry home --name library --version 0.1.0 --lock "$WORK/library.lock"
result/bin/tricu package fetch --registries "$WORK/registries" \
  --lock "$WORK/library.lock" --store "$WORK/application"
result/bin/tricu package bind --lock "$WORK/library.lock" \
  --store "$WORK/application" --output "$WORK/library.tri"

Configuration rows are tab-separated registry name and origin URL. Names must be unique. URLs are HTTPS origins without trailing slash, path, credentials, query or fragment; HTTP is allowed only for localhost, 127.0.0.1 and [::1]. Each request uses the explicitly configured registry. Redirects are rejected.

resolve alone consults the index. It records registry name/origin, exact version, module-manifest hash and whole-bundle hash. Changing a selection requires --update; a changed mapping for the same locked registry/origin/name/version is rejected even with that flag. This detects changes relative to the current lock. Retain older locks separately to track historical registry claims.

fetch uses only the lock's bundle pin, verifies the download and its advertised module identity before importing into the application CAS, while leaving mutable module aliases unchanged. Its configured origin must still match the lock. A complete, verified local module is reused offline. Corrupt or incomplete objects under an existing manifest fail and require explicit repair.

bind is offline. It verifies the local module and writes a normal workspace module containing !export * from HASH. An application beside that file can use !import library as L and call L.someExport. Use the same application store for compilation. Commit the lock and generated binding; after an explicit update, fetch and regenerate the binding. The compiler reads the source binding and local CAS. Registry resolution and lock updates belong to the package commands. For several dependencies, use the project declaration and consolidated lock below to manage them in one lock.

Trusting a registry accepts its name/version assertions. Establish build provenance separately and grant program IO permissions explicitly. TLS uses the host client trust configuration. Metadata downloads are bounded to 4 KiB, bundles to 64 MiB, and requests to a 60-second overall deadline (30-second response timeout). These limits bound transport. Set heap limits and execution isolation separately; the native server reads/encodes complete files. This profile supports explicitly trusted public-read registries and exact one-module releases. Credential handling, signatures, version solving, multi-module records, and provenance require a separate package profile.

Project dependencies

tricu project prepares a whole dependency set while keeping registry resolution, fetching, workspace binding and compilation separate. Declare each dependency and exact version explicitly, fetch it, and import its generated source binding. Compilation uses local inputs under the ordinary workspace rules above.

A tricu.project file declares local binding, registry label, package name, exact version, separated by tabs, with LF endings and a final newline:

arboricx.project.v1
foundation<TAB>home<TAB>tricu-base<TAB>0.1.0
fileio<TAB>home<TAB>tricu-file-io<TAB>0.1.0
inspection<TAB>home<TAB>arboricx-inspection<TAB>0.1.0

<TAB> above denotes one actual tab. The checked-in inspector declaration is ready to copy. Selection is independent of declaration order; generated locks are sorted by binding name. Bindings use the flat workspace name grammar above, at most 128 ASCII characters, excluding names that would parse as full manifest hashes. Duplicate and case-colliding names are rejected, even on case-sensitive systems. Project metadata is bounded to 64 KiB and 128 direct dependencies. Empty projects are valid. Versions are opaque exact labels: 1.x selects the literal version 1.x. Each row must contain the header or a dependency declaration.

With an explicitly trusted registry configuration whose home entry serves those releases, prepare a fresh workspace (the site releases must be deployed or served locally first):

mkdir -p "$WORK/inspector"
cp demos/package-inspector/main.tri demos/package-inspector/tricu.project "$WORK/inspector/"
PROJECT="$WORK/inspector/tricu.project"

result/bin/tricu project resolve --project "$PROJECT" --registries "$REGISTRIES"
result/bin/tricu project fetch --project "$PROJECT" --registries "$REGISTRIES" --store "$APP_STORE"
# The registry can now be stopped.
result/bin/tricu project bind --project "$PROJECT" --store "$APP_STORE"
result/bin/tricu project check --project "$PROJECT" --store "$APP_STORE"
result/bin/tricu arboricx compile --file "$WORK/inspector/main.tri" \
  --store "$APP_STORE" --output "$WORK/inspector.arboricx"

From within that workspace, omit --project: it defaults to tricu.project. The lock defaults to tricu.lock beside the declaration; --lock FILE overrides it. Bind/check default to that same directory; --directory WORKSPACE overrides where generated modules live. Authored sources stay in their existing locations. Explicit paths are relative to the caller's working directory. Local or global --store selects the application CAS, just as with the other CLI operations.

Frozen resolution and deliberate updates

For example, after changing only the foundation version in the declaration:

result/bin/tricu project update --project "$PROJECT" --registries "$REGISTRIES" \
  --dependency foundation
result/bin/tricu project fetch --project "$PROJECT" --registries "$REGISTRIES" --store "$APP_STORE"
result/bin/tricu project bind --project "$PROJECT" --store "$APP_STORE"
result/bin/tricu project check --project "$PROJECT" --store "$APP_STORE"

Fetching, binding and recovery

fetch first checks the whole declaration/lock and every configured origin. It fetches by locked bundle hashes. Verified complete local modules are reused offline. Transport and executable verification use the same package controls above; an interrupted fetch may leave valid immutable objects available for retry. Corrupt/incomplete existing modules fail and require explicit repair. This verifies local executable availability. Retaining the original distribution bundle’s bytes is a separate responsibility.

bind verifies all required local interfaces and preflights all output paths before changing bindings. It writes normal !export * from HASH modules. A derived .arboricx-project-bindings ledger tracks which files it owns, allowing safe updates and removal of obsolete bindings. Output paths must be fresh or already owned by the ledger; unmanaged files cause rejection even when their bytes match. Edited owned files, direct symlinks, directories and input/output collisions are errors. Unrelated authored files are left alone. To migrate manually generated bindings, explicitly remove those files after preserving any edits, then bind into the clean paths.

Multi-file binding uses journaled, per-file updates. A .arboricx-project-bindings.pending journal is published before any changes; interruption can leave a mixture of prior and pending files. After interruption, rerun bind with the same project/lock; it accepts files from the prior or pending generation but refuses user edits. Finish recovery before another dependency update. If the lock has already changed, restore the interrupted generation's declaration/lock first. Use exclusively owned, trusted workspaces and keep their contents stable throughout bind/check/compile; concurrent updates or hostile directory mutation are unsupported.

check is read-only and offline. It rejects stale declarations, mismatched or edited bindings, pending transactions, missing modules and corrupt objects. The compiler reads ordinary source bindings: run check before compilation when lock/binding agreement matters. The check covers dependency selections and bindings. Track source inventories, compiler pins, and build receipts separately, and configure execution isolation at the host boundary.

Commit the declaration and lock. Either regenerate bindings in fresh workspaces, or commit generated modules and their ownership ledger together. Complete journal recovery before committing. The one consolidated lock stores registry labels/origins, exact releases, module hashes and bundle hashes; the ledger records derived local file ownership.

Explain selections and compare locks

These commands are offline and read-only. They inspect the supplied project metadata and existing local store as data. Store contents, bindings, and caches remain unchanged; missing evidence is reported as unavailable. Registry resolution, fetching, and export execution are separate operations:

result/bin/tricu project explain --project "$PROJECT" --store "$APP_STORE"
result/bin/tricu project diff --before initial.lock --after current.lock --store "$APP_STORE"

explain uses the same declaration/lock/directory defaults as bind/check. It shows declared and locked selections, exact registry origins, full module and bundle pins, binding-file agreement, ownership/journal status, and verified or unavailable local interfaces/exports. Other evidence remains visible even when a declaration or generated file is stale. A missing or malformed declaration/lock produces an input error. Registry claims come from the lock; current registry trust, publisher authentication, and build provenance require independent checks.

diff needs only two consolidated locks and a local store containing evidence for either/both. It separates selection additions/removals/changes, export and ABI changes, object identities, and ordered tree structure for common exports. Unchanged selections are included too. Comparison of unavailable evidence is unknown, including when the locks contain equal hashes. Every relevant local object is authenticated and decoded, including unchanged pins. An absent dependency on one side is distinct from a selected module whose objects are missing.

Structural comparison operates on validated indexed DAG records. Bottom-up interning of constructor/child-ID tuples in a shared table gives exact structural equality directly over stored records. It ignores physical node numbering, unreachable records and irrelevant packaging metadata, while retaining constructor and child order. Work for an export pair scales with stored records (with logarithmic map operations), independently of the number of logical occurrences in the unfolded tree. Normal native package import already canonicalizes exports into name-neutral indexed objects; the inspector acceptance case therefore also has unchanged common-export object hashes. The comparison accepts readable noncanonical indexed encodings and compares their tree structure directly. Normalization and behavioral equivalence require separate analysis. Distinct trees can have identical observed results; tricu can also observe their structure.

The first report profile supports closed nonempty native package modules with indexed-tree exports and arboricx.abi.tree.v1. Unknown kinds/ABIs are explicitly unsupported; available manifest interfaces can still be compared. Corrupt bytes, malformed graphs, missing files, nonregular CAS paths and unsupported profiles are reported as invalid or unavailable evidence. Display fields percent-escape non-ASCII/control bytes and %, so package metadata cannot insert terminal control sequences.

Report commands return 0 when all requested evidence is available/verified (and explain's declaration/bindings/ownership state agree), even when a valid diff finds changes. They return 1 for incomplete/invalid/unsupported evidence or stale project state, printing the available report to stdout. Invalid input files produce a diagnostic instead. Report wording is intended for human readers and may change between releases.

Reads are bounded to 64 KiB per module manifest and 64 MiB of retained authenticated executable bytes per module. Repeated object references within a module share inspection results; report text is materialized per dependency before proceeding to the next. A budget-limited export is reported unavailable. These limits cover encoded bytes; parsed graphs and comparison tables take additional heap space. Use stable, trusted stores and external time/heap controls for large inputs. Filesystem safety relies on that trust and stability. Bundle pins are reported as locked claims; authenticated executable evidence comes exclusively from CAS modules and objects. Authenticate original distribution bundles separately when their bytes are needed.

The inspector acceptance example compares its retained initial.lock with the updated consumer/tricu.lock: foundation gained revision; its three common exports remained structurally identical; fileio and inspection stayed pinned. The acceptance harness separately records whether the rebuilt inspector itself remained byte-identical. Determining a downstream build’s identity requires inspecting its own artifacts.

Retained build inputs and reproduction

The native receipt records input and output identities. A clean rebuild also requires the corresponding source bytes and executable inputs. provenance retains that closure and keeps three assurances distinct: verified bytes, recorded producer claims, and a reproduced build.

First compile with an explicit trusted toolchain, as in build/run. Keep the source workspace stable and use the same application/tool stores for capture:

result/bin/tricu arboricx compile --file "$WORK/inspector/main.tri" \
  --store "$APP_STORE" --toolchain "$COMPILER_MANIFEST" --tool-store "$TOOL_STORE" \
  --output "$WORK/inspector.arboricx"

result/bin/tricu provenance capture --file "$WORK/inspector/main.tri" \
  --store "$APP_STORE" --toolchain "$COMPILER_MANIFEST" --tool-store "$TOOL_STORE" \
  --project "$WORK/inspector/tricu.project" --output "$WORK/inspector-inputs"

Capture prints the input-pack hash. It discovers the native root receipt through the latest cache locator, or accepts an explicit --receipt HASH if that locator is absent or another historical build is desired. Every reached source file must still match its receipt. --project is optional (omit it for a library workspace); its lock defaults to tricu.lock beside the declaration, with --lock FILE as an explicit override. Capture is limited to reached sources, required executable inputs, receipts, and optional project metadata; registry credentials and unrelated checkout files stay outside the pack. Paths are caller-relative; output directories must be new. A failed capture can leave an incomplete directory without a descriptor.

Copy the pack directory to another machine or isolated working directory and transfer its hash through the channel whose assertions you intend to accept:

INPUTS_HASH=PASTE_THE_PRINTED_HASH
result/bin/tricu provenance verify --pack "$PACK" --pin "$INPUTS_HASH"

# This executes the retained compiler. Select its hash from an independently
# trusted source and authorize only a compiler you approve.
result/bin/tricu provenance rebuild --pack "$PACK" --pin "$INPUTS_HASH" \
  --trust-compiler "$COMPILER_MANIFEST" --output "$WORK/fresh-rebuild" \
  --compile-timeout 900

verify is read-only and checks the authenticated descriptor and its referenced closure as data. Its verification scope is that closure; unrelated files alongside it remain unverified. rebuild verifies first, requires explicit compiler authorization, and creates new workspace, application and tools directories. It uses only retained sources and executable inputs. The retained compiler and pack supply all compilation inputs, independently of the registry, producer cache, and host’s packaged compiler.

A successful result reports exact native module and receipt identities plus structural comparison of exported trees, and saves rebuild.txt. The resulting module is runnable from the new application CAS using arboricx run. An exact reproduction returns 0; a mismatch reports available structural differences and returns nonzero. Exact reproduction requires matching module and receipt identities as well as export structure. A compiler/validation failure also returns nonzero and may leave partial local artifacts. Retry into a new directory. Application exports are compared as tree data. The workspace restores input snapshots; project binding ownership must be established separately. A project may also declare unused dependencies intentionally absent from the pack.

The reproduction target is the native module. Recreating a distribution bundle’s export selection and metadata requires those settings separately. The pack retains the compiled compiler; preserve its source provenance, host executable, OS environment, and repository history separately as needed. Supply a compatible Haskell host and transfer the pack as files. Registry discovery, HTTP transport, signatures, and remote attestations require separate services. Before distribution, review source/toolchain contents for secrets and licensing requirements, and supply repository license and origin metadata explicitly.

Use trusted, stable directories. Symlinked internal CAS directories and input files are rejected. Keep contents stable throughout each operation. Limits are 128 reached source units, 1 MiB per source/receipt, 64 KiB per descriptor/project file/module manifest, 64 MiB per executable object, and 256 MiB of retained closure bytes. The shared inspection reader additionally bounds retained executable bytes per module. These bounds cover encoded bytes; runtime heap/RSS requires separate controls. Rebuilding executes the selected compiler under the existing host IO model and compiler deadline. OS/network isolation requires external controls. See storage for the identities and validation for the real-library and false-claim acceptance tests.

First published libraries: an offline bundle inspector

The initial reusable libraries expose the following public interfaces:

Release name Authored interface Exports
tricu-base lib/packages/base.tri append, matchResult, showNumber
tricu-file-io lib/packages/io.tri io, bind, readFile, sha256Hex, putStr, abortIO
arboricx-inspection lib/arboricx/inspect.tri inspectBundle, renderSummary, describeError

Their closed executable exports include the required binary, numeric and parser helpers, so consumers can use the closed exports directly. Grant IO permissions separately when executing IO trees. Each release uses the ordinary one-module package profile above.

demos/package-inspector/main.tri is a real consumer of all three releases. Its tricu.project selects tricu-base as foundation.tri, tricu-file-io as fileio.tri, and arboricx-inspection as inspection.tri. Use project resolve/fetch/bind/check above, then compile that directory using the application store containing the fetched modules; the registry may be offline. The compiler itself still needs its selected toolchain.

After importing the resulting inspector bundle indexed into APP_STORE, run its module manifest:

result/bin/tricu arboricx run --store "$APP_STORE" \
  --module "$INSPECTOR_MANIFEST" --name main --arg "$INPUT_BUNDLE" \
  --io --no-result --allow-read "$INPUT_BUNDLE"

Output is a line-oriented, tab-separated report:

arboricx.inspect.v1
bundle-sha256<TAB>whole-file SHA-256
stored-nodes<TAB>physical record count
export<TAB>name<TAB>root index<TAB>kind<TAB>ABI

The report preserves export order. Fields percent-escape % and bytes outside printable ASCII, including UTF-8, tabs, newlines and terminal controls. It hashes the same in-memory byte snapshot it validates. Validation covers every export and stored node, including unused ones, directly over encoded records. Exported trees remain inert, with sharing preserved. Malformed bundles and file permission/read failures produce a nonzero process exit and suppress the success report. Execution requires only read permission for the input bundle.

inspectBundle returns the library Result protocol: failure is a numeric codec diagnostic; success is (stored node count, export records), with an empty Result remainder. An export record is (name bytes, (u32 root bytes, (kind bytes, ABI bytes))). renderSummary requires that validated success payload; describeError gives a human-readable diagnostic category. Success hides the internal decoder cursor; failure retains the codec remainder as opaque diagnostic context. Incremental parsing requires a separate parser interface. The three listed exports form the complete public interface.

The inspector establishes structural validity and byte identity. Authenticate publishers, verify build provenance, and assess execution safety separately. It reads/parses the whole file in memory; use external process/time/heap limits for untrusted large files. Current formats and supported ABIs follow the underlying native Arboricx reader. The acceptance workflow checks the actual compiled libraries against an independent Haskell decoder, including inspection of the published libraries and the inspector itself.