language.md (13725B)
The tricu language
This is the source contract for the stable compiler shipped with tricu. See Build and run for compiler selection and Language features for practical examples. Modules specifies the directive header and dependency resolution.
Trees and application
Every value is a tree. The constructor forms are t (Leaf), t child (Stem),
and t left right (Fork). Application f x y groups as (f x) y and evaluates
by the Tree Calculus rules. Applying a function is distinct from constructing
Fork function argument.
Functions can receive, return, and inspect other functions. Structural equality observes their compiled trees; function representation depends on the compiler and lowering backend. Equal results on chosen inputs do not imply equal trees.
Evaluation and observation
Execution is non-strict: an application need not evaluate an unused argument, and observing a constructor need not evaluate its children. Local bindings do not themselves force their values. Pattern selection demands only the structure needed by reached tests; an ignored subject or sibling and an unselected body remain unevaluated. A function can consume a finite prefix of a recursively produced tree without consuming the rest.
The Arboricx execution contract names this normal-order. This does not mean
that every invocation first produces a complete normal form. The observer
matters: inspecting a root constructor, decoding a number, rendering a complete
tree, and publishing an executable can demand different amounts of work.
Serialization requires a complete finite tree in normal form; an infinite or
partially defined runtime value is not itself a portable bundle. A finite function
tree that produces such a value when applied can be bundled. Normalizing the
function's representation does not guarantee eager-compatible execution: applying
it to an argument creates new computations whose termination can depend on the
evaluation strategy. Compilation includes normalization, with the construction
limits described below.
The Haskell host implements demand through lazy fields and call-by-need sharing. Physical pointer identity, exact reduction counts, and time/space bounds are not source-language guarantees. Sharing a stored subtree is also distinct from memoizing all applications of that subtree. See compiler architecture for implementation and regression-test boundaries, and profiling for ordinary observation versus explicit full-result forcing.
Source characters and tokens
The lexer consumes a list of Unicode scalar characters. Native source readers strictly decode UTF-8, rejecting malformed, overlong, truncated, surrogate and out-of-range sequences. An initial UTF-8 BOM is rejected, not stripped. No normalization or newline conversion occurs. Strings and comments may contain Unicode scalars; identifier syntax remains ASCII. The browser compiler supplies character lists directly.
- Identifiers start with
[A-Za-z_]and continue with[A-Za-z0-9_?\-'$%.]. Dots permit qualified names such asMath.add. - Reserved words are
t,let,in,where,do,case,of, andrec. - Numbers are nonnegative ASCII decimal integers. Leading zeroes are accepted; values use the canonical compact-natural tree encoding.
- Strings use double quotes and the escapes
\n,\t,\r,\f,\b,\\,\", and\'. Their values are character lists. --starts a line comment outside a name or string.|- ... -|is a non-nesting block comment.- Layout uses ASCII spaces and LF newlines. Tabs produce a located diagnostic.
The lexer produces located tokens. Spans are half-open; positions contain a one-based line and column plus a zero-based absolute character offset. LF moves to column one of the next line. Columns count characters, rather than display cells or encoded UTF-8 bytes.
Unterminated strings and block comments use diagnostic code 5. Invalid escapes
use 6, with the remainder beginning at the offending escape. Unsupported
characters use 4, with that character in found and its position in location.
Expressions
Atoms include names, t, numbers, strings, lists, and grouped expressions.
Application associates left-to-right. Lambdas use (x : body) or
(x y : body); the latter introduces nested binders in source order. A lambda
head occupies one line; its body may begin on a later line.
Lists contain whitespace-separated atoms. [a b] contains two elements;
[(f x)] contains one application. Construct a pair with an imported
pair left right or with t left right. Comma syntax belongs to patterns.
f <| x and x |> f both mean f x. Application binds tighter than pipes,
and pipe chains associate left-to-right: f <| g <| x means (f g) x.
Newlines separate expressions unless the enclosing layout permits continuation. Parenthesize a complete multiline application, including its last argument:
main = (choose
first
second
condition)
Inside parentheses, newlines can separate application operands and precede
where. Inside brackets, they separate list elements. These rules preserve the
original token spans.
Definitions and scope
name x y = body defines a curried function. Native modules contain definitions
and select public bindings with !export. eval selects exported main.
Top-level forward references are dependency-ordered. Every definition is checked for missing names and cycles, including unused definitions. Duplicate names are errors except for contiguous equations forming one function. Native top-level names must be distinct from imported names. Lexical parameters and local bindings can shadow surrounding bindings.
A definition body may begin after = on a later line. If its first token is
indented, further application lines must begin at a deeper column. Same-column
lines start another definition. Missing bodies use diagnostic code 40.
Local bindings
let name args = value in body and body where name args = value bind names
lexically. A plain binding scopes over the body, not its own value. Within a
local block, each group sees outer bindings and earlier groups. The enclosing
expression sees all groups.
A block beginning on a new line is indented beyond the enclosing expression's
layout column and beyond column one. Same-column definitions continue the block;
deeper lines continue a body; dedenting ends the block. in closes a let block.
A where clause may begin on the following line. Its value may contain another
binding; each expression has one where suffix.
Stable backend selection
Stable lowering uses normal-form-preserving abstraction. Constructor applications of represented literal trees may fold; potentially executing applications remain residual until abstraction is complete.
A top-level function referring lexically to itself receives a generated self binder and delayed fixed point. Named parameters, explicit lambdas, and patterned equations support this direct recursion. Self detection respects parameter, local-binding, and pattern-capture shadowing.
!export copy
copy [] = []
copy [head | tail] = t head (copy tail)
copy _ = []
Recursive values such as x = x use diagnostic 48, with expected field
recursive function requires parameters. Mutual definition cycles use 31.
Local self-recursion is explicit, as described below.
Constructing a function tree and executing it are separate operations. A published function may diverge when applied. Compilation itself can also diverge while constructing arbitrary closed computations or partial applications; use host time and memory budgets. Backend-sensitive tree observations must identify the selected compiler.
Patterned equations
Contiguous equations with the same name form one definition. They have positive, identical arity and select in source order. Argument patterns are inspected left-to-right. A miss discards that equation's captures and tries the next one; a selected body executes without backtracking. The final equation must have only irrefutable argument patterns.
| Form | Meaning |
|---|---|
_ |
Ignore the value. |
name |
Capture the value. |
t, (t child), (t left right) |
Inspect a constructor. |
| Number or string literal | Match the exact literal tree. |
^name |
Match the tree of a surrounding binding, including a qualified name. |
(left, right) |
Match a Fork as a pair. |
[], `[head |
tail], [a b c]` |
whole{pattern} |
Capture the original value and match its structure. |
Captures belong to one equation and may shadow outer names. Repeated captures are errors. Pins resolve outside that equation's captures; a capture colliding with a referenced pin name is an error. Only reached pin tests evaluate the referenced value.
Braces enclose one complete pattern and implicitly group constructors and pairs:
whole{t child}, whole{left, right}, and whole{[head | tail]} are valid.
Nesting is allowed; whole{_} is irrefutable. Whitespace before { is insignificant.
Outside braces, f t child has two patterns, while f (t child) has one.
Comments and blank lines preserve a same-name group. A different name ends it; returning to an earlier name is a redefinition error. Grouped patterns treat internal newlines as trivia. Equation groups also work in sequential local blocks.
Boolean patterns are exact: false is t, and true is t t. Library eliminators
such as matchBool use their own truthiness convention. Preserve that distinction
when replacing an eliminator with patterns.
Expression matching and explicit local 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)
case subject of introduces a newline and indented arms. Arms align at one
column and contain one pattern, =, and a body. Deeper lines continue the body;
dedenting or an enclosing delimiter ends the arms. Group a case expression used
as an application argument, and group multiline subject applications.
Arms use equation matching, including final irrefutability and capture/pin checks. The subject resolves in the surrounding scope before arm bodies; captures are arm-local. Every body is checked, including unreachable ones. An initial wildcard can discard the subject without inspecting its root. Unselected bodies stay unevaluated during selection.
let rec and where rec introduce exactly one contiguous self-recursive function
group. Named parameters, patterned equations, and explicit lambdas qualify. Its
name scopes over both its value and enclosing body. Outer lexical values remain
captured; parameters and pattern captures may shadow the recursive name. Nested
recursive bindings are allowed. Recursive values are rejected before free-name
analysis, even when unused.
Plain locals remain sequential and nonrecursive: the f in
let f x = f x in body refers to an outer f, or produces an unresolved-name
error. Mutually recursive groups require a different authored representation.
Sequencing
do bindOperator supplies the sequencing operator explicitly. Each non-final
statement becomes an application of that operator to an action and a continuation.
name <- action binds one name over later statements. A non-final expression
discards its result; the last statement is an expression. A single-expression
block returns that expression directly.
The operator is an atom, such as a name, qualified name, or grouped expression.
The first statement starts past column one; subsequent statements align with it.
Deeper lines may continue expressions. A where at the statement column belongs
to the final statement, rather than to the whole block.
Choose an operator with the correct continuation protocol. IO bind passes one
action result; onOk_ unwraps a successful Result; parser parserBind passes one
parsed value. bindResult passes both value and remainder, so adapting it to a
one-value binding requires an explicit choice about the remainder.
Contracts
x@contract checks an argument, and =@contract checks a result. A successful
annotated call returns the checked plain value; failure is an in-band Result.
Compound contract expressions are parenthesized. Named and phantom annotation
forms have separate binding rules. Annotations belong to ordinary definitions,
rather than patterned or multi-equation heads. See Contracts for calling conventions,
transformed values, higher-order proxies, and effects.
Source formatting
The formatter aligns column-one groups with at least two same-name, same-arity,
positive-arity definitions and single-line heads and bodies. It changes only
spaces between the name, complete argument-pattern cells, and =. Cell spelling
and the suffix starting at = remain verbatim. Columns use character widths
and at least one separating space.
Comments and blank lines stay in place. A multiline or unsupported member leaves the whole group unchanged. Local definitions, singleton groups, zero-argument bindings, and inconsistent arities are preserved. Tabs and comments in gaps are preserved; malformed lexing returns the original source. Header masking is used only for recognition. Line endings and final-newline presence survive formatting.
The formatter uses located syntax without resolving names or requiring complete equation coverage. See Tools for preparation and use.