language-features.md (19798B)
Language features
tricu combines functions, structural patterns, local bindings, and explicit sequencing. This guide shows the tools available when writing programs with the included stable compiler.
Language specifies syntax and semantics; Modules covers imports and exports. Use Build and run to select the compiler and Testing when changing implementations.
Select the stable compiler
Use bootstrap/build-stable.arboricx with the Haskell host for native applications.
It supports direct top-level self-recursion and explicit local self-recursion.
Native modules declare their dependencies and exports. The lower-level unit API
receives a definition body and resolved external trees instead.
Function representations remain inspectable and backend-sensitive. See Compiler architecture for lowering, construction costs, and the separate optimized backend.
Choose a tool
| Need | Available tool | Important boundary |
|---|---|---|
| Traverse or scan | Direct self-recursive functions; let rec / where rec; ordered equations |
Plain locals remain nonrecursive; no mutual groups. |
| Inspect a tree or positional structure | Constructor, pair, and list patterns in arguments or case |
Pairs match Forks; lists match empty, cons, or exact-list shapes. |
| Keep the original value while inspecting it | whole{pattern} |
Captures are equation/arm-local. |
| Compare against an existing value | ^name, including a qualified name |
Exact tree equality. |
| Name intermediate work | let / where, including local equation groups |
Plain groups are sequential/nonrecursive; rec marks one self-recursive function. |
| Sequence computations | do bindOperator |
Operator is explicit; its continuation receives one bound value. |
| Make an application pipeline readable | `value | > function, function < |
| Branch without pattern equations | Library eliminators and lazy continuations | Handler order/truthiness depends on the actual helper. |
| Compose failure-aware work | Result/Maybe helpers, parser combinators, IO helpers | They have different continuation/remainder protocols. |
| Check a runtime boundary | Contracts and argument/result annotations | Runtime checks with explicit failure handling. |
| Inspect code as data | Tree constructors, triage, structural equality/inspection |
Functions with equal application results can have different trees. |
| Organize/share code | Explicit native imports, namespaces, exports, and reexports | No implicit prelude or registry lookup. |
Functions and tree values
name x y = body defines a curried function; (x y : body) is a lambda.
Functions can capture lexical bindings, be passed as arguments, and be returned.
Application f x y means (f x) y; helper names such as pair, true, if
and bind are library bindings.
The kernel shapes are t (Leaf), t child (Stem), and t left right (Fork).
triage and structural equality make these representations observable, including
compiled functions. Prefer ordinary data encodings when that is all a program
needs, but do not erase deliberate intensional behavior during a rewrite.
Patterns and direct recursion
Available argument patterns:
_(ignore), a name (capture),t,(t child),(t left right);- numeric and string literals;
^namefor a value in the surrounding scope; (left, right),[],[head | tail], exact lists[a b c];- whole captures such as
original{[head | tail]}ororiginal{left, right}.
Equations must be contiguous, have the same positive arity, and end with an all-irrefutable equation. They select in source order, inspecting arguments left-to-right. A failed pattern advances to the next equation; execution of a selected body does not backtrack. Repeated capture names are errors, not unification. Pins resolve outside the equation's captures; pin/capture name collisions are errors. Ordinary currying/partial application remains available.
Use argument patterns to replace chains of fst/snd and shape-dispatch helpers.
Use case value of with indented pattern = body arms to match an intermediate
result directly. A final irrefutable arm is required; captures are arm-local. Exact Boolean matching can
use ^true / ^false when those names are imported, rather than spelling their
constructors. That is not the same as the library's broader truthiness.
Top-level functions support direct self-recursion, including patterned equations
and explicit lambdas. Locally, mark one function group with let rec or
where rec; its name then scopes over both value and body, capturing outer
bindings. Recursive values and mutually recursive groups are rejected. Plain
locals retain sequential, nonrecursive scope; explicit fixed-point helpers remain available.
Evidence: pattern corpus,
pattern demo,
HTTP scanners, and test/Spec.hs stable-recursion tests.
Local bindings, layout, and pipes
let name args = value in body and body where name args = value bind names
lexically. Indented local blocks can contain multiple equation groups: each
sees only outer bindings and earlier groups. Local names
can shadow outer names. Native top-level names cannot collide with imports.
Application is left-associative and binds tighter than pipes. f <| x and
x |> f both mean f x. In particular, f <| g <| x means (f g) x, not
f (g x); group the latter explicitly.
Continue an application on lines indented beyond its owning definition or
statement. The first operand may follow = on the next line; later operands
can align with it. Same-column siblings and dedents end the expression. Extra
indentation does not group arguments: use parentheses for a nested application.
Use spaces for source layout. Lists are whitespace-separated atoms:
[a b] has two items; [(f x)] has one application item. Commas are not list
separators. Pair-pattern syntax is not a general tuple-expression constructor:
construct pairs with pair left right (imported) or t left right.
-- starts a line comment; |- ... -| is a non-nesting block comment. Numbers
are nonnegative decimal literals using the language's compact tree encoding;
strings encode character lists. Do not infer arbitrary text/byte interchangeability
from ASCII examples; use the relevant codec/API contract.
Evidence: source parser,
equation parser, and test/Spec.hs local-binding,
pipe-precedence and layout tests. The formatter
aligns eligible equation heads.
Explicit sequencing operators
do bindOperator lowers each non-final statement to an application of that
operator and a continuation. For example, x <- action followed by more work
becomes bindOperator action (x : remainingWork). The explicit operator is an
atom (a name, qualified name or grouped expression).
Statements align at one indented column; deeper lines may continue an expression.
A binding scopes only over later statements. A non-final expression discards its
result; the final statement must be an expression. A one-expression block returns
that expression without invoking the operator. There is no implicit pure or
return. <- accepts a single name.
Choose the operator according to its actual protocol:
| Operator | Continuation / behavior |
|---|---|
bind from io |
Passes one action result intact to its continuation; constructs host-interpreted IO sequencing. |
onOk_ from io |
Unwraps successful action Results to one value; propagates failures; deliberately drops the success remainder. |
bindM / bindE from base |
One value in the pure interaction-tree layer; use runM to interpret it. Not a host IO interpreter. |
maybeBind from base |
One successful Maybe value; propagates nothing. |
bindResult from base |
Calls its continuation with value and remainder; one-value do requires an adapter. |
parserBind from parser |
Continuation receives a parsed value and returns the next parser; parser machinery threads stream/progress. |
If a Result pipeline needs do, choose explicitly whether its bound value
includes the remainder or whether an adapter may discard it. Do not silently
lose parser/body suffixes just to shorten the code. Write adapters as ordinary
library functions.
Evidence: do AST lowering,
contract-effects demo,
Result/Maybe helpers, IO helpers, and
parser protocol. Portable do tests in
test/Spec.hs include custom operators, nested blocks, and ignored statements.
Library conventions worth checking before inventing helpers
preludereexportsbase,bytes, andconversions; native modules must import it explicitly. List mapping/folding/filtering, association helpers, Maybe and Result operations already exist.- Result is
ok value restorerr diagnostic rest.matchResultandlazyResulttake failure first, success second; both handlers receive two arguments.matchMaybetakes nothing first, just second.matchBoolandlazyBooltake true first, false second. Inspect the implementation when changing representation handling; malformed-tree behavior is not uniform. lazyBooltakes branch thunks. Patterns can often express the same branch selection directly; preserve the helper's truthiness and demand behavior when replacing it.patternsprovides ordinary librarymatch,cond, andguardfunctions for predicate-based dispatch. These are not expression-level equation matching or language guard syntax. Current-sourcematchrequires exact two-element[predicate action]rows;condconsumesguardpairs of unit thunks. They short-circuit on first truthy success and return Leaf on exhaustion or a reached malformed row. See the predicate-matching demo and implementation for their callback conventions.native_mathprovides direct compact-natural arithmetic/comparison and byte classification (digit,letter,printable,inRange, etc.). Its inputs must be canonical naturals. Compare costs when choosing an arithmetic implementation. Current libraries share this binary engine with base arithmetic, adddivModand exponentiation by squaring, and format decimal numbers by repeated division by ten. The checked base facade rejects zero divisors; the raw kernel has an explicit degenerate zero convention. See numeric boundaries.graphsorts small, caller-validated dependency graphs in earliest-ready input order and reports a name on an actual cycle. Names are opaque trees; uniqueness, proper lists, and dependency existence remain caller obligations. The lower-levelgraphCyclehelper expects a blocked residual graph and a starting name in it, as supplied bygraphSort; it is not a cycle detector for arbitrary graphs. Sorting uses linear membership/removal scans.lexersupplies reusable positioned scans and first-match dispatch. Positions count source characters; LF advances the line. Callers validate character/position domains. Quoted scans start after an opening delimiter; emit/resume adapters assume an outer prefix was already consumed. These helpers do not silently introduce backtracking after a selected failure.parseralready supplies reply diagnostics, consumed/committed flags, sequencing, alternatives, labels, repetition, and rollback. A parser reply is not merely an HTTP Result; preserve its full protocol when adapting it.ioreexportsio: Result-aware sequencing, scoped files, bounded binary chunks, child tasks, consumable joins, supervision, cancellation and ownership handoff, plus staged publication and independent incremental SHA-256 contexts. Borrowedstdin/stdout/stderrand sourcecopyStreamcompose bounded binary pipelines without closing inherited process handles. File readers/writers support absolute positioning and size queries; update, append and resize support bounded edits with explicit access modes and a shared cursor lease (not atomic multi-call transactions). Path metadata, nonrecursive directory initialization/removal, rename/unlink and scoped one-entry directory iteration support store workflows; path resolution and filename encoding follow the host. Numeric IPv4/IPv6 TCP listeners and duplex streams support scoped handoff, independent read/write leases and write half-close. Explicit bounded host resolution and source candidate fallback are separate from connecting; TLS and deadlines are not implied. AuthenticatedreadIndexedTreepreserves DAG sharing using bounded transfer and a scoped disk snapshot before decoding; callers select framing and disk budgets, while decoded graphs still occupy memory proportional to their nodes. Canonical indexed hashing/writing and exact-byte comparison avoid expanded-tree transport; source code still owns publication and whole-file verification policy. File and directory synchronization compose withio_objects, which performs source-owned sharding, no-replace publication, exact winner verification and synchronization before returning object identities. Store roots must preexist; synchronization is subject to host filesystem guarantees. Stateless bounded UTF-8 codecs preserve strict scalar/byte semantics;io_textcomposes text writing and whole-source reads that preserve raw bytes and reject an initial BOM. Whole-source conveniences retain their complete values in memory. Its explicit single-consumer line cursor preserves bounded read-ahead and strict incremental UTF-8, with a caller-selected encoded-byte limit per line. LF/EOF framing is explicit; CR and BOM remain generic text content. Thread the returned cursor forward and retire it on failure. Callers serialize composite operations.io_storagecomposes byte hashing, budgeted whole-file reads, immutable winner verification and mutable alias replacement. Its source-only compiler projection is selected ascompiler_ioby current compiler store callers; see the isolated compiler procedure in the IO validation guide.io_artifactscomposes exact-length artifact publication in source, with verification of existing winners.IO.capabilitiesreports implemented named/versioned facilities and limits;IO.inspectclassifies one action header without executing it or forcing payloads/callbacks (using the chosen import alias forIO). These use OS process authority.handlers.scope,spawnandrunIOinherit lexical services without sharing continuation frames. Stateful collection must be installed inside the child explicitly. See the implemented contract and validation for outcome shapes, cancellation limits, unsupported facilities and backend boundaries.handlerssupplies named request/response scopes and pure/host-IO runners.handler_logsupplies context, sinks and capture;handler_artifactssupplies artifact storage;handler_testingsupplies exact scripts. Service Results remain ordinary computation values.runPurewraps completion in an execution Result;runIO/runIOyield successful values and map handler failures to terminal IO failures. Host scopes own resource cleanup. See scoped handlers.handler_uploadselects the scoped v2 producer/staged-publication composition; see the registry runner. Bundle downloads use authenticated native snapshots and bounded transfers; producer uploads consume bounded chunks. Whole-value artifact APIs remain available for callers needing resident byte lists.- Contract helpers include
checkContract,withContract,guardC, collection/higher-order combinators, andx@contract/=@contractannotations. Contracts and Boolean predicates have different protocols. Annotated success is a plain value, while failure is a Result; annotations are not composable with patterned/multi-equation heads in the current profile. Read contracts before using them.
Native modules
Use !import prelude * for an explicit open import, !import name for qualified
access, or !import name as Alias for a chosen qualifier. Full manifest hashes
can be imported with an explicit alias or *. !export name selects a local
binding; !export * exports local definitions only.
!export * from REF explicitly reexports and acquires that dependency.
The CLI discovers source locations through the nearest tricu.workspace; it
does not search registries or mutable store aliases for source imports. There is
no implicit prelude or fallback to another frontend after compilation failure. Dependency cycles and name/export collisions
are errors. The native profile requires a nonempty public interface and rejects
bare final expressions; export main for CLI evaluation. See the
module contract for exact rules.
Small combined example
This complete native module needs no imports. passAlong demonstrates a custom
sequencing operator: it passes a value directly to the next computation.
!export main
copy [] = []
copy [head | tail] = t head (copy tail)
copy _ = []
marker = 7
isMarker ^marker = 1
isMarker _ = 0
keep whole{left, right} = whole
keep _ = t
firstOr fallback xs = (let
pick [head | _] = head
pick _ = fallback
in pick xs)
identity x = x
piped x = x |> identity |> identity
viaWhere x = local x where local y = y
passAlong value next = next value
sequenced input = (do passAlong
value <- input
identity value
identity value)
main = [(copy [1 2]) (isMarker 7) (isMarker 8) (keep (t 3 4))
(firstOr 99 [5]) (firstOr 99 []) (piped 6)
(viaWhere 8) (sequenced 9)]
The result contains, in order: [1 2], 1, 0, the pair t 3 4, 5, 99,
6, 8, 9. Compile/run through the stable portable route in
tri-tools.
Local expression matching and recursion
firstOr fallback xs = case xs of
[head | _] = head
_ = fallback
copy xs = (let rec
walk [] = []
walk [head | tail] = t head (walk tail)
walk _ = []
in walk xs)
Arms begin on a new indented line and align at one column. Group a case used
as an application argument. Matching reuses equation order, captures, pins and
final-irrefutable coverage; an initial wildcard need not inspect its subject.
Unselected bodies are not run, but every body is checked for valid names.
case, of, and rec are reserved words. See the
language contract
and focused checks.
Extending a program
A local patterned function can name a structural operation; whole captures retain the original input while inspecting it. Pins compare against existing values, and explicit combinators adapt sequencing protocols.
Before replacing a library call, check its argument order, remainder, and failure conventions. Decide which tree identities and malformed-input behavior matter to the caller. Compile a small candidate, check complete results, and measure costs on a useful workload. Follow Testing for candidate validation and publication.