tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

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.

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.