Syntax¶
Every ll-lang file begins with module, followed by optional import
declarations, then top-level declarations: type declarations (Uppercase),
function declarations (lowercase + params), value bindings (lowercase),
tag, unit, trait, impl, external, and opaque.
Indentation is significant. Comments start with -- and run to end of line;
both full-line (-- header) and trailing (x + 1 -- increment) forms are
accepted. ASCII only. No semicolons, no braces.
Active keywords: let, tag, unit, trait, impl, import, export,
module, external, opaque, infix, infixl, infixr, if, else,
true, false, match.
There is no fn, type, in, then, or with keyword.
Module header¶
module Examples.Basics
The module path is one or more uppercase-starting segments separated by dots. Must be the first non-comment token in the file.
Value declarations¶
Three declaration forms at the top level:
| Form | Description | Example |
|---|---|---|
name = expr |
value binding (no params) | pi = 3.14159 |
name(p T)... = expr |
function declaration | add(a Int)(b Int) = a + b |
let name = expr |
constant (explicit let) |
let greeting = "hello" |
let is optional for simple constants without parameters. Both pi = 3.14159
and let pi = 3.14159 are accepted at the top level.
Literals¶
| Form | Type | Notes |
|---|---|---|
42 |
Int |
64-bit signed |
3.14 |
Float |
requires a decimal point |
"hi" |
Str |
supports escapes \n, \t, \\, \" |
true / false |
Bool |
keywords, not constructors |
'c' |
Char |
supports escapes \n, \t, \\, \' |
Functions¶
Parameters come in individual (name Type) groups. Juxtaposition with
separate parens means currying, not multi-arg:
add(a Int)(b Int) = a + b
double(x Int) = x * 2
The return type after the last param is optional when H-M can infer it.
double above has no return annotation — the inferred type is Int -> Int.
Multi-line body¶
Indent the body one level:
clamp(x Int)(lo Int)(hi Int) =
if x < lo
lo
else if x > hi
hi
else x
Zero-arg functions¶
Use empty parens (or bare binding):
main() = printfn "Hello, ll-lang!"
Local bindings¶
Chain bindings by layout — no let...in:
example =
y = double 5
y + 1
Lambdas¶
triple = \x. x * 3
Backslash, parameter names, a period, then the body expression. Multiple parameters:
add = \a b. a + b
Operators and fixity¶
ll-lang ships with a canonical baseline operator table from Std.Operators
and also supports custom symbolic operators through fixity declarations.
Baseline defaults are loaded by prelude wiring, so projects get standard
precedence/associativity without local declarations.
| Operator | Meaning | Associativity | Precedence (low -> high) |
|---|---|---|---|
<|> |
choice | left | 1 |
>>= |
bind | left | 2 |
>> |
sequence | left | 2 |
|> / -> |
pipe (-> kept as expression-level compatibility alias) |
left | 3 |
== != < > <= >= |
comparisons | left | 4 |
:: |
list cons | right | 5 |
+ - |
arithmetic add/sub | left | 6 |
* / |
arithmetic mul/div | left | 7 |
application (f x) |
function application | left | 8 |
Fixity declarations¶
You can declare parser-contract metadata using:
infixl 6 +
infixr 5 ::
infix 4 ==
infixl 6 %%
Custom operators must stay symbolic and pass safety checks. Reserved single
tokens (=, |, :, .), malformed (?), comment-like (--) and overlong
forms are rejected with E030. Diagnostics E027..E031 cover assoc,
precedence, duplicate declarations, reserved/unsafe operators, and import
conflicts.
if / else¶
if is an expression — both arms must have the same type. Body is indented
on the next line; else follows at the same indent as if. There is no
then keyword.
abs(n Int) =
if n < 0
0 - n
else n
Chain with else if:
sign(x Int) =
if x < 0
"neg"
else if x == 0
"zero"
else "pos"
Type declarations¶
Types start with an uppercase identifier — no type keyword required:
Sum types (tagged unions)¶
Shape = Circle Float | Rect Float Float | Empty
Constructors are uppercase identifiers followed by zero or more type arguments.
Branches are separated by |.
Multi-line form with an optional leading |:
Color =
| Red
| Green
| Blue
Parametric types¶
Bare type parameters after the type name:
Maybe A = Some A | None
Result A E = Ok A | Err E
Phantom type parameters¶
Bracketed parameters carry no runtime value:
Email[state] = Str
See 04-tags-and-units for the phantom state pattern.
match: pattern matching¶
The compiler enforces exhaustiveness (error E003) in all forms.
Clause sugar — when a function body is a match over its last parameter, list arms directly:
area(s Shape) =
| Circle r -> 3.14159 * r * r
| Rect w h -> w * h
| Empty -> 0.0
Explicit form — match <scrutinee> followed by indented arms (no with):
describe(m Maybe[Int]) =
fallback = 0
match m
| Some n -> n
| None -> fallback
Supported patterns:
ConstructorName arg1 arg2— constructor pattern, binds args by position (e.g.Some n,None,Rect w h)name— variable binding (catches anything, binds toname)42,"hi",true,'c'— literal match (Int, Str, Bool, Char)_— wildcard
tag: semantic labels¶
tag UserId
tag Email
tag m
tag s
tag kg
A tag declaration introduces a label with no runtime representation. Applied
with postfix [Tag]:
uid = "user-42"[UserId] -- type: Str[UserId]
dist = 5.0[m] -- type: Float[m]
See 04-tags-and-units.
trait: higher-kinded type classes¶
trait Functor F =
map(f A -> B)(fa F[A]) F[B]
trait Monad F =
pure(a A) F[A]
bind(fa F[A])(f A -> F[B]) F[B]
The trait body is indented and contains method signatures (no bodies, no fn).
impl: trait implementations¶
impl Functor Maybe =
map(f A -> B)(fa Maybe[A]) Maybe[B] =
| Some a -> Some (f a)
| None -> None
An impl provides concrete function definitions for a trait applied to a
specific type constructor.
See 05-traits.
external: platform-native functions¶
-- Declares a function implemented in the host platform (no ll-lang body).
external console_log(msg Str) Unit
external JSON_parse(src Str) Str
logGreeting(name Str) =
console_log (strConcat "Hello, " name)
external functions are called exactly like regular ll-lang functions.
The compiler validates that the selected target has a mapping for the name in
discovered FFI.lll sidecars (SDK and/or vendored packages); if not, it emits
E026 UnknownExternalMapping.
Pre-mapped names available on all targets: console_log, JSON_parse.
opaque: platform-native types¶
-- Declares a type whose internals are managed by the host platform.
opaque HttpClient
opaque Buffer
Opaque types can appear as parameter and return types on both external and
regular ll-lang functions.
Expression-level constructs¶
Application (juxtaposition)¶
add 1 2 -- == (add 1) 2, left-associative
getUser "id"[UserId]
Whitespace between atoms is function application. Left-associative — curried.
Lists¶
xs = [1 2 3]
ys = [1; 2; 3]
Both space-separated and ;-separated list elements are valid. Commas are not
list separators ([1, 2] is tuple syntax).
Tuples¶
Comma-separated inside parens in a grouping context:
t = (1, "hi")
Pipes¶
-> is the pipe operator in expression context:
s -> trim -> len -- equivalent to len (trim s)
The type-arrow -> (as in Int -> Bool) only appears in type positions, so
the two uses do not conflict.
Arithmetic and comparison¶
Standard set: + - * / < > <= >= == !=. Precedence follows math convention:
* and / bind tighter than + and -, comparisons lower still.