tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

README.md (12375B)


Effect-interface comparison fixture

This is an opt-in, source-level experiment, not a supported library API. It compares closure-backed service bundles (a* exports), tree-rewriting handlers (b* exports), environment-interpreted handler scopes (c* exports), and fused routing/execution (d* exports). All use the same physical request protocol and physical IO filesystem adapter. A/B/C share a scripted evaluator; D incorporates that evaluator's rules into its routing machine. B/C/D reuse identical application functions. B is retained only for semantic regression checks, not as a performance candidate. No compiler or host implementation changes are required. Files are scoped resources; the experimental effect protocol itself remains sequential. Its filesystem checks include real OS denial and ensure existing artifacts do not produce another log.

From the repository root:

nix build .#
python3 test/effect_interfaces.py result/bin/tricu \
  --directory /tmp/effect-comparison --benchmark

The output directory must not exist. Omitting --benchmark still runs the behavioral and filesystem checks. The runner compiles the checked-in source using the packaged stable compiler, imports the bundle into an isolated store, and invokes exports in fresh host processes. report.json records source and bundle digests, checks, and benchmark medians; samples.json and invocation logs retain individual measurements. All generated artifacts stay in that directory. This is not compiler-generation acceptance or a general effect-system benchmark.

Protocol and scope

The experimental tree tags are distinct from host IO and base.exceptE:

Replies use the ordinary base Result representation with empty remainders. Operations are existence (1), write (2), clock (3), log (4), and read (5). Logical instances are default store (1), clock (2), logger (3), source store (4), and destination store (5). Physical instances are stores 11/12, clock 20, logger 30. These integers are routing labels, not fresh identities or security tokens.

The B handler fixture recursively transforms trees. Matching requests are lowered to physical requests outside that handler; their continuations resume inside it. Nonmatching requests retain the handler around their continuations. The innermost matching handler wins. Handler ordering therefore matters. The logging interposer must be inside the store-lowering handler to see logical store requests. There is no multishot, resource-scope, or concurrency API.

C interprets scope nodes with an immutable environment and continuation stack. It normalizes only up to the next physical request, which the existing backend then executes. Each stack frame saves a continuation and its caller environment; scope exit restores the previous environment using an identity continuation. Handler lookup returns both the handler and the environment suffix outside it. A handler's own actions run in that outer environment, excluding itself and any intervening inner scopes. Its request continuation resumes in the original caller's environment. This preserves B's lexical routing semantics, rather than silently substituting dynamic lookup for handler-emitted requests.

Lookup is linear in active handler depth. Normalizing binds does not traverse all handlers; installing a scope does not rewrite its body. C is a source-level interpreter experiment, not a new host primitive or a constant-time dispatcher. Constructors are trusted: neither representation validates arbitrary supplied handler functions. Returning an inert action from a scope does not automatically capture that scope; explicitly wrapping it in a handler is a separate operation.

D combines environment routing and execution in one machine. It consumes scripted physical requests directly instead of building normalized request trees for a second interpreter. The IO variant invokes a supplied physical adapter directly. Its handler lookup, scope frames, and handler-emission environment are unchanged from C. The scripted implementation carries script/trace state through the fused machine; fewer intermediate constructors do not by themselves promise less allocation after compilation to tree calculus.

A service bundle has exactly three fields: store, clock, and logger functions. Constructed publishers capture this bundle. The positional representation is intentionally small, not a recommendation for a public record API. Missing services use an explicit failing proxy; a malformed bundle shape is tested separately. Arbitrary malformed callable fields are not dynamically recognized as functions or made safe by this representation.

The fixed clock response is 42 and the read response is 7. This finite response validation is an experimental domain contract, not a general time or storage codec. Existence accepts 0/1; writes and logging accept 1. Malformed response shapes and out-of-domain values terminate with a diagnostic. Failure responses propagate, without rolling back prior requests.

Diagnostic codes are 80 (scripted operational failure), 90 (invalid response, with instance/operation), 91 (missing bundle service, with role/operation), 92 (script request mismatch), 93 (script exhaustion), 94 (invalid evaluator input), and 95 (unhandled request). A-side response diagnostics name the physical instance; B/C/D-side diagnostics name the logical role. These are structured test values, not finished human-facing diagnostics.

Behavioral controls

The Python runner constructs independent complete-tree expectations for all implementations, including remaining script and ordered trace. It does not use one implementation as the other's oracle. Cases include:

For the contextual callback, A supplies an unconfigured callback with new services; B/C/D supply an ordinary request-producing callback under a handler. For stable store identity, A captures its services and B/C/D wrap its callback's computation in the original store interpretation. B/C/D pin only the store in this fixture; A's captured bundle also pins clock/logger. The comparison checks store routing, not equivalence of those broader dependency-binding choices.

The filesystem smoke uses real permission-checked reads/writes and the same fixed clock. It verifies denied access creates nothing, publication writes byte 7 and the expected log, and an existing artifact causes neither a rewrite nor another log. It is deliberately not an atomic or concurrent publication protocol.

Measurement contract

Benchmarks cover 10/100 clock requests and 10/100 complete publications of distinct artifacts, each with 0/4/16 unrelated handler scopes. All publications are scripted as new artifacts and perform existence/write/clock/log operations. There are three fresh-process invocations per cell; variant order rotates across repeats to reduce systematic timing bias. A's depth argument is a control: captured services do not traverse unrelated scopes. C/D traverse the same linear routing environment during request lookup. Only A/C/D are benchmarked; B remains covered by the behavioral controls. Median time and allocated bytes cover invocation through complete rendered output, including response validation, script matching, trace construction, and rendering, but excluding bundle load. Allocated bytes are allocation volume, not peak live memory.

This measures the supplied implementations. In particular, growth with B's handler depth does not establish an inherent cost of handler-scoped APIs: an environment interpreter removes per-handler bind rewriting but retains linear lookup and scope/continuation bookkeeping. Shared backend validation, script matching, and output costs remain in all measurements. No production default should be selected from timing alone.

Artifact-server request slice

python3 test/effect_server.py result/bin/tricu \
  --directory /tmp/effect-server --benchmark

server.tri adapts the production GET /_arboricx/bundle/hash/HASH path. It imports the real server's hash contract, path builder and prefix parser, and its HTTP response constructors. Invalid hashes return 400 before storage, reads return bundle bytes on success, and read failures map to redacted 404s. Read-Result remainders are deliberately discarded; arbitrary Result payloads are not made safe by adding handlers.

The experiment adds completion logging of [requestContext [hash status]]. This experimental log format is separate from production access logging. Logger failure terminates the experiment's action rather than pretending that the response was delivered. Only the selected bundle-GET route logs; unmatched routes retain the production 404 response without completion logging. Ops 6/7 represent raw read and log Results and do not use the microbenchmark's finite response codecs.

The captured-service version constructs a two-field request-specific bundle of reader and logger functions and closes over it in the router. The handler version installs storage and logging context around a router with no service arguments. Both reuse the same readBundle validation/reply-mapping helper. Ordinary, overridden, and pinned-store modes are exercised. The dual-component case uses both a pinned primary-store reader and a contextual alternate-store reader under one request context. The service version shares its context logger while choosing reader closures; the handler version pins only the first component's store.

The runner independently constructs complete HTTP response bytes, log events, and scripted request traces. Checks include binary response bodies, malformed read/log replies, ignored read remainders, invalid/traversal-like hashes, unmatched methods/targets, logger failure, and context isolation across sibling requests. Actual filesystem runs independently check success, malformed hashes, missing files, OS permission denial, and failed-log attempt retention. The old differential execution of the v1 route is retired; expectations are complete Python-built responses and events, not results from another interpreter. File reads use a 64-KiB budget. OS denial tests require a non-root process and restore directory permissions in finally; absence of grant flags is no longer a denial policy. The raw physical adapter assumes its application has validated hashes; it is not a security boundary for hostile hand-constructed requests.

Server benchmarks cover 1/20 requests, with per-request scope/service setup and three fresh-process observations per variant. Every complete HTTP response is returned and rendered, including intermediate responses: discarding those values would allow lazy evaluation to skip response construction. Scripted runs also render complete traces. Filesystem runs use warm local reads and explicitly thread log state through each source interpreter, then render responses and logs. There is no native mutable state slot, implicit state inheritance, or shared child-task state. There is no listening socket, network latency, scheduler contention, streaming, or server-throughput claim.

The local workspace selects experiment imports to these fixtures and existing production libraries. Neither runner modifies production server source, compiler artifacts, or the host ABI.