language-features.md (16190B)
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, not a predicate or expression guard. |
| 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, not types or automatic host exceptions. |
| Inspect code as data | Tree constructors, triage, structural equality/inspection |
Equal application results do not imply equal function 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, not additional kernel primitives.
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 outer bindings and earlier groups, not itself or later 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.
Newlines can end applications. Parenthesize a complete multiline application;
do not depend on a visually unfinished call continuing onto the next line.
Use spaces, not tabs, 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. <- currently accepts one name, not a destructuring pattern.
Choose the operator according to its actual protocol:
| Operator | Continuation / behavior |
|---|---|
bind from io |
One action result; constructs host-interpreted IO sequencing. Does not itself unwrap a Result. |
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; not directly interchangeable with a one-value do operator. |
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; it does not validate arbitrary trees. 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 line), not UTF-8 bytes or display cells. 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.ioincludes Result-aware handlers, cleanup helpers, state/environment and concurrency actions. Current-sourcebracketrequires Result-returning acquisition: failures skip use/release; success supplies the resource value.finallypreserves the action's ordinary result and ignores the cleanup result. Neither guarantees cleanup after abort, divergence or host exceptions. These are explicit interaction trees; real effects need the Haskell IO driver and permissions. Consult host ABI.- 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, not imports.
!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.