The syntax and semantics of Mere as currently implemented (as of 2026-06-24 / Phase 46). &T references / region / view / effects / FFI / 4-backend codegen are all implemented. Phase 36 added 13 kinds of syntactic sugar (range / op section / :: / <| / @@ / \ lambda / string interp / ? / ?! / list comp / if let / for-in-do / while-do), substantially improving ergonomics in the ML-family tradition.
// Line comment (to end of line)
| Kind | Example |
|---|---|
| Integer | 0, 42, -5 (syntactically Neg (Int_lit 5)) |
| Float | 1.5, 3.14, 0.0, 1e3, 2.5e-8, 4E+5 (digits.digits, or an exponent; bare 1. is not a float) |
| Boolean | true, false |
| String | "hello"; escapes are \n \t \\ \" |
| Char (length-1 str) | 'X'; escapes are '\n' '\t' '\\' '\'' '\"' |
| Unit | () |
A char literal 'X' is just a length-1 str (Mere has no separate char type). Convenient for dispatch like match c with | 'n' -> .... To avoid ambiguity with the type variable syntax ('a opt etc.), the lexer distinguishes 'X' (closing quote present) from 'NAME (no closing quote; alphabetic start).
- Start with a lowercase letter or
_; continue with alphanumerics /_. - Uppercase-leading is recognized by the parser as "constructor / record / type name".
- Type variables:
'a,'b, etc. ('+ lowercase-leading ident).
let rec and in if then else true false fn type signature
match with when of as _ for do while
module open import extern using region view drop
+ - * / % arithmetic
== != < <= > >= comparisons
&& || logic (short-circuit)
++ string concatenation
|> << >> pipe / function composition
<| reverse pipe (Phase 36): f <| x = f x
@@ low-precedence apply (Phase 36): f @@ x = f x
:: cons operator (Phase 36): h :: t = Cons (h, t)
.. range literal (Phase 36): a..b = [a, ..., b-1]
? Option early return (Phase 36)
?! Result early return (Phase 36)
<- list comprehension generator (Phase 36)
\ lambda shorthand (Phase 36): \x -> e
-> function type / match-arm separator
= binding
: ; , . annotation / terminator / separator / field
( ) { } [ ] grouping
... signature spread / list tail
| match separator / variant separator / record update / list comp
Inside string literals, {expr} is interpolation: the lexer tokenizes recursively, and the parser expands "a {x} b" into something like "a " ++ show_or_str x ++ " b" (actually a ++ chain depending on expr's type). \{ escapes a literal brace; nested string literals inside the interpolation are forbidden (work around by binding via let first).
let n = 42 in print "answer = {show n}" // "answer = 42"
print "escape: \{not interpolated\}" // "escape: {not interpolated}"
int float bool str unit
float is IEEE 754 double. Literals with a decimal point and digits (e.g. 1.5) are float, and so is anything with an exponent — 1e3, 2.5e-8, 4E+5 — which is how the ends of the double range are written (1.7976931348623157e308). A digit must follow the e, so 1.5 e is still a float applied to a variable named e. 1 is int (bare 1. is not float but 1 + a potential .field). int and float are distinct types with no implicit coercion — use float_of_int / int_of_float explicitly; arithmetic uses f_add / f_sub / f_mul / f_div.
t1 -> t2 function type (right-assoc: a -> b -> c == a -> (b -> c))
t1 * t2 * ... tuple type
t list type constructor (postfix application)
(t1, t2) result multi type-arg
'a type parameter (in declaration / annotation)
&R t region-tagged reference type (Phase 1: syntax only; semantic checks come later)
42 true "hi" ()
x (variable reference)
1 + 2 * 3 7 (* / has higher precedence)
10 / 3 3 (integer division; 0 div is Eval_error)
10 % 3 1 (mod; 0 div is Eval_error)
"a" ++ "b" "ab" (string concat)
5 <= 5 true
1 != 2 true
true && false false (short-circuit: don't eval RHS if LHS is false)
false || true true
not true false (builtin)
All desugar at the parser or lexer level, so the AST and beyond are unaffected. Per-form precedence is in §6.
0..5 // range: [0, 1, 2, 3, 4] (parser directly generates this; effectively list_iota)
1 :: 2 :: [] // cons: Cons (1, Cons (2, Nil))
(+ 1) // op section: fn x -> x + 1
(* 2) // (- 1) is ambiguous with unary -, so parenthesize
(< 10) // comparison sections also work
\x -> x + 1 // lambda shorthand: = fn x -> x + 1
\(a, b) -> a + b // tuple destructure OK
f <| x // reverse pipe: = f x
f @@ x // low-precedence apply: = f x; readable across line breaks
"x = {show n}" // string interpolation (lexer level; see §1)
[expr | x <- xs, p x] // list comprehension (single gen + filter)
[expr | x <- xs, y <- ys, p x y] // multi-generator (cartesian)
// desugar: list_map / list_flat_map
if let pat = e then yes_branch else no_branch
// = match e with | pat -> yes_branch | _ -> no_branch
// (else is required; both branches share the same type)
for x in xs do body // = list_iter xs (\x -> body)
// body must be unit-typed
while cond do body // = let rec __while_N = fn () ->
// if cond then (body; __while_N ()) else () in
// __while_N ()
// Note: currently only runs inside an fn body (top-level is codegen-unsupported)
let pat = e? in body form:
e?(Option): ifeisSome v, bindvtopatand evaluatebody; ifNone, the enclosing fn immediately returnsNone.e?!(Result): ifeisOk v, bind; ifErr e, the enclosing fn immediately returnsErr e.
Both desugar to Match in the parser:
let v = parse_int s ? in body
≈ match parse_int s with | Some v -> body | None -> None
let x = 5 in x + 1 // ident
let _ = side_effect in 1 // wildcard
let (a, b) = (3, 4) in a + b // tuple destructure
let (a, (b, c)) = (1, (2, 3)) in a + b + c
let rec fact = fn n -> if n < 1 then 1 else n * fact (n - 1) in fact 5
let rec is_even = fn n -> if n == 0 then true else is_odd (n - 1)
and is_odd = fn n -> if n == 0 then false else is_even (n - 1)
in is_even 10
if cond then a else b // standard if; a and b share the same type
if cond then print "msg" // side-effect-only; body must be unit-typed
with c = v in body is for resources with Drop (DB connections / file handles / mutexes etc.). The bound value's type must be a drop type ...-declared Drop type (use let for Trivial values). At scope end, the value's close: unit -> unit field is invoked (no-op if absent). Multiple bindings close in LIFO order.
drop type Conn = { id: int, close: unit -> unit };
let mk_conn = fn id ->
Conn { id = id, close = fn () -> print ("close " ++ show id) };
with c = mk_conn 1 in c.id
// Result: 1. At scope end, "close 1" is printed.
with c1 = mk_conn 1, c2 = mk_conn 2 in c1.id + c2.id
// Result: 3. Prints "close 2" → "close 1" (LIFO).
with x = 5 in x + 1 // ERROR: int isn't a Drop type. Use `let`.
Design notes: implements option (i) from the internal design notes — "region is strict-Trivial; Drop is managed via with".
See memory-model.md for the memory-management concepts, comparisons, and Mere's overall strategy.
region R { body } // bring R into scope as a region name; evaluate body
region R { region S { ... } } // nesting OK
fn (x: &R int) -> x // `&R T` reference type (R is a region name)
&R 5 // value expression: tag 5 as `&R int`
let x: &R int = &R 5 in ... // combined with explicit annotation
Current semantics (Phase 2):
region R { body }binds R into the inner scope and evaluates body. R itself is a unit-value placeholder.&R Tis the region-tagged reference type as expressed in the type system.&R vis a value expression that wraps v at&R T(interpreter passes the value through).- Escape check active: if R appears in the body's type of
region R { body }, it's a compile-time error —&R Tvalues can't leak out of the region. - Future (Phase 3+): the
r.alloc(v)method form (sugar for&R v), theTrivial[R]constraint,with+ Drop integration, child regions and promotion, etc.
Escape check examples:
region R { 42 } // OK: int doesn't contain R
region R { let x = &R 5 in 42 } // OK: `&R int` used inside, but result is int
region R { &R 5 } // ERROR: result is `&R int`; R leaks out
region R { (&R 1, 2) } // ERROR: `&R int` inside a tuple
R.alloc(v) sugar (Phase 2.5): inside a region, R.alloc(expr) is syntactic sugar for &R expr. The desugaring only happens when R is a lexically enclosing region name (ordinary obj.alloc(...) field accesses keep working).
region R {
let x = R.alloc(5) in // == let x = &R 5 in ...
let p = R.alloc((1, 2)) in
42
}
Trivial[R] constraint (Phase 2.6): only types without Drop semantics (Trivial) can be placed in a region. Drop types are declared with drop type Name = ...; including such a type in a region (&R v / R.alloc(v) / view fields) is a type error. This is "a constraint that allows bulk region freeing"; caps that need Drop (DB connections / file handles etc.) are separately managed by a future with expression.
drop type Conn = { id: int };
let c = Conn { id = 1 } in c.id // OK: Drop types are usable outside a region
region R {
&R Conn { id = 1 } // ERROR: Trivial[R] violated
}
view Holder[R] { c: Conn };
region S { Holder { c = ... } } // ERROR: view field has a Drop type
region R {
&R (fn (c: Conn) -> c.id) // OK: function types are Trivial (closure values)
}
Trivial[R] is implicitly the default: ordinary types (int / str / record / tuple / variant / Vec[R, T] / &R T / closure etc.) are automatically Trivial[R]. Users do not need to declare impl Trivial[R] for X { } (a future trait system may revisit this; see the internal design notes §3). The sole exception is types declared with drop type — they break Trivial[R] at every position they structurally appear (contains_drop_type walker in lib/typer.ml). So the judgment scheme is the simple "default-Trivial + drop-blacklist". Full trait-system rollout (DEFERRED §3.1) and explicit impl Trivial[R] syntax (§6.1) are linked in the design but don't affect the current implementation.
view V[R] of T { f1: T1, f2: T2, ... }; // view type over region R (with explicit inner type T)
view V[R] { f1: T1, ... }; // `of T` is optional
view V[R] of T { ... } is a data declaration with a region parameter. In Phase 2.4:
- View construction is only allowed inside a
region { ... }block (writingV { ... }outside is a type error). - At construction, the view's region parameter
Ris substituted with the innermost active region's name, and the view value's type becomesV[<region>]. - Field access
v.f1and record update{ v | f1 = e1 }work like records;&R Tfields are retrieved with the type substituted to the construction-time region. - The view value itself is subject to escape checking — cannot leave the construction region.
view Node[R] of int { value: int, next: int };
region R { let n = Node { value = 1, next = 0 } in n.value } // 1
region MyArena { let n = Node { value = 7, next = 0 } in n.value } // 7 (R → MyArena)
let n = Node { value = 1, next = 0 } in ... // ERROR: must be inside a region block
view Slot[R] { item: &R int };
region S {
let s = Slot { item = &S 42 } in // s : Slot[S]
let take_s = fn (x: &S int) -> 99 in
take_s s.item // s.item : &S int → 99
}
region S { Slot { item = &T 42 } } // ERROR (region mismatch)
region S { Cell { v = 1 } } // ERROR: Cell[S] cannot leave region S
Planned tightening for later phases:
- Cyclic construction within the same region (two-phase: mutable construction + immutable use).
- Q-009's "structural identity by region" axiom (identifying same-typed views inside a region).
See memory-model.md and the internal design notes.
using [cap1, cap2, ...] is a sugar that eases the repeated partial-application patterns of cap-passing style. Caps are expanded as the outermost curried args.
fn x using [logger] -> body
// ≡ fn logger -> fn x -> body
Callers can immediately get a T -> U with the cap embedded via f cap, ready to pass to higher-order functions like map:
let log_x = fn x using [logger] -> logger (show x);
let bound = log_x my_logger; // bound : int -> unit
iter bound [1, 2, 3];
- Type annotations OK:
fn x using [c: int -> int] -> c x - Multiple caps:
fn x using [logger: Logger, metrics: Metrics] -> ... - Combined with normal params:
fn (x: int) using [c: Logger] -> c.info (show x) - Empty
using []is a parse error.
fn x -> x + 1 // single arg (type-inferred)
fn (x: int) -> x + 1 // single arg (annotated)
fn (x: int, y: int) -> x + y // multi-arg (desugared to currying)
fn (a, b, c) -> a + b * c // multi-arg, no annotations
fn () -> 42 // no args (internally _u : unit)
inc 5
add 3 4 // = (add 3) 4
let inc1 = (+) 1 in ... // turning operators into functions is not yet supported (use a curried fn)
(1, 2, 3) // tuple
type Point = { x: int, y: int };
let p = Point { x = 3, y = 4 } in p.x + p.y // record
let p2 = { p | x = 100 } in p2.x // record update
type 'a list = Nil | Cons of 'a * 'a list;
[1, 2, 3] // list literal sugar = Cons (1, Cons (2, Cons (3, Nil)))
[1, 2, 3,] // trailing comma allowed (also in tuple / record literals)
[] // = Nil
type 'a opt = None | Some of 'a;
match Some 42 with
| None -> 0
| Some n when n > 10 -> 1000
| Some n -> n + 1
match xs with
| [] -> "empty"
| [h, ...t] -> "head + rest"
| [a, b, c] -> "exactly three"
match x with
| (a, b) as p when a < b -> p // as-pattern: bind whole to p
| _ -> (0, 0)
match day with
| 1 | 2 | 3 | 4 | 5 -> "weekday" // or-pattern
| 6 | 7 -> "weekend"
| _ -> "invalid"
{ } // → unit
{ e1; e2; e3 } // → eN; e1..e_(N-1) are discarded (sugar for let _ = ... in chains)
5 |> inc |> dbl // = dbl (inc 5); left-assoc; lowest precedence
inc << dbl // = fn x -> inc (dbl x); right-assoc
inc >> dbl // = fn x -> dbl (inc x); right-assoc
(42 : int) // expressive; must agree with the existing type
((fn x -> x + 1) : int -> int) 5 // function-typed annotation
signature ctx = (db: int, log: int);
let save = fn (...ctx, order: int) -> db + log + order in
save 100 10 5 // 115
| Kind | Syntax | Example |
|---|---|---|
| Wildcard | _ |
_ |
| Variable | name |
n, xs |
| Integer | N |
0, 42 |
| Boolean | true / false |
|
| String | "..." |
"foo" |
| Unit | () |
|
| Tuple | (p1, p2, ...) |
(a, b), (a, (b, c)) |
| Constructor | Name or Name sub_pat |
None, Some x, Cons (h, t) |
| List | [] / [a, b, c] / [h, ...t] / [..._] |
|
| Record | Name { f1 = p1, f2 = p2 } |
Point { x = 0, y = py }; partial OK |
| as | pat as name |
Cons (h, t) as whole |
| or | `p1 | p2` |
match x with
| n when n > 0 -> "positive"
| _ -> "non-positive"
let x = 5; // ident form
let (a, b) = (3, 4); // pattern form
let _ = print "init"; // wildcard is fine
let rec fact = fn n -> ... ;
let rec is_even = ... and is_odd = ... ;
// 1. Sum type (variant)
type 'a opt = None | Some of 'a;
type ('a, 'b) result = Ok of 'a | Err of 'b;
// 2. Record
type Point = { x: int, y: int };
type 'a Box = { value: 'a };
// 3. Type alias
type UserId = int;
type Pair = int * int;
type 'a Stack = 'a list;
Disambiguation:
=followed by{→ record.- Leading
|, or uppercase ident followed by|/of→ variant. - Otherwise → alias.
signature ctx = (db: int, log: int);
// Expanded by `fn (...ctx, x: int) -> ...` (parse-time)
| Precedence | Operators | Associativity |
|---|---|---|
| 1 (low) | let, if, fn, match, with, for, while |
- |
| 2 | @@ (low-precedence apply, Phase 36) |
right |
| 3 | ` | >/< |
| 4 | <<, >> |
right |
| 5 | ` | |
| 6 | && |
left |
| 7 | ==, !=, <, <=, >, >= |
non-associative |
| 8 | :: (cons, Phase 36) |
right |
| 9 | .. (range, Phase 36) |
non-associative |
| 10 | +, -, ++ |
left |
| 11 | *, /, % |
left |
| 12 | unary - |
- |
| 13 | ? / ?! (postfix, Phase 36) |
postfix |
| 14 | function application | left |
| 15 (high) | atom / (...) / [...] / {...} / .field / op section (+ N) / \x -> e / "...{expr}..." |
- |
expr : type (annotation) is applied once at the outermost level.
- Strict (call-by-value);
&&and||are short-circuit. - No mutation; rebinding is not allowed;
withalso creates a new binding. - Closure capture is by value-reference (the environment is closed in the closure).
- Errors: type errors are compile-time;
fail/assert/div by zero/unmatched match etc. are runtimeEval_error.
Mere has no explicit "copyable" marker like Rust's Copy trait. Instead, the following implicit rules:
- Value types (int / float / bool / str / unit / list / tuple / variant / record / closure): free to rebind under the same or different names with
let x = v in ..., pass repeatedly as arguments ("Copy" treatment). Implementation-wise this is structural sharing + GC-less region alloc of immutable values. - Region-bound reference types (
&R T/Vec[R, T]/Map[R, K, V]/StrBuf[R]): freely duplicable during the region's lifetime (internally a pointer + bulk-freed with the region). - Drop types declared with
drop type ...(Conn/Fileetc.): can't be placed in a region (Trivial[R] violation); managed scope-bound bywith. Outside a region, let rebind is permitted (no Linear enforcement; close runs automatically at scope end). OwnedVec[T]: linear-ish. Phase 38.G-1 Level 1 added auto-Drop (freeat lexical scope end).let v2 = v1-style aliasing is syntactically possible but problematic (double Drop), so users are encouraged to use idioms likevec_to_ownedfor explicit conversion.
So Mere's Copy/Linear distinction is realized via three layers — Drop types / OwnedVec / everything else — without explicit Copy/Linear trait annotations. Design room remains to introduce T: Copy / T: Linear type bounds later (linked to the trait system §3.1), but with no dogfood signal, it's confirmed-deferred (same §6.4).
Items previously listed as "not implemented" were implemented incrementally through Phases 14-36; the following remain:
- Exhaustiveness check is Phase 1 (bool + variants only): non-exhaustive → warning to stderr; evaluation proceeds (case omissions become runtime fallthrough errors).
- For int / str / float / tuple / record, a wildcard arm is required (precise checks come later).
- String escapes are only
\n \t \\ \"plus Phase 36's\{(interp brace escape). No Unicode escape (\uXXXX). - Integers are fixed-width; no arbitrary precision. Per backend: the C and LLVM backends use 64-bit int (
long long/i64; the LLVM backend was widened from i32 to i64 in v0.1.96, forced by running contrib/bignum on it), the interpreter uses OCaml's int (host-dependent, normally 63 bits), and the Wasm backend uses i64 as well (widened from i32 in v0.1.127, forced by a Date.now()-driven clock whose epoch-ms exceeds 2^31 — the uniform value model is now 8-byte slots, with pointers wrapped to 32-bit addresses at memory operations). At the C FFI boundary (extern fn) int follows the backend width (i64 on C/LLVM); a C function that returns a 32-bitintshould be declared accordingly on the foreign side. Hex literals are written0xFF/0Xff(v0.1.46); they lex to ordinary ints (no separate type) and obey the same per-backend width. There is no octal or binary literal syntax and no digit separator. - Float: IEEE 754 double. The arithmetic and comparison operators are numeric-overloaded —
+ - * /,< <= > >= == !=, and unary-all work on floats (v0.1.44 corrected this entry: it long claimed prefix-onlyf_addstyle, which the Mandelbrot example disproved — the infix forms had worked on interp/C/Wasm for a while; the same probe found and fixed LLVM emitting invalid IR for them). The overload picks float only when an operand is concretely float, so unannotated fn params default to int — annotate (fn (x: float) -> ...) in float-heavy code.%stays int-only; thef_addfamily remains available as ordinary functions. - No nested string literals in interpolation:
"x = {show \"abc\"}"is a lexer error (work around via let). whileonly inside fn bodies: writingwhiledirectly under top-level main is codegen-unsupported (top-level Let_rec constraint).- REPL
:type EXPRis value-expressions only: type display of top-level decls is available via:show NAME. - FFI types are MVP:
int / bool / str / unitonly (float / tuple / record / variant / callback deferred, Phase 32). - Polymorphism: HM inference + let-polymorphism + per-instantiation specialization of polymorphic user let-recs (Phase 23.3 / 25.5 / 26.4). Phase 36 introduced a narrow value restriction (don't generalize on let-bind when the type contains a mutable container).
- 1573 tests passing (test/test_basic.ml).
- 4-backend feature parity: interpreter + C / LLVM IR / Wasm runtime.
- 16 realistic examples (~1500 LoC + toy_sql 1165 LoC) match diff = 0 PERFECT.
- See Changelog / Codegen for details.
For detailed behavior, see examples/ and test/test_basic.ml.