Contents
- 1. Lexical
- Comments
- Literals
- Identifiers
- Keywords
- Operators and symbols
- String interpolation (Phase 36)
- 2. Types
- Primitives
- Composite types
- 3. Expressions
- Literals / identifiers
- Arithmetic / comparison / logic
- Phase 36 syntactic sugar at a glance
- Option / Result early-return (`?` / `?!`, Phase 36)
- let bindings
- let rec / mutual recursion
- Top-level functions in any order (v0.1.588)
- Forward declarations (`let fn
: ;`) - `mere --decls
` - if-then-else / if-then
- with (scope-bound resources with Drop, Phase 3.1)
- region (Phase 2: syntax + value expression `&R v` + escape check)
- view (Phase 2.4: declaration + region enforcement + type-tag propagation)
- Functions + `using [cap]` syntactic sugar
- Functions
- Application / partial application
- Tuples / records / lists
- Sum types / constructors / match
- Block / side-effect sequencing
- Function composition / pipe
- Type annotation
- Signature alias (function-argument bundling)
- 4. Patterns
- String prefix patterns (v0.1.504)
- Guards (in match)
- 5. Top-level declarations
- let / let rec
- Type declarations
- signature
- 6. Operator precedence (low → high)
- 7. Evaluation model
- Copy semantics (implicitly default)
- 8. Known constraints (2026-06-24)
- 8.5. `pub` inside a module (v0.1.504)
- 9. What the compiler warns about
- 10. Status summary
Language reference (mere)
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.
1. Lexical
Comments
// Line comment (to end of line)
Literals
| 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).
Identifiers
- 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).
Keywords
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
trait impl dyn derive
None of these can be a name; let view = 5 is refused with "view is a reserved word". The list is the lexer's own table, and scripts/keywords_doc_check.sh holds this block and reserved-names.md to it.
Operators and symbols
+ - * / % 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
String interpolation (Phase 36)
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}"
2. Types
Primitives
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.
Composite types
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)
3. Expressions
Literals / identifiers
42 true "hi" ()
x (variable reference)
Arithmetic / comparison / logic
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)
Phase 36 syntactic sugar at a glance
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)
Option / Result early-return (? / ?!, Phase 36)
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 bindings
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 / mutual recursion
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
Top-level functions in any order (v0.1.588)
A top-level function may call one defined below it, and two top-level functions may call each other from separate declarations -- no and needed:
let area = fn (s: Shape) -> scale * base_area s;
let scale = 2;
let base_area = fn (s: Shape) -> match s with | Sq n -> n * n | Dot -> 0;
let is_even = fn (n: int) -> if n == 0 then true else is_odd (n - 1);
let is_odd = fn (n: int) -> if n == 0 then false else is_even (n - 1);
What runs still runs in the order it is written: a value (let t = map_new ();, let _ = print "hi";) keeps its place relative to every other value. Only function definitions -- which do nothing when they are reached -- are placed after what they refer to, and functions that refer to each other across declarations are checked as one group (so they are monomorphic in each other, the way an and group is). A value still cannot use something defined below it: let v = f 1; above the definition of a function f needs is the unbound variable error it always was. Inside an expression, let ... in keeps its order.
Forward declarations (let fn <name>: <type>;)
let rec ... and ... is the way two definitions inside an expression can call each other, and a chain closes where it ends. import is a splice, so a chain also closes at an import: two files, or two chains in one file, could not be mutually recursive. A forward declaration binds a name to a written type at the point of the promise; the definition follows anywhere below, including in a file imported further down.
let fn is_even: int -> bool; // the promise
let is_odd = fn (n: int) -> if n == 0 then false else is_even (n - 1);
let is_even = fn (n: int) -> if n == 0 then true else is_odd (n - 1);
It is the mirror of extern fn <name>: <type>;, which declares a name defined outside Mere; this one is defined inside it, later. Two rules, both checked:
- the definition must have the declared type — otherwise callers written
above the definition and callers written below it would see different types for the same name;
- every promise must be kept — a declared name with no definition is
refused, naming the declaration.
A written type variable is quantified, not rigid: let fn idl: 'a list -> 'a list; declares a scheme, and each call site instantiates it fresh. (In a parameter annotation the same 'a still names one type the caller chose — there the writer is naming, here promising.) Region parameters make this the ordinary case rather than the exotic one: every function that takes a Map or a Vec has one.
The definition must be at least as general as the declaration, not merely an instance of it — let fn idl: 'a list -> 'a list; with let idl = fn (xs: int list) -> xs; is refused. It has to be: a caller written above the definition would otherwise be free to pass a str list, which is the one thing a declaration exists to allow and the definition cannot do.
A declaration also does not cost the name any polymorphism it would have had: a declared 'a -> 'a is usable at as many types as the same definition with no declaration at all.
The name may be module-qualified — let fn M.f: int -> int; declares a member of module M { ... }, whose splice gives it that name.
mere --decls <file>
Prints the declaration for every top-level name the file defines — how a large chain gets split without transcribing hundreds of types by hand. It reports the file's own names only (not the prelude's), under the spellings the source uses, and does not run the program.
Two kinds of name come out commented, with the reason, because pasting them back would not mean what it says:
- a name bound more than once at top level (
let x = 1; let x = x + 40;) —
one declaration cannot name two bindings;
- a name that shadows a builtin. Top-level bindings are sequential, so a caller
written above let show = ... uses the builtin show; a declaration puts the user's show in scope from the declaration down, which changes what that caller calls. Uncomment it only if that is what you want.
scripts/decls_roundtrip.sh is the gate: every program in test/parity/ must produce identical output with its own --decls output prepended.
⚠ A record type declared inside a module cannot be named in an annotation from outside it — M.t and t are both rejected — so a declaration mentioning one does not type-check. That is a pre-existing gap in annotations generally, not in declarations: let use = fn (r: M.t) -> r.a fails the same way.
if-then-else / if-then
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 (scope-bound resources with Drop, Phase 3.1)
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".
region (Phase 2: syntax + value expression &R v + escape check)
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 (Phase 2.4: declaration + region enforcement + type-tag propagation)
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.
Functions + using [cap] syntactic sugar
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.
Functions
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)
Application / partial application
inc 5
add 3 4 // = (add 3) 4
let inc1 = (+) 1 in ... // turning operators into functions is not yet supported (use a curried fn)
Tuples / records / lists
(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
Sum types / constructors / match
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"
Block / side-effect sequencing
{ } // → unit
{ e1; e2; e3 } // → eN; e1..e_(N-1) are discarded (sugar for let _ = ... in chains)
Function composition / pipe
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
Type annotation
(42 : int) // expressive; must agree with the existing type
((fn x -> x + 1) : int -> int) 5 // function-typed annotation
Signature alias (function-argument bundling)
signature ctx = (db: int, log: int);
let save = fn (...ctx, order: int) -> db + log + order in
save 100 10 5 // 115
4. Patterns
String prefix patterns (v0.1.504)
match url with
| "https://" <> rest -> secure rest
| "http://" <> rest -> plain rest
| "mailto:" <> _ -> mail
| other -> none other
The scrutinee starts with the literal, and what is left is bound (_ discards it). The literal's length is counted in code points, not bytes: "ét" <> rest on "été" binds "é", where the prefix is two characters and three bytes.
Lowered before inference into the guard and the slice the same code was writing by hand — str_starts_with, then utf8_sub — so no backend knows the syntax exists. Two consequences worth knowing:
- A prefix arm does not make a match total. It is a guarded arm, and a
guarded arm closes nothing, so a match on str still wants a catch-all.
- A guard may name the binder:
| "ab" <> r when str_len r > 1 ->works,
because the binding is made for the guard as well as for the body.
⚠ utf8_sub walks the string. That is right for parsing and wrong for a hot loop; a byte-indexed slice would be a builtin on five backends, and waits for something measured to ask for it.
| 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 | 1 | 2 | 3; both branches bind the same names + types |
Guards (in match)
match x with
| n when n > 0 -> "positive"
| _ -> "non-positive"
5. Top-level declarations
let / let rec
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 = ... ;
Type declarations
// 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
signature ctx = (db: int, log: int);
// Expanded by `fn (...ctx, x: int) -> ...` (parse-time)
6. Operator precedence (low → high)
| Precedence | Operators | Associativity | ||
|---|---|---|---|---|
| 1 (low) | let, if, fn, match, with, for, while | - | ||
| 2 | @@ (low-precedence apply, Phase 36) | right | ||
| 3 | | > / < | (Phase 36) | left / right |
| 4 | <<, >> | right | ||
| 5 | | | left | |
| 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.
7. Evaluation model
- 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.
Copy semantics (implicitly default)
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` / `File` etc.): can't be placed in a region (Trivial[R] violation); managed scope-bound by
with. 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).
8. Known constraints (2026-06-24)
Items previously listed as "not implemented" were implemented incrementally through Phases 14-36; the following remain:
- A `match` missing a named case is an error, on every path that runs or emits the program — interp, C, LLVM, Wasm and RV32IM alike. The error names every missing case, prints the arm to add for each, and offers
| _ -> fail "todo"as a hole that lets the rest of the file keep compiling (failis typed'a, so it satisfies any match).--allow-nonexhaustivedowngrades it to a warning, for a tree mid-port. Until v0.1.468 this was a warning, and one printed only by the interpreter: the four backends that produce the artifact said nothing and exited 0, and each filled the fallthrough in with a value of its own. - The check looks inside patterns (v0.1.472). It is the standard usefulness algorithm, so a refutable payload is not a blind spot:
| Cons (_, Cons (b, _))and| Nilleave a one-element list uncovered and the checker says so, naming the value —missing Cons (_, Nil). Records are checked by declared field (a field the pattern omits is a wildcard), tuples component by component, and bool and unit as the finite types they are. - What is refused, and what is only reported (v0.1.473). A witness built entirely from constructors of FINITE signatures — variants, bool, unit, tuples, records — names a shape, and the fix is the arm the error prints: those stop the build, at any depth. A witness whose decisive position is in an infinite domain — an int / str / float value, or a bare
_— is a warning:match n with | 0 -> … | 1 -> …ismissing 2, and the only arm that closes it is| _ -> …, which this reference already asks for at every match over a scalar, so refusing the program would add nothing to what the warning says. - A type name may not be declared twice with different constructors (v0.1.474). Restating a type identically is fine and common — twelve files in this tree restate
'a listor'a optfor self-containment — but two different types cannot share a name:typesand the variant registry key on the bare name, so the second declaration wins for the name while the first's constructors stay usable, and amatchover one was checked against the other. That is refused now, on every path, with both constructor sets named. This is not a module rule: module-internal types are registered globally and unqualified on purpose (Json.jsonin an annotation resolves to the barejson), somodule A { type t = … }andmodule B { type t = … }is the same collision as declaringtype ttwice at top level. Two modules may still share a constructor name (Traffic.RedandMood.Red), which is what module scoping is for. - The same holds for records, by fields (v0.1.525): a record type name may not be declared twice with different fields, and the refusal names both field sets. Until then records had no such check at all —
type t = { a: int }; type t = { b: str };was accepted in silence and the second won. Records now follow the same rule as variants because they now have the same identity: a record declared in a module is its bare name, soM.tandtare one type, an annotation may be written either way, and the module that declared it can annotate with it. Before v0.1.525 it could not — a literal insidemodule Mbuilt anM.twhile every annotation resolved tot, somodule M \{ type t = \{ a: int }; let get = fn (v: t) -> v.a; }did not compile in the module that declared the type. Variants never had that, because their qualification lands on the constructor and leaves the type canonical. - And a variant and a record cannot share a name (v0.1.551):
type result = { name: str }is refused, because the prelude declaresresult(withoptionandlist) as a variant. Until v0.1.551 it passedcheckand ran on the interpreter while the C backend failed inside the compiler. The capability recordsLoggerandMetricsare the exception: a program may declare its owntype Loggerin any shape and it replaces the builtin. - String escapes are
\n \t \r \0 \\ \", Phase 36's\{and\}(interpolation braces), and `\uXXXX` (v0.1.513): exactly four hex digits, encoded as UTF-8 — so"\u3042"is three bytes andutf8_lencounts it as one character. A surrogate half (\uD800–\uDFFF) is refused: it is not a character, andstris bytes, so nothing downstream would put a pair back together. Beyond the BMP is written as the character itself; this file is UTF-8 and the lexer copies unknown bytes through. - 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. Binary and octal literals are written0b1010/0B1010and0o17/0O17, and `_` may be written between digits in any base and in a float:1_000_000,0xFF_FF,0b1010_1010,1_000.5(v0.1.513). Each_has to be followed by another digit, so1_is still the integer 1 next to an identifier. Before that these were not syntax errors but NAME errors —0b1010read as the integer 0 next to a variable calledb1010, and was reported as an unbound variable. - 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). - A top-level `while` must be bound or be the last expression (this entry said "only inside fn bodies, codegen-unsupported" long after it stopped being true).
let _ = while cond do body;and a trailingwhile cond do bodyboth type-check, emit on C / LLVM / Wasm and run — a loop written that way at top level fills aVecand prints the same count on the interpreter and a compiled binary. What is still refused iswhile cond do body;with a statement after it: the;ends aletor a declaration and cannot follow a bare loop, so the parser answerstrailing input. - REPL `:type EXPR` is value-expressions only: type display of top-level decls is available via
:show NAME. - FFI types, and the fact that they differ per backend: this entry said "
int / bool / str / unitonly (float / tuple / record / variant / callback deferred, Phase 32)" long after it stopped being true. Measured at v0.1.444: on the C and LLVM backendsfloat, a monomorphic record, and `bytes` all cross anextern fn, and a hand-written C file linked against the emitted code sees them asdouble, a by-value struct, andmere_bytes*({ long long len; unsigned char data[]; }). Two cautions. `int` at the boundary is C `int`, 32 bits, not thelong longMere uses internally (v0.1.41) -- a pointer does not fit, so handles cross asintthe way the socket family does. And a record's C layout is the emitted one: field order as declared, names prefixedmu_. Do not transcribe it by hand: `mere --ffi-header <file>` prints the header for this direction — the prototypes and the structs they carry, and only the types the boundary reaches — and a shim should#includeit rather than writing the layout out again. (mere --headeris the OTHER direction: Mere compiled as a shared library, for a C caller.)scripts/ffi_header_check.shbuilds a shim against it, and its poison shows the same shim with a hand-copied struct giving a different answer. Wasm is different: the same declaration becomes anenvimport with every parameter ani32, so a value crosses as a pointer and the host glue does the reading. Still untested and therefore still unclaimed: tuples, variants, and passing a Mere closure as a callback (browser glue does that on Wasm through__indirect_function_table, which is not the same question). - 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).
8.5. pub inside a module (v0.1.504)
module Store {
let secret_key = fn (n: int) -> n * 7; // internal
pub let get = fn (n: int) -> secret_key n + 1;
}
print_int (Store.get 5) // fine
print_int (Store.secret_key 5) // type error: `Store.secret_key` is internal to module `Store`
Opt-in per module. A module that marks nothing exports everything, exactly as every module written before this does; a module that marks anything is saying it has decided what its surface is, and its unmarked members become internal. Members of the module — and of modules nested inside it — reach each other regardless.
pub is not a keyword: it is an identifier the module-body parser recognises in front of let, so a program using pub as a name is unaffected.
And at the top of a FILE (v0.1.514, Q-166). import "path"; splices the imported file's declarations into this one's, so after an import there is a single top-level namespace — which is why this entry used to say file-level visibility had "nothing to enforce". The boundary the splice erases is the one Loc.t keeps: every token carries the file it came from, so a reference can be checked against the file that made the binding.
// lib.mere
let internal_helper = fn (n: int) -> n * 7;
pub let public_api = fn (n: int) -> internal_helper n + 1;
import "lib.mere";
print_int (public_api 6) // 43
print_int (internal_helper 6) // type error: `internal_helper` is internal to lib.mere
Opt-in per file, as it is per module: a file that marks nothing exports everything, which is every file written before this one. A file that binds the name itself is unaffected by what another file decided about its own copy — the single namespace means two files may bind the same top-level name, and without that rule one library marking pub would make a common name unusable everywhere.
⚠ This is visibility, not separate compilation. The splice still happens: the program is still one translation unit, mere -c still reads the whole tree, and pub changes what may be REFERRED to rather than what is compiled.
9. What the compiler warns about
A warning is not an error: the program compiles and runs. Every one of these is something the compiler knows and the person cannot see.
| warning | what it means |
|---|---|
| a type name collides with a C type, keyword or libc symbol | the C backend will refuse this later, with an error about generated code rather than about your line (reserved-names.md). A let name cannot collide: every backend prefixes it |
extern fn declares a different arity than the compiler implements | the same, one layer down |
main is not special in Mere | the entry point is the file's trailing expression; a binding named main reads as if it were one |
non-exhaustive match with no wildcard for an unenumerable type | an approximation the checker cannot prove; a named missing case is an error, not a warning |
| an arm no value can reach | an earlier arm already answers it |
| an unread binding (v0.1.503) | a local let or a match binder that nothing reads. Prefix it with _ if that is deliberate. Not reported for top-level names (a file that is imported has its readers elsewhere), for function parameters, or for any file that did not type-check — in a half-inferred tree "nothing reads this" is usually "the line that reads it is the one being typed" |
| a `let` that can fail (v0.1.505) | an ERROR, not a warning: let Some n = e; is a match with one arm, and the value it does not handle is named. let (a, b) = ..., a record pattern and a constructor pattern on a one-constructor type are all total and stay free. if let is untouched — it is the construct for a pattern that may not match |
| a deprecated name (v0.1.503) | one of the compiler's own names that has been retired, with the replacement. Only where the name resolves to the builtin: a binding of your own by that name is yours |
--warnings-as-errors makes a run that produced any of these exit 1 — everything is still printed and still emitted, and the status is the answer. It counts warnings PRODUCED, not the ten a terminal prints.
A terminal prints at most ten warning blocks and then says how many more there are; an editor draws all of them.
Where a diagnostic points (v0.1.506): at the line you can act on. A failure raised inside a prelude function — assert, divmod, list_max are written in Mere — reports the call in your file rather than the prelude's own line, and declaring a type twice puts the caret on the second declaration while the message names the first. (The first tree the unread-binding check was pointed at answered with 462, which is four thousand lines of stderr in front of whatever you ran the compiler to see.)
When the spelling is another language's (v0.1.507): the error says what to write instead. This is the layer you meet first, and until now it was the only one with no help: lines of its own — var x = 1; and def f(n): both came back as trailing input.
parse error: expected ';' or 'in' after let binding
--> x.mere:1:35
|
1 | let _ = if true then 1 elif false then 2 else 3;
| ^^^^
|
= help: `elif` — chain with `else if`
| you wrote | Mere | |
|---|---|---|
x += 1 | bindings do not change: let y = x + 1; | |
# | comments are // | |
!x | negation is not x — != is the comparison | |
$ outside a string | interpolation is "x = {expr}", so $ never starts anything | |
=> | the arrow is -> | |
{ ... } as a block | if c then a else b, match x with \ | pat -> e; a sequence is let _ = a; b |
= as a comparison | == | |
mut / var / val | let name = value; | |
def / func / fun | let name = fn (x: int) -> body; | |
case / switch | match x with \ | pat -> e |
elif | else if | |
return | the last expression is the value | |
a and b | a && b — and joins a let rec ... and ... group | |
a leading ; | ; ends a let or a declaration; it cannot begin one |
A hint names the token it keyed on, and a word you BIND is yours: var, case, val and mut are ordinary identifiers, and a file that binds one is never told about another language. (of is not in the table at all — it is Mere's own keyword in type t = A | B of int.)
10. Status summary
- 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.