tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

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.

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:

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.