language.md (15110B)
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. Functions with equal results on chosen inputs can have different compiled trees.
Evaluation and observation
Execution is non-strict: unused arguments, constructor children and locally bound values remain lazy until demanded. 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. The observer
determines demand: 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. A finite function
tree can be bundled even when applying it produces an infinite or partially
defined value. Application 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 depend on the implementation and workload. 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. 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.
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.
Application continuation is relative to the owning definition or statement. A later line continues the expression when its first token is deeper than that owner. These definitions are equivalent:
main = choose first second condition
main = choose
first
second
condition
main =
choose
first
second
condition
Same-column siblings and dedents end the expression. Local-block definitions,
case arms and do statements establish their own boundaries. Application
remains left-associative across indented continuation. Blank lines and comments
preserve the boundary.
Parentheses explicitly group expressions and allow newline continuation without an indentation requirement; a local/case/do block inside them still has its own layout boundary. Inside brackets, newlines separate list atoms. Original token spans are preserved.
A pipe operator or where on a later line may align with its owner or be deeper,
but cannot cross a dedent out of an inner block. For example, a where dedented
past the last case arm belongs to the enclosing case expression.
A pipe operand beginning on a later line must be deeper than the owner unless
explicitly grouped. Standalone expression parsing still does not join bare
newline-separated operands outside parentheses.
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, indented beyond the
definition's name. Subsequent operands need only remain beyond that same owner;
they may align with the first operand. Plain and contract-annotated definitions
use the same rule. Missing or insufficiently indented bodies use diagnostic
code 40, without consuming a following sibling as the body.
The historical first-body-token continuation boundary and unindented next-line body exception are retired. Parentheses are no longer needed merely to continue an indented application.
Local bindings
let name args = value in body and body where name args = value bind names
lexically. A plain binding scopes over the body; its value sees the outer scope. 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. An inline binding inherits the
enclosing expression's layout context; a newline immediately after let or
where opens a separate local block.
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 has two conservative column-one alignment passes:
- Same-name, same-arity equation groups align complete argument-pattern cells
and
=. They require at least two positive-arity, single-line definitions. An unsupported member leaves the whole equation group unchanged. - Adjacent distinct definitions align names, argument tails and
=, including constant bindings. A shared named-call prefix can also align its first simple argument on the right-hand side. This local pass adds at most eight padding spaces beyond the required separator per gap; otherwise the group is preserved.
Blank lines, comments, headers, continuation bodies and unsupported rows separate
local groups. Layout forms (let, where, case, do) retain their arrangement.
The formatter preserves argument-pattern interiors, literal spellings, comments,
line endings and final-newline presence. Authors set group boundaries with
blank lines and comments. Tabs and comments in gaps remain intact. Malformed lexing returns the original source.
Header masking is used only for recognition.
The formatter uses located syntax without resolving names or requiring complete equation coverage. See Tools for preparation and use.