Contents

Stdlib reference (mere)

202 builtins that are always available via initial_env. Check a name's type with mere -te NAME.

Legend:

Sugar / prelude added in Phase 36 (2026-06-22)

Syntactic sugar (13): lexer / parser-level changes only; preserves 4-backend compatibility:

Prelude additions (16 of 34 entries added in Phase 36):


I/O (12)

NameTypeDescription
printstr -> unitWrite to stdout with newline
print_no_nlstr -> unitWithout newline + flush (for prompts)
print_intint -> unitPrint integer with newline
print_boolbool -> unitPrint bool with newline
print_errstr -> unitWrite to stderr with newline
read_lineunit -> strOne line from stdin; empty string on EOF
read_filestr -> strRead the whole file as text; raises on failure. On the C backend the str is NUL-terminated, so binary data silently truncates at the first 0x00 byte (the interpreter's strings carry NULs) — use read_file_bytes for binary (v0.1.43)
read_file_bytesstr -> Vec[R, int]Read the whole file as raw bytes — one int (0..255) per byte, binary-safe on every supported backend. interp + C only (v0.1.43, CRC-32 probe). Costs eight bytes per byte; prefer read_bytes
read_bytesstr -> bytesThe whole file as a bytes: one byte per byte, and no NUL hazard. interp + C (v0.1.216, mpng dogfood)
write_bytesstr -> bytes -> unitWrite a bytes to a file. interp + C
bytebuf_newint -> ByteBuf[R]n zeroed bytes, region-bound and mutable. One byte per byte, with random access — which bytes (immutable) and StrBuf (append-only text) leave uncovered (v0.1.218, mpng dogfood)
bytebuf_lenByteBuf[R] -> int
bytebuf_getByteBuf[R] -> int -> intThe byte at an index; out of bounds is an error
bytebuf_setByteBuf[R] -> int -> int -> unitWrite a byte (masked to 0..255)
bytebuf_pushByteBuf[R] -> int -> unitAppend, growing the buffer
bytes_of_bytebufByteBuf[R] -> bytesFreeze a copy, which can then leave the region
bytebuf_of_bytesbytes -> ByteBuf[R]The other way, for editing
print_bytesbytes -> unitWrite a bytes to stdout, unbuffered and with no newline. This is what print_no_nl cannot be: a str is NUL-terminated in the compiled backends, so a zero byte ended the output there and did not on the interpreter. All four backends (v0.1.216, Wasm in v0.1.219)
write_filestr -> str -> unitWrite content to path (overwrite); raises on failure

The FFI byte arena's `bytes` bridge (v0.1.282). tcp_read and friends write into an integer-addressed arena, and mem_to_str cannot bring binary back out — it stops at the first zero byte, which every binary protocol has. Declare these as extern fn alongside the rest of the mem_* family:


extern fn mem_to_bytes: int -> int -> bytes;          // arena ptr, len -> bytes
extern fn mem_copy_bytes: int -> int -> bytes -> int; // arena ptr, offset, bytes -> written

Native (C) and a Wasm component both provide them; scripts/socket_parity.sh requires the two backends to agree. In a plain Wasm build they become an env host import like any other extern, so a host that does not provide them fails at instantiation with a missing-import error rather than a wrong answer.

write_file_bytesstr -> Vec[R, int] -> unitWrite an int vec as raw bytes (each element 0..255) — the write half of the binary path; PPM P6 etc. interp + C only (v0.1.44, Mandelbrot probe)
read_lines ⚡ ★str -> str listRead line by line, returns str list (Phase 19.6; depends on prelude)
file_existsstr -> boolWhether path exists (Phase 19.6; on C native since v0.1.15)
file_mtimestr -> floatModification time in seconds; raises if the path is missing (interp + C native)
file_sizestr -> intFile size in bytes (stat); binary-safe length where str_len (strlen) stops at a NUL. interp + C native (v0.1.21)
file_openrwstr -> FileOpen a read/write handle, creating the file if absent and not truncating it. The handle for everything below (v0.1.115, mbtree dogfood)
file_preadFile -> int -> int -> Vec[R, int]Read at most len bytes starting at an offset; a read past the end comes back short rather than padded
file_pwriteFile -> int -> Vec[R, int] -> intWrite a byte vec (each element 0..255) at an offset, extending the file if it writes past the end; returns the count written
file_pwrite_bytesFile -> int -> bytes -> intThe same over bytes, without exploding a byte string into one boxed int per byte (v0.1.222, mraft dogfood)
file_fsyncFile -> unitForce the OS to commit this handle's writes to stable storage. The difference between "written" and "durable", and what a store calls at a commit point
file_closeFile -> unitClose the handle
env_varstr -> str optionFetch env var; None if unset (Phase 19.6; depends on prelude)
argsunit -> str listThe program's own args (after the script path / binary name); consistent interp ↔ native since v0.1.12
runstr -> intRun a command line via the shell, inherit stdio, return its exit code (interp + C native; v0.1.13)
stdin_byteunit -> intOne byte from stdin without blocking; -1 when nothing is ready. read_key blocks, which a device emulator polling a line-status register cannot afford (interp + C native)

file_exists "/etc/hosts"            // → true
env_var "PATH"                      // → Some "..."
env_var "BOGUS"                     // → None
read_lines "data.txt"               // → ["line1", "line2", ...]
args ()                             // → ["foo", "bar"] (mere prog foo bar)
run "clang -O2 main.c -o app"       // → 0 on success, nonzero exit code otherwise

Sockets are extern fn declarations rather than builtins — the C backend defines them when a program declares them (native_ffi_names in codegen_c.ml), which is why they take flat-arena offsets rather than bytes. They are native-only in practice.

nametypenotes
tcp_listenint -> intBind a listener on a port (all interfaces), SO_REUSEADDR; the fd, or -1
tcp_acceptint -> intAccept one connection; the fd, or -1
tcp_connectstr -> int -> intDial host:port; the fd, or -1
tcp_writeint -> int -> int -> intWrite len bytes from an arena offset
tcp_readint -> int -> int -> intRead into an arena offset. See the codes below
tcp_set_timeoutint -> int -> intSO_RCVTIMEO / SO_SNDTIMEO in milliseconds
tcp_closeint -> unitClose the fd

What `tcp_read` returns (v0.1.226, mraft dogfood). A count when it read something, and 0 at end of stream — a peer that closed cleanly, which is information rather than a failure. A negative result says which failure, because with a timeout set these are opposite events for the caller:

meaningwhat a caller does
-1nothing arrived before the deadlinewait again
-2the connection is gonereconnect
-3any other errorusually give up

Before v0.1.226 every failure was -1, and a program that needed the difference had to time the call and ask whether it had failed slowly enough to have been a timeout — inferring a cause from a duration. Every existing < 0 check is unaffected. scripts/tcp_read_codes.sh produces all three rather than describing them.

On Wasm the whole family works — scripts/socket_parity.sh runs the same round trip natively and under wasmtime -S inherit-network=y and compares — with two differences:

Telling them apart means decoding WASI's stream-error variant rather than its is-error bit.

no-op that returned success, so a program that set a deadline blocked forever on the next read. A bounded wait needs the native backend.

Positioned file I/O (file_openrw through file_close) works on all four backends: interp and C natively, Wasm over host imports since v0.1.153 (bytes cross in the mere_bytes layout rather than one call per byte), LLVM since v0.1.163. It is the group a paged store or a write-ahead log needs, and it was documented only in the changelog until v0.1.222 — which is how the mraft dogfood came to write its log through the Vec-taking call for a whole slice before noticing.

★ Codegen status (v0.1.246 for the first two): print_no_nl and print_err lower on interp + C + LLVM; both were refused by LLVM until it grew a write(fd, ...) for its own panic diagnostic, at which point they were three lines each. On Wasm print_err is refused: it used to write to the same host sink as print, so a diagnostic landed in the program's own output and nothing said so, and the JS host ABI has no second sink to give it. print / print_int / print_bool / read_file / write_file work in all 3 backends (Wasm goes through host imports; scripts/run_wasm.js provides puts / read_file / write_file). print_int / print_bool were the exception until v0.1.190 — this line claimed them for years while only the interpreter had them; C emitted a call to an undefined symbol and LLVM / Wasm refused outright. They now lower on all four (C through printf, LLVM and Wasm through the str_of_int they already had), locked by test/parity/print_int_bool.mere. read_lines / env_var are interpreter-only (codegen would need 'a list / 'a option construction + systematic outside-world access; not yet covered by Phases 22-31). args works on all four backends: C and LLVM read the argc/argv their main was handed, Wasm folds the host's arg_count / arg_get (v0.1.159 for Wasm, v0.1.169 for LLVM). The native-CLI / dogfood builtins run / print_err / file_exists / file_mtime / file_size / tty_raw / tty_restore / read_key / random_int also work on the C native backend (added for the mk / mrog / mwasm dogfoods, v0.1.13-v0.1.21).


let _ = print "Hello";
let _ = print_no_nl "Name: ";
let name = read_line () in print ("Hi, " ++ name);

// File round-trip
let _ = write_file "/tmp/out.txt" "hello lang";
let content = read_file "/tmp/out.txt" in print content;

Value conversion (3)

NameTypeDescription
str_of_intint -> strInteger to string
int_of_strstr -> intParse after trim; raises on bad input
bool_of_strstr -> boolTrim then "true"/"false" only; raises otherwise
float_of_intint -> floatint → float (no precision loss)
int_of_floatfloat -> intfloat → int (truncation)
float_bits_hifloat -> intThe top 32 bits of a double's IEEE-754 pattern (v0.1.281)
float_bits_lofloat -> intThe bottom 32 bits (v0.1.281)
float_of_bitsint -> int -> floatfloat_of_bits hi lo — the inverse of the two above (v0.1.281)
f32_bitsfloat -> intThe 32-bit IEEE-754 pattern of the float32 nearest this double. The narrowing is the rounding (round-to-nearest-even); a value with no float32 becomes ±inf rather than wrapping (v0.1.281)
float_of_f32_bitsint -> floatA float32 pattern back to a double (v0.1.281)

Why the bits come out in two halves. A double's pattern read as one signed int64 does not fit the interpreter's native int — which is OCaml's, and 63-bit — for a large share of ordinary values: -1.5, 1e308, inf and nan all exceed it. A single 64-bit accessor would therefore answer differently on the interpreter than on every compiled backend, for a literal as plain as 1e308. Each 32-bit half is always below 2^32, so there is nothing left to diverge about. Pinned by test/parity/float_bits.mere on all four backends.

These are the primitive the rest is built from: contrib/proto/wire.mere writes a protobuf double as put_double and a float as put_float on top of them, and neither the caller nor the generated codec learns about the split.

str_of_floatfloat -> strFloat to string (OCaml semantics)
float_of_strstr -> floatParse after trim; raises on bad input

str_of_int 42        // "42"
int_of_str "  -7  "  // -7
bool_of_str "true"   // true

String operations (22)

NameTypeDescription
str_lenstr -> intByte length
str_containsstr -> str -> boolSubstring containment
str_starts_withstr -> str -> boolPrefix test
str_ends_withstr -> str -> boolSuffix test
str_countstr -> str -> intNon-overlapping occurrence count
str_index_ofstr -> str -> intFirst position of needle; -1 if not found. Empty needle returns 0 (Phase 19.1)
str_splitstr -> str -> str listSplit by delimiter; returns str list. Requires type 'a list = ... declared. Empty delimiter returns a single-element list (Phase 19.1)
utf8_lenstr -> intCodepoint count (a str is bytes; str_len is the byte length). Invalid bytes count as single units (v0.1.38)
utf8_charsstr -> str listSplit into codepoints — the building block for text processing (v0.1.38)
utf8_atstr -> int -> stri-th codepoint (prelude, on utf8_chars)
utf8_substr -> int -> int -> strCodepoint-indexed substring (prelude)
utf8_revstr -> strCodepoint-wise reverse — str_rev is byte-wise and scrambles multibyte text (prelude)
utf8_widthstr -> intDisplay width (East Asian Width, wcwidth-lite): CJK / fullwidth / emoji = 2 columns, combining marks = 0, halfwidth katakana = 1. utf8_len counts codepoints; terminals draw columns — use this for alignment (prelude, v0.1.45)
pad_rightstr -> int -> strPad with spaces to a display width (table columns, left-aligned); no-op if already wide enough (prelude, v0.1.45)
pad_leftstr -> int -> strRight-align to a display width — numbers in table columns (prelude, v0.1.45)
str_joinstr -> str list -> strJoin with separator. Empty list → empty string (Phase 19.1)
str_compare 🌐str -> str -> intLexicographic -1 / 0 / 1 (Phase 31.0 ported to 3 backends; sign-normalized)
str_repeatstr -> int -> strRepeat N times; raises on N<0
str_replacestr -> str -> str -> strReplace all; empty needle = no change
str_revstr -> strReverse string
str_trimstr -> strStrip leading/trailing whitespace
str_unescapestr -> strDecode \n \t \r \\ \" \/; raises on unknown escape
substringstr -> int -> int -> strs[start:end_excl]; raises on out of range
char_atstr -> int -> strIndex access (length-1 str); raises on OOB
chrint -> strint in 0..255 to single-char str; raises out of range
ordstr -> intSingle-char str to int code point; raises if length != 1
to_upperstr -> strASCII uppercase
to_lowerstr -> strASCII lowercase
is_digitstr -> boolTrue for single char in '0'..'9'; otherwise false
is_alphastr -> boolTrue for single char that's a letter
is_spacestr -> boolTrue for single char that's space/tab/\n/\r

type 'a list = Nil | Cons of 'a * 'a list;
str_split "a,b,c" ","                          // ["a", "b", "c"]
str_join "-" ["alpha", "beta", "gamma"]        // "alpha-beta-gamma"
str_index_of "hello world" "world"             // 6
str_index_of "hello" "xyz"                     // -1

★ Codegen status: str_index_of / str_split / str_join / str_count / str_compare / str_trim / str_starts_with / str_ends_with / str_contains / str_replace / str_repeat / str_rev all work across all 4 backends (Phase 19.1.1 added str_index_of; Phase 22 added str_split / str_join; Phase 26.5 added all Wasm str ops; Phase 31.0 added str_compare; Phase 36 added str_trim / starts_with / ends_with / contains / replace / repeat / rev). not / abs / min / max / clamp / chr / ord / to_upper / to_lower / even / odd / gcd / bool_of_str also reached the 3 backends in Phase 36. The fn (_: unit) -> body wildcard parameter was also parser-fixed in Phase 36.


str_replace "foo bar foo" "foo" "X"           // "X bar X"
substring "hello world" 6 11                  // "world"
char_at "abcdef" 2                            // "c"
"world" |> str_contains "hello world"         // true (pipe + curry)
str_unescape "a\\nb"                          // a + newline + b (3 chars)

Numeric operations (23)

NameTypeDescription
minint -> int -> intSmaller
maxint -> int -> intLarger
absint -> intAbsolute value
signint -> int-1 / 0 / 1
clampint -> int -> int -> intclamp lo hi x restricts to [lo, hi]
powint -> int -> intbase^exp by square-and-multiply; raises on negative exp
squareint -> intx x
cubeint -> intx x x
incrint -> int+1
decrint -> int-1
evenint -> booln mod 2 == 0
oddint -> booln mod 2 != 0
gcdint -> int -> intEuclid (handles negatives and 0 correctly)
lcmint -> int -> inta/gcd b; 0 in input → 0
divmodint -> int -> (int * int)(quotient, remainder); raises on 0 div — the check is its own, because bare / by zero raises on interp and returns 0 on C and LLVM
sum_rangeint -> int -> intSum over lo..hi (Gauss formula, O(1)); halves inside the product, so it is portable over the whole range its result can hold
notbool -> boolLogical negation
bit_andint -> int -> intBitwise AND on the backend's native int width (v0.1.42)
bit_orint -> int -> intBitwise OR (v0.1.42)
bit_xorint -> int -> intBitwise XOR (v0.1.42)
bit_notint -> intBitwise complement; numerically -x - 1 on every backend (v0.1.42)
bit_shlint -> int -> intShift left. Keep counts in 0..62 for portable code — int is 64-bit on C, LLVM and Wasm (widened in v0.1.96 / v0.1.127), 63-bit on interp
bit_shrint -> int -> intArithmetic (sign-propagating) shift right; bit_shr x n equals floor division by 2^n on every backend (v0.1.42)

★ A transcendental is not correctly rounded by anybody (v0.1.248). exp and log reach C through libm, LLVM through @llvm.exp.f64, and Wasm through the host's Math.exp — and measured, exp -10 is 4.5399929762484854e-05 on three of those and 4.539992976248485e-05 through JavaScript. That is not a bug in any of them. test/parity/exp_log.mere therefore prints exact values only at the points that are exact in binary floating point (exp 0, log 1) and asserts everything else as an identity within a tolerance — which still fails an exp that returns its argument or a log wired to log10, and does not report the C library's build options as a difference.

★ Integer `/` and `%` by zero raise (v0.1.247): division by zero and modulo by zero, catchable with try_or, on the interpreter and the C, LLVM and Wasm backends. It cost a branch per division to make that true, and it was worth it because the alternative was not one behaviour but four: the interpreter raised, the C backend emitted a bare a / bundefined behaviour in C, which an arm64 build answers with 0 and an x86-64 build answers with SIGFPE — LLVM emitted sdiv, which is undefined in IR and licenses the optimizer to assume it cannot happen, and Wasm trapped with no message at all. INT_MIN / -1 is the other undefined case and wraps now, which is what the interpreter already did.

The `-rv` backend is the exception, and it is measured rather than assumed: under QEMU's virt board, 17 / 0 is -1 and 17 % 0 is 17 there — the RISC-V specification's non-trapping answer. That backend targets bare metal, where there is no stream to write a diagnostic to and no process to exit: the platform's answer is the answer. Float division is IEEE on every backend and keeps giving inf / nan.

★ On the width these are actually computed at (v0.1.245): a builtin can be present on every backend, answer every small question correctly, and still be implemented at a narrower width than the language's int. gcd was a static int __lang_gcd(int, int) in the generated C — gcd 3037000493 3037000493 came back as 1257966803 — and int_of_str on LLVM parsed with strtoll and then truncated the result to i32, so the largest int read back as -1. Both were invisible to every existing test and to host-matrix.md, because the arguments used to probe a builtin were all one or two digits.

test/parity/int_width.mere is the gate for this: every deterministic int builtin, with arguments above 2^31, held to one answer on all four backends. It found the second bug while being written for the first. Values there stay inside ±(2^62 − 1) so that the interpreter's 63-bit int is not itself the difference — and note that an intermediate counts: sum_range used to form a product twice the size of its own answer, which made it portable over only half the range its result could hold.

Float arithmetic (4)

Note (v0.1.44): the infix operators + - * /, all comparisons, and unary - are numeric-overloaded and work directly on floats, on every backend — prefer them. The f_ functions below remain as ordinary function values (useful for passing to higher-order functions). The overload resolves to float only when an operand is concretely float; annotate fn params (fn (x: float) -> ...) in float-heavy code.
NameTypeDescription
f_addfloat -> float -> floatAddition
f_subfloat -> float -> floatSubtraction
f_mulfloat -> float -> floatMultiplication
f_divfloat -> float -> floatDivision (IEEE 754: 0 div is inf/nan)
f_ltfloat -> float -> boolLess than
f_lefloat -> float -> boolLess than or equal
f_gtfloat -> float -> boolGreater than
f_gefloat -> float -> boolGreater than or equal
f_negfloat -> floatUnary minus (Neg is int-only, so use this for float)
f_absfloat -> floatAbsolute value
sqrtfloat -> floatSquare root (NaN for negatives)
floorfloat -> floatFloor
ceilfloat -> floatCeiling
roundfloat -> floatRound
f_minfloat -> float -> floatSmaller (Phase 19.7)
f_maxfloat -> float -> floatLarger (Phase 19.7)
f_powfloat -> float -> floatPower base ^ exp (Phase 19.7)
logfloat -> floatNatural log (Phase 19.7; all 4 backends in v0.1.248)
expfloat -> floate^x (Phase 19.7; all 4 backends in v0.1.248)
sinfloat -> floatSine (radians; Phase 19.7)
cosfloat -> floatCosine (Phase 19.7)
tanfloat -> floatTangent (Phase 19.7)
atan2float -> float -> floatatan2 y x for angle (Phase 19.7)
random_int ★ ⚡int -> intrandom_int n returns int in 0..n-1; raises if n<=0 (Phase 19.7)
random_floatunit -> floatFloat in [0.0, 1.0) (Phase 19.7)
pifloatπ ≈ 3.14159265 (constant builtin)
efloate ≈ 2.71828183 (constant builtin)

★ Codegen status: the 11 entries added in Phase 19.7 are interpreter-only. Codegen support requires libm linking or per-backend wiring of built-in math functions, planned for a follow-up slice (19.7.1).


f_add 1.5 2.5                    // 4.0
f_div 10.0 4.0                   // 2.5
3.14 |> f_mul 2.0                // 6.28

clamp 0 100 150                  // 100
pow 2 10                         // 1024
gcd 12 18                        // 6
sum_range 1 100                  // 5050
fst (divmod 100 7) + snd (divmod 100 7)   // 14 + 2

Control / error (3)

NameTypeDescription
fail ⚡ ★str -> 'aPanic that unifies with any type
assertbool -> str -> unitOn false, raises "assertion failed: MSG"
try_or(unit -> 'a) -> 'a -> 'aEvaluate the thunk; catch Eval_error and return default

let safe = fn s -> try_or (fn () -> int_of_str s) (- 1);
safe "42"      // 42
safe "abc"     // -1

if x < 0 then fail "negative" else x

fail is polymorphic, so type inference works at branch merges (if c then fail msg else int_val → int).

★ What an uncaught failure does (v0.1.246): the program writes one line to stderr and exits 1, on every backend. The line is the message raised, tagged fail: when it came from the fail builtin — the tag belongs to the builtin, so a backend's own failures (int_of_str on junk, an out-of-range index) are not tagged, which is what the interpreter has always done. The interpreter additionally prefixes the source file it is running; a compiled binary has none.

None of that was true before. The same program exited 1 on two backends and 134 (SIGABRT) on two others, wrote its diagnostic to stderr on two and stdout on two, tagged the message on three and not on the fourth, and int_of_str on junk named the offending input on two backends and not on the other two. It went unnoticed because the parity harness compared stdout and nothing else, so no parity test used `fail` — none could have passed. test/parity/fail/*.mere is the gate now: exit status, the output written before the failure, and the message, on all four.

Two limitations remain, and both are pinned rather than described:

so the diagnostic lands in stdout there. The harness knows this and would break if it changed. Under --component the same backend writes through WASI.

out, so statements after it in the same body still run: inside a try_or thunk, work that follows the failure happens. test/parity/failure_caught.mere holds the other three to one answer and declares Wasm's exact output in a .wasm.expected file next to it, so the day that backend learns to unwind, the declaration breaks and says so.

try_or catches all of these on all four backends, including the ones raised inside the backend rather than by fail. What it hands back is the default — not the message: the language can observe that something failed, not why.


Polymorphic helpers (8 ★)

NameTypeDescription
show'a -> strStringify any value via to_string
id'a -> 'aIdentity function
fst('a * 'b) -> 'aTuple first
snd('a * 'b) -> 'bTuple second
pair'a -> 'b -> ('a * 'b)Tuple constructor (curried)
swap('a * 'b) -> ('b * 'a)Tuple swap
const'a -> 'b -> 'aDrop second arg, return first
flip('a -> 'b -> 'c) -> ('b -> 'a -> 'c)Reverse arg order of a curried fn (higher-order)

show 42                          // "42"
show (Some 5)                    // "Some 5"
show [1, 2, 3]                   // "[1, 2, 3]"   (Cons/Nil chains shown as [..])
show [Some 1, None, Some 3]      // "[Some 1, None, Some 3]"

fst (pair "hi" 42)               // "hi"
let always_7 = const 7 in always_7 "anything"   // 7
let sub = fn a -> fn b -> a - b in (flip sub) 3 10   // 7 (= sub 10 3)

JSON, derive-style (5 ★)

Structural JSON, compile-time-specialized per type (no trait machinery), like show. to_json works on all four backends — on LLVM it shares the emitter with show, since the two differ only in literals (v0.1.184). of_json and its siblings are interp / C / Wasm: decoding needs a JSON parser in the target language, and LLVM has no hand-written one.

NameTypeDescription
to_json'a -> strSerialize any value to JSON structurally
of_jsonstr -> 'aParse JSON into a typed value; fails fast on error (trusted input)
of_json_optstr -> 'a optionSame, but returns None on any error (safe for untrusted input)
of_json_like'a -> str -> 'aTarget type from a witness value instead of an annotation (v0.1.183)
of_json_opt_like'a -> str -> 'a optionThe non-crashing witness form

Decoding inside a polymorphic function

of_json reads the target type off the call node, which is fine at a use site with an annotation and impossible inside a generic helper: there the node's type is a variable, and the interpreter has no runtime types to resolve it with. So a generic "decode it back" had to name the record type, and every record needed its own copy.

A witness supplies the type instead. The interpreter reads it off the value's runtime shape — a record carries its type's name — and the compiled backends read it off the witness's static type, which is the same variable the result unifies with:


let with_field = fn (rec_) -> fn (name: str) -> fn (v: str) ->
  ... of_json_opt_like rec_ (rebuilt_json) ...

The witness is a value the caller already has whenever this comes up: replacing one field of a record means holding the record. contrib/schema is this, and examples/claims generates its whole form from it.

A polymorphic record still needs the annotation — a value carries its type's name but not its type arguments, so a witness cannot describe Box[int].

The of_json result type comes from the use site — annotate the expression: (of_json s : T). A JSON object maps to a record's fields (by name), an array to a list or tuple, null/value to option (None / Some), and a string / {"Ctor": payload} to a variant. to_json uses the same mapping in reverse, so (of_json (to_json x) : T) == x.


type User = { id: int, name: str, bio: str option };
to_json (User { id = 1, name = "ada", bio = None })
                                 // {"id":1,"name":"ada","bio":null}
let u = (of_json body : User);   // fails fast if body is malformed
match (of_json_opt body : User option) with
| Some u -> u.name               // decoded
| None   -> "bad request"        // malformed / missing field — no crash

Comparison, derive-style (v0.1.11)

== / != (structural equality) and < <= > >= (structural ordering) are compile-time-specialized per operand type — the same no-trait mechanism as show / to_json. Both work on interp / C / Wasm.

lexicographically).

order; lists compare element-wise (a shorter prefix is smaller); variants order by declaration order (the constructor listed first is smallest), then by payload. All backends agree byte-for-byte, so a value sorts the same under the interpreter, a native binary, and Wasm.


(1, 2) < (1, 3)                        // true  (tuple, lexicographic)
[1,2] < [1,2,3]                        // true  (prefix is smaller)
type C = Red | Green | Blue; Red < Blue // true  (declaration order)
list_sort_by (fn (a: float) -> fn (b: float) -> a < b) [3.1, 1.2]  // [1.2, 3.1]

Honest edges. float uses a total order where NaN sorts as least. Comparing two functions is defined but meaningless (they order as equal). The bare default list_sort still bakes in an int comparison — its comparator's type variables default to int, the same rule that keeps fn a -> fn b -> a < b monomorphic — so sorting a non-int list needs list_sort_by with an annotated comparator (as above). A fully-polymorphic list_sort over any orderable element would need ad-hoc-polymorphism resolution (deferred).


Loop helper (1 ★)

NameTypeDescription
iter_nint -> (unit -> unit) -> unitApply thunk N times (side-effect loop); no-op when N≤0

Capability (2 + 2 builtin record types)

Used by the effect system (see effects.mere). The Logger and Metrics cap types are pre-registered as builtins. Users can also override with their own type Logger = ....


type Logger  = { info: str -> unit, warn: str -> unit, error: str -> unit };
type Metrics = { inc: str -> unit, record: str -> int -> unit };
NameTypeDescription
mk_loggerstr -> LoggerCreate a prefixed Logger. Each field prints as prefix [LEVEL] msg
mk_metricsunit -> MetricsCreate a Metrics. inc / record print as [METRIC] ...

let lg = mk_logger "app" in
{ lg.info "started";
  lg.warn "slow query";
  lg.error "abort" }

let m = mk_metrics () in
{ m.inc "users";
  m.record "latency_ms" 23 }

For a complete cap-passing example see examples/effects.mere.

Raw memory, CSRs, traps and tasks (14, RV32I bare-metal only)

A Raw is a window onto physical memory — the one capability that is not a record of functions, because its operations lower to load and store instructions. It is the escape hatch a device driver needs, and it is a value rather than an ambient builtin so that "this function cannot touch raw memory" is something you read off a signature.

Raw is opaque: nothing constructs one, and there is no function that mints one. The only source is the argument mere -rv --bare hands to the program's top-level main, and raw_window can only narrow it. Offsets are relative to the window, so a driver holding a UART window cannot express an address outside it; every access bounds-checks the offset, and widening faults.

NameTypeDescription
raw_windowRaw -> int -> int -> RawA window over [off, off+len) of another. Faults if that is not inside it
csr_readint -> intA machine CSR by number — the number must be a literal (it is an immediate field of the instruction)
csr_writeint -> int -> unitWrite a machine CSR. Not behind a capability: a CSR has no base and length to narrow, and the hardware's privilege modes are what separate a kernel from a user process
raw_lenRaw -> intIts length — so a kernel can partition a window it was handed without hardcoding the runtime's geometry
raw_baseRaw -> intA window's base as a number. Not authority — touching anything still needs a window — but a stack pointer is an address and hardware wants the number
trap_saveRaw -> RawThe trap trampoline's 31-word register save area. A context switch is a copy through this: outgoing registers to a TCB, incoming registers back
machine_scratchRaw -> RawReserved RAM the runtime is not using — where task stacks come from. A bare program owns no fixed address of its own: the heap grows up from 2MB and the stack down from the top
closure_code(unit -> unit) -> intA closure's entry point. A task IS a closure, so starting one means building a context whose PC is this
closure_env(unit -> unit) -> intIts environment — the value the first argument register must hold when that PC is entered. ABI knowledge, which a kernel has
set_trap_handler(int -> int) -> unitInstall a trap handler. The argument is mcause; the result is the PC to resume at. Anything else (mepc 0x341, mtval 0x343) is a csr_read away. A closure, not a named function: a handler needs the machine capability to do anything useful and an interrupt has no caller to hand it one, so it captures instead. Codegen emits the trampoline that saves the register set and returns with mret
raw_peek8Raw -> int -> intThe byte at that offset
raw_peek32Raw -> int -> intThe 32-bit word at that offset
raw_poke8Raw -> int -> int -> unitStore a byte
raw_poke32Raw -> int -> int -> unitStore a 32-bit word

let putc = fn (uart: Raw) -> fn (c: int) -> raw_poke8 uart 0 c;

let main = fn (mach: Raw) ->
  let uart = raw_window mach 0x10000000 256 in    // the UART, and nothing else
  putc uart 65;

A context switch needs no new mechanism: the trampoline saves the interrupted register set to the area trap_save hands back and restores from it before mret, so a handler swaps tasks by copying through it and returning the incoming task's PC. Switch every register, gp included, and give each task a heap arena of its own (carve it from machine_scratch — heap up from the bottom, stack down from the top). Sharing one heap looks workable until a region R { } in one task rolls the bump pointer back and frees what another task allocated meanwhile; the rule that survives is that contexts share gp only if they genuinely share a heap, and a context that uses regions must not. See examples/riscv_bare_sched.mere.

Device MMIO sits above any RAM (the UART data register is at 0x10000000, the address QEMU's virt machine uses), so a device address does not move when --ram does. On every other backend these refuse: there is no honest physical address in a hosted process. See examples/riscv_bare_uart.mere.


System / constants (4)

NameTypeDescription
timeunit -> floatUnix epoch seconds (gettimeofday). For benchmarks / timestamps
exitint -> 'aExit the process with an exit code (never returns; polymorphic return)
int_maxintMax int value (OCaml runtime dependent; 2^62-1 on 64-bit) — constant builtin
int_minintMin int value — constant builtin

let start = time () in
{ run_heavy_computation ();
  print ("elapsed: " ++ str_of_float (f_sub (time ()) start) ++ " sec") }

if config_invalid then exit 1 else continue ()

iter_n 3 (fn () -> print "===")   // prints === three times

All builtins (alphabetical, 129)


abs args assert atan2 bit_and bit_not bit_or bit_shl bit_shr bit_xor
bool_of_str ceil char_at chr clamp const
cos cube decr divmod e env_var even exit exp f_abs f_add
f_div f_ge f_gt f_le f_lt f_max f_min f_mul f_neg f_pow
f_sub fail file_exists flip float_of_int float_of_str floor
fst gcd id incr int_max int_min int_of_float int_of_str
is_alpha is_digit is_space iter_n lcm log max min mk_logger
mk_metrics not odd ord pair pi pow print print_bool
print_err print_int print_no_nl random_float random_int
closure_code closure_env csr_read csr_write machine_scratch
raw_base raw_len raw_peek32 raw_peek8 raw_poke32 raw_poke8 raw_window trap_save
stdin_byte
read_file read_file_bytes read_line read_lines round show sign sin snd sqrt
square str_compare str_contains str_count str_ends_with
str_index_of str_join str_len str_of_float str_of_int
str_repeat str_replace str_rev str_split str_starts_with
set_trap_handler str_trim str_unescape substring sum_range swap tan time
to_lower to_upper try_or write_file write_file_bytes

Q-010 collection builtins (vec_* / owned_vec_* / strbuf_* / map_* / len) are registered builtins outside this table; see language-reference / tutorial. Phase 19.2 added `map_iter : Map[R, K, V] -> (K -> V -> unit) -> unit` (works in all 4 backends).


See also