tricu

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

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:

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

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.