Skip to content

Latest commit

 

History

History
934 lines (686 loc) · 16.7 KB

File metadata and controls

934 lines (686 loc) · 16.7 KB

March Syntax Quick Reference

A terse example of every construct. See lib/parser/parser.mly for authoritative grammar.


Comments

-- line comment

{- block comment
   (nestable: {- inner -}) -}

Module

mod MyApp do
  ...
end

Every file must start with a module declaration. Dotted names and nesting are supported:

mod A.B.C do ... end           -- dotted module name

mod Outer do
  mod Inner do ... end         -- nested module
end

Functions

fn add(x: Int, y: Int): Int do
  x + y
end

pfn helper(x) do              -- pfn = private
  x * 2
end

Multi-head pattern matching — consecutive clauses with the same name are merged:

fn len(Nil) do 0 end
fn len(Cons(_, t)) do 1 + len(t) end

Function clause guard:

fn abs(n) when n < 0 do -n end
fn abs(n) do n end

Return-type annotation is optional (fn f(x: Int): Bool do ... end).

Default argument values (Elixir-style \\):

fn greet(name, greeting \\ "Hello") do
  greeting ++ ", " ++ name ++ "!"
end

greet("World")           -- "Hello, World!"
greet("World", "Hi")    -- "Hi, World!"

Multiple defaults — all must be trailing parameters:

fn make(x, y \\ 10, z \\ 20) do x + y + z end
make(1)          -- 31  (uses y=10, z=20)
make(1, 5)       -- 26  (uses z=20)
make(1, 5, 6)    -- 12  (all explicit)

Local function inside a block:

fn outer() do
  fn inner(x) do x + 1 end
  inner(5)
end

Lambdas

fn x -> x + 1                 -- single param
fn _ -> 42                    -- wildcard (1-arg, NOT zero-arg)
fn (a, b) -> a + b            -- multiple params
fn -> compute()               -- zero-arg short form
fn () -> compute()            -- zero-arg explicit form (identical)

Multi-statement lambda bodies are supported with leading let bindings followed by a final expression — identical to match arm block bodies:

fn x ->
  let y = x + 1
  let z = y * 2
  z

fn (a, b) ->
  let sum = a + b
  sum * 2

fn () ->
  let x = compute()
  let y = x + 1
  y

The body is: zero or more let/linear let bindings, then a final expression. Single-expression lambdas are unchanged — no let bindings means no EBlock wrapper.

Both fn -> expr and fn () -> expr are valid zero-arg lambdas — they are identical.


Let Bindings

Block-level let — no in; subsequent exprs in the block see the binding:

fn main() do
  let x = 42
  let y = x + 1
  y
end

With type annotation:

let count: Int = 0

Linear let (must be consumed exactly once):

linear let handle: Handle = open_file("foo.txt")

Module-level let:

let pi = 3.14159

Result Propagation (let?)

let? p = e binds the Ok payload of a Result expression and short-circuits on Err:

fn parse_and_add(a: String, b: String): Result(Int, String) do
  let? x = int_from_string(a)   -- propagates Err(msg) immediately
  let? y = int_from_string(b)   -- only reached if previous succeeded
  Ok(x + y)
end

Rules:

  • The right-hand side must be Result(T, E).
  • The pattern binds the T (Ok payload).
  • The code after the let? must also produce Result(R, E) — the error type E must match across all let? bindings in the block.
  • let? cannot be the last expression in a block (there must be something after it).

Works in function bodies, match arms, and lambda bodies:

fn process(items: List(String)): Result(List(Int), String) do
  List.map(items, fn s ->
    let? n = parse(s)
    Ok(n * 2))
end

Types

Variant (ADT) — no leading |:

type Color = Red | Green | Blue
type Shape = Circle(Float) | Rect(Float, Float)

Generic variant:

type Option(a) = None | Some(a)
type Result(a, b) = Ok(a) | Err(b)

Record:

type Point = { x: Float, y: Float }
type User = { name: String, age: Int }

Private type (type and constructors both private):

ptype Internal = Foo | Bar(Int)

Phantom label type (tag — zero-arg type used as a state or resource marker):

tag ConnTag    -- equivalent to: type ConnTag = ConnTag
tag Open       -- equivalent to: type Open = Open
tag Closed     -- equivalent to: type Closed = Closed

Always-linear type (every binding is automatically tracked as linear — no per-site annotation needed):

always_linear type Handle(r, s) = Handle(Int)
-- Binding a Handle without consuming it is a compile-time error.
-- Double-use is also an error.

Combining tag + always_linear type for typestate handles:

mod Connection do
  tag ConnTag
  tag Open
  tag Closed

  always_linear type Conn(r, s) = Conn(Int)

  fn connect(_host : String) : Conn(ConnTag, Closed) do Conn(0) end
  fn open(h : Conn(ConnTag, Closed)) : Conn(ConnTag, Open) do
    match h do Conn(raw) -> Conn(raw) end
  end
  fn close(h : Conn(ConnTag, Open)) : Unit do
    match h do Conn(_) -> () end
  end
end
-- Wrong-state calls are caught at compile time.
-- Dropped handles are caught at compile time.

Opaque type (type name public, constructors private):

opaque type Handle = Handle(Int)
-- Inside the module: can construct and pattern-match Handle
-- Outside the module: type name visible, constructors hidden

Use opaque types to hide implementation details while keeping the type name usable in signatures:

mod Token do
  opaque type Token = Token(String)
  fn make(s) do Token(s) end
  fn value(Token(s)) do s end
end
-- Outside: can call Token.make/Token.value, cannot use Token(_) directly

Type Syntax

Int                       -- concrete type
List(Int)                 -- generic application
(Int, String)             -- tuple type
()                        -- unit type
Int -> Bool               -- function type (right-associative)
a -> b -> c               -- curried: a -> (b -> c)
Mod.Type                  -- qualified type
linear Handle             -- linear type (must use exactly once)
affine Handle             -- affine type (use at most once)

In nat-level arithmetic (for sized arrays):

type Arr(n) = Array(n * 2)

Refinement Types

A base type plus a predicate, checked by an SMT solver (Z3) at compile time. _ is the value being refined; the predicate is Int/Bool linear arithmetic.

{Int | _ >= 0}                     -- a non-negative Int
{Int | _ != 0}                     -- a non-zero Int
{v : Int | v >= 0 && v < 100}      -- named binder form

Used as parameter (precondition) or return (postcondition) types:

fn at(xs : List(Int), i : {Int | _ >= 0 && _ < len(xs)}) : Int do ... end
fn count(xs : List(a)) : {Int | _ >= 0} do List.length(xs) end

A @[measure] function (total, terminating, pure) may be used in predicates:

@[measure]
fn size(t : Tree(a)) : Int do
  match t do
    Leaf          -> 0
    Node(l, x, r) -> 1 + size(l) + size(r)
  end
end

Checking is definite-failure (flags only what can never hold — no false positives) and runs only when z3 is on PATH. See the Refinement Types guide for the full story and limitations (Int/Bool only, no Float value-refinements, direct calls only).


Patterns

_                         -- wildcard
x                         -- variable binding
42                        -- int literal
3.14                      -- float literal
"hi"                      -- string literal
true / false              -- bool literals
Nil                       -- nullary constructor
Some(x)                   -- constructor with args
Cons(h, t)                -- nested constructor
(a, b)                    -- tuple
(a, b, c)                 -- triple
[a, b, c]                 -- list (sugar for Cons chains)
[]                        -- empty list (Nil)
:ok                       -- atom
:error(msg)               -- atom with args
Mod.Con(x)                -- qualified constructor (disambiguation)
-5                        -- negative int literal

Match

match expr do
  Nil        -> "empty"
  Cons(h, _) -> h
end

Arms separated by newlines. Multi-statement arms need no wrapper:

match result do
  Ok(v)  ->
    let s = to_string(v)
    print(s)
  Err(e) -> print(e)
end

Guard on a match arm:

match n do
  x when x > 0 -> "positive"
  x when x < 0 -> "negative"
  _             -> "zero"
end

Cond (pattern-free multi-way if):

match do
  x > 10  -> "big"
  x > 0   -> "small"
  _       -> "non-positive"
end

With Expressions

Elixir-style monadic chaining for Result/Option types:

with Ok(x) <- f(),
     Ok(y) <- g(x) do
  x + y
end

With an else handler for failed patterns:

with Ok(x) <- fetch_user(id),
     Ok(y) <- fetch_data(x) do
  process(x, y)
else
  Err(e) -> handle_error(e)
end

Each pat <- expr binding: if expr matches pat, continue; otherwise fall through to else arms (or propagate the non-matching value if no else). Multiple bindings are separated by commas.


If / Else

if x > 0 do
  "positive"
end

With optional else block (both branches can be multi-statement):

if x > 0 do
  let msg = "positive"
  print(msg)
else
  print("non-positive")
end

else is optional — if without else returns (). There is no then keyword.


Operators

Integer arithmetic: +, -, *, /, % Float arithmetic: +., -., *., /. String/list concat: ++ Comparison: ==, !=, <, >, <=, >= Logic: &&, ||, ! (prefix not), unary - (negate)


Pipe

[1, 2, 3]
|> List.map(fn x -> x * 2)
|> List.filter(fn x -> x > 2)

|> threads the left value as the first argument of the right expression.


Literals

42                        -- Int
3.14                      -- Float
"hello"                   -- String
true / false              -- Bool
:ok                       -- Atom
:error("msg")             -- Atom with args

Triple-quoted strings preserve newlines:

let s = """
  multi
  line
"""

String interpolation:

let greeting = "Hello, ${name}!"
let info = "x = ${to_string(x)}"

Sigils

~H"<p>Hello</p>"          -- HTML sigil (produces IOList)
~H"<p>${name}</p>"        -- sigil with interpolation
~H"""
  <div>multi-line</div>
"""                       -- triple-quoted sigil

Any uppercase letter can be a sigil prefix (~H, ~R, etc.).


Tuples

(1, "two", true)          -- 3-tuple
(x, y)                    -- 2-tuple (pair)
()                        -- unit

Lists

[]                        -- empty list (Nil)
[1, 2, 3]                 -- list literal (sugar for Cons chains)
Cons(1, Cons(2, Nil))     -- explicit cons

List Comprehensions

[expr for pat in list]              -- map: apply expr to each element
[expr for pat in list, pred]        -- filter-map: only elements where pred is true

-- Examples:
[x * 2 for x in [1, 2, 3]]         -- [2, 4, 6]
[x for x in nums, x % 2 == 0]      -- only even numbers
[x + 1 for x in [10, 20, 30]]      -- [11, 21, 31]

Desugars to List.map / List.filter + List.map. Requires List in scope.


Records

Literal:

let p = { x: 1.0, y: 2.0 }

Field access:

p.x

Functional update:

{ p with x: 3.0 }
{ state with count: state.count + 1, name: "new" }

Function Calls & Field Access

List.map(xs, fn x -> x + 1)   -- module-qualified call
String.length(s)
p.x                            -- field access
a.b.c                          -- chained field/module access

Constructor application:

Some(42)
Ok("result")
Cons(1, Nil)

Block Expression

do ... end is usable as an expression anywhere:

let result = do
  let a = compute()
  a + 1
end

Typed Holes

?                             -- anonymous hole (compiler fills / reports type)
?name                         -- named hole

Useful for type-directed search.


Visibility & Doc/Attrs

fn pub_fn() do ... end        -- public (default)
pfn priv_fn() do ... end      -- private

doc "Returns the length of a list."
fn length(xs) do ... end

@[deprecated]
fn old_api() do ... end

Interfaces (Typeclasses)

interface Eq(a) do
  fn eq: a -> a -> Bool
  fn neq: a -> a -> Bool do  -- default implementation
    !eq(x, y)
  end
end

interface Ord(a) requires Eq(a) do
  fn cmp: a -> a -> Int
end

Implementations

impl Eq(Int) do
  fn eq(x, y) do x == y end
end

impl Eq(List(a)) when Eq(a) do
  fn eq(xs, ys) do ... end
end

Derive

derive Json, Eq for MyType
derive Show for Color

Module Imports

use List.*                    -- import all from List
use List.{map, filter}        -- import specific names
use List.map                  -- import single name
use A.B.C.*                   -- dotted path, all names

import String                 -- Elixir-style, all names
import String, only: [length, split]
import String, except: [dangerous_fn]
import String.{length, split} -- dot-brace form

Alias

alias Very.Long.Module as Short
alias Very.Long.Module, as: Short   -- comma-colon form
alias Very.Long.Module              -- alias to last segment

Signatures

sig MyCollection do
  type Elem
  fn insert: Int -> List -> Int
end

FFI (Extern)

extern "libc": Cap(LibC) do
  fn malloc(n: Int): Int
  fn free(ptr: Int): ()
end

Capabilities

needs IO.Network, IO.Clock

Declares capability requirements for the module.


Actors

actor Counter do
  state { count: Int }
  init { count: 0 }

  on Increment() do
    { state with count: state.count + 1 }
  end

  on GetCount(reply_to) do
    send(reply_to, state.count)
    state
  end
end

Spawn an actor and send messages:

let pid = spawn(Counter)
send(pid, Increment())

Supervision block inside an actor:

actor App do
  state {}
  init {}
  supervise do
    strategy one_for_one
    max_restarts 3 within 60
    Worker w
  end
end

Application Entry Point

app MyApp do
  on_start do
    Logger.info("starting")
  end
  on_stop do
    Logger.info("stopping")
  end
  Supervisor.spec(:one_for_one, [Worker])
end

Tasks (structured concurrency)

Spawn a task and await its result:

let t = Task.async(fn () -> expensive_computation())
Task.await(t)               -- Ok(result) or Err(reason)
Task.await_unwrap(t)        -- unwrap, panic on Err

Parallel map:

let results = Task.async_stream([1, 2, 3], fn n -> n * n)
-- [Ok(1), Ok(4), Ok(9)]

Structured combinators:

-- First to finish wins; the rest are cancelled
Task.race([t1, t2, t3])

-- First Ok wins; all-Err returns Err(list_of_reasons)
Task.any([t1, t2, t3])

-- Collect every result; never short-circuits
Task.all_settled([t1, t2, t3])   -- [Ok(v1), Err(e2), Ok(v3)]

-- Cancel any tasks still running when the scope exits
Task.scope(fn () ->
  let t = Task.async(fn () -> fetch_data())
  Task.await_unwrap(t)
)

Cancellation tokens:

let tok = task_cancel_token_new()
task_is_cancelled(tok)                          -- false

let t = task_spawn_with_cancel(fn _ -> work(), tok)
task_cancel(tok)                                -- mark cancelled
Task.await(t)                                   -- Err("cancelled")

-- Cancel a running task by its handle
task_cancel_by_id(t)

Session Type Protocols

protocol Transfer do
  Client -> Server : Request(String)
  Server -> Client : Response(Int)
  loop do
    Client -> Server : More(String)
    Server -> Client : Ack()
  end
end

protocol Negotiation do
  choose by Client:
    | accept -> Client -> Server : Accept()
    | reject -> Client -> Server : Reject()
  end
end

Linear / Affine Types

fn consume(linear h: Handle): () do
  close(h)
end

type Resource = { linear fd: FileDesc }

Testing

test "addition works" do
  assert (1 + 1 == 2)
end

describe "arithmetic" do
  setup do
    -- runs before each test in this describe block
  end

  setup_all do
    -- runs once before all tests
  end

  test "multiply" do
    assert (2 * 3 == 6)
  end
end

Debugger

dbg()                         -- unconditional breakpoint
dbg(some_expr)                -- trace / conditional

Complete Module Example

mod Main do

use List.*

type Shape = Circle(Float) | Rect(Float, Float)

fn area(Circle(r)) do
  3.14159 *. r *. r
end
fn area(Rect(w, h)) do
  w *. h
end

fn main() do
  let shapes = [Circle(1.0), Rect(2.0, 3.0)]
  let areas  = shapes |> map(fn s -> area(s))
  print(to_string(areas))
end

end