Skip to content

Latest commit

 

History

History
551 lines (436 loc) · 31.2 KB

File metadata and controls

551 lines (436 loc) · 31.2 KB

Memory management model (mere)

A summary of Mere's memory-management strategies, how mere currently handles them, and what's planned. Deeper design notes live in separate internal notes.


1. Comparison of memory-management strategies

Strategy When is memory freed? Examples Strengths Weaknesses
Manual (malloc/free) The programmer calls free each time C Maximum control Frequent use-after-free / leaks
GC A runtime garbage collector decides OCaml, Java, Go, Python Safe, easy Pause times, memory overhead, poor for real-time
Ownership (move + borrow) Automatically at scope exit Rust Compile-time guarantees, zero cost Cyclic / self-references are painful; learning curve
Region (bulk per-region) The whole region scope is freed at once Cone, Vale, Cyclone, ML/Talpin research Fast alloc / bulk free; cyclic refs OK Requires designing region lifetimes
Stack-only At stack-frame exit C autos, Rust let Zero cost Strict size/lifetime constraints

Mere is designed to let you mix and match: the programmer chooses a strategy explicitly, and the compiler verifies safety.


2. Mere's memory strategies (5)

Based on the design note 01_memory_model.md:

owned T — sole ownership

A value has one owner; freed when the owner leaves scope. Equivalent to Rust's T.

let x: owned String = String.from("hello")
let y: owned String = x    // move; x is no longer usable

&borrowed T — borrow

Pass a reference without transferring ownership. Equivalent to Rust's &T / &mut T, except Mere plans to refine the borrow annotations (&shared write etc.; design Q-004).

③b region R loop x { ... } — the region-carrying loop (v0.1.296)

Long-lived state that is periodically compacted — an interpreter's heap, a server's session table — does not fit region R { }: its lifetimes nest (LIFO), and a compaction needs the NEW generation to outlive the OLD one. The loop is the hand-over-hand version: each iteration runs in a fresh arena named R; the body sees x : option C (None on entry, Some carry after) and answers region_flow[C, D] (prelude): Continue carry deep-copies the carry into the next arena — containers included, via the __mdeep family — and releases the current one; Done d copies d out and exits.

The escape rule splits where the block's could not: C may mention R (a Map[R, ...] carried from arena to arena is the construct's point), D may not — same check as a block's result. No fresh rigid variable per iteration is needed: nothing of arena N reaches arena N+1 except through the Continue copy, so one name R honestly denotes "the loop's current arena" throughout. Not carryable (refused at emit time, by type): functions (their captures hide behind a void* the deep copier cannot see into), Channel, ThreadHandle, OwnedVec, StrBuf, ByteBuf.

Measured on the C backend: 2.4 GB of dead map-entry overwrites churned through 2000 cycles, live set 51 entries — peak footprint 1.6 MiB, 0.32 s. Backends: interp + C; llvm / wasm / rv32 refuse cleanly (their reclamation is a LIFO bump rollback, which cannot express the swap).

region R { ... } — bulk per-region

Values placed in region R are freed all together when R is destroyed. Bump-allocator backed. Only Trivial types (no Drop) can live in a region. Unified with arena under Q-008.

view V[R] of T — self-referential views (Q-009)

A "bundle type" built inside a region: immutable, non-moving. Lets you express self-references without unsafe.

view DocumentView[R] of Document {
  own:    &R Document,
  tokens: &R [&R str],    // points into own.text
}

stack { ... } — stack-only

Guarantees the value lives only on the stack. No heap allocation.


3. Why region (in detail)

Problems it solves

Areas where Rust's ownership struggles:

  • Self-referential structs (Pin<T> + unsafe)
  • Graphs / cycles (escape into Rc/RefCell)
  • Bulk short-lived allocations (overhead of individual frees)
  • async + lifetimes (lifetimes threaded across function calls)

Region resolves these via bulk-free per region.

How it works

region R {
  // R is a bump allocator (just a pointer + size)
  let a = R.alloc(Node {...})    // one pointer bump
  let b = R.alloc(Node {...})    // ditto
  a.next = b                      // references freely valid inside the region
  b.next = a                      // cycle is fine
  // ... computation ...
}  // The whole region's memory is freed at once — no individual destructors

Typical uses

  • Parsers / compilers: one input = one region; AST is built, then freed in bulk.
  • One frame of a game: per-frame region; everything dropped at frame end.
  • Request handlers: one request = one region; freed after response.
  • Transactions: a region per transaction boundary.

The Trivial constraint

Only "types without Drop" (Trivial) can be placed in a region. Why: bulk free without invoking individual destructors. Types that have Drop (DB connections, file handles, etc.) are managed separately via with (Q-011 resolved):

with db = Database.connect(...) in
  region R {
    let nodes = ...   // many allocations into R
    process(db, nodes)
  }
  // R is destroyed (Trivial only; no destructors)
// db.drop() runs (it has Drop, so it's released individually)

3.5 Region blocks reclaim values (v0.1.30–31, implemented)

The "one request = one region" use case above is no longer aspirational — as of v0.1.31 it is the implemented semantics on the C backend:

  • Value allocations follow the current region. Strings, cons cells, and variant nodes allocate in a thread-local current region. A region R { ... } block makes itself current for its body, so everything the body allocates is reclaimed when the block exits. Container structs (Vec / Map / StrBuf) stay in their own binding region — they carry identity and must not die with a scratch block.

  • A container a FUNCTION builds follows the region its CALLER decided (v0.1.464, C backend). A container's region lives in its type, and a function's body used to allocate through the region variable in its own scheme, which the call site never bound: the type said the caller's block, the value went to the default region. The region is now passed IN, as leading __lang_region* arguments on the uncurried __direct twin, and a call site hands over what it bound — a block it is inside, one of its own region parameters (which is how a chain reaches a body three calls down), or the default region where nothing decided.

    What this is NOT is the callee asking what region is current at run time. That was tried in v0.1.453 and is unsound for a chain of calls, because the body and the call site hold different copies of the variable; m3d could not render a second frame, and v0.1.456 withdrew it. Passing it in makes the type and the value say the same thing, so carrying the result out of the block stays the type error it already was.

    Per backend: the C backend passes it. The LLVM backend does not yet — it has no uncurried twin to hang the argument on, so its region_reclaim_check leg asserts the old behaviour and goes red when it is taught. Wasm needs nothing: it has one bump for every region, and what keeps a callee's allocation alive there is the high-water mark (Q-132), not a region argument. RV32I needs nothing either: it does not reclaim regions at all. Those two are not "unimplemented" — there is no argument for them to pass.

  • Closure environments follow the current region too (v0.1.290). They used to be allocated in the default (program-lifetime) region for one reason: a closure's env is a type-erased void*, so nothing could copy it out of a dying block, and a permanent allocation was the only safe answer. A closure now carries a third member beside env and fn — a copy function generated per env type, which region-allocates a fresh env and deep-copies each captured field through that field's own __mcopy_<tag>. __mcopy for an arrow calls it, so a closure leaves a region block the way every other value does. A closure with no captures (env == NULL) and an FFI adapter holding a borrowed pointer leave copy zero and are copied shallowly, exactly as before.

  • The copier lives on the ENV, not in the closure (v0.1.292). Every env struct starts with {__lang_region* __r; void* (*__copy)(region*, void*);}, and __mcopy for an arrow reads that header through the void* it already has. So the closure struct stays {env, fn} — two pointers, for every program. That matters because a closure is passed by value through every frame of an interpreter's dispatch: a third pointer per frame took a 512 MB-stack interpreter over the edge on a deeply recursive program, and ld caps -stack_size at 512 MB on arm64, so there is nothing to buy it back with. The __r field also makes copying idempotent: copying an env into the region it is already in returns it unchanged, so a chain of envs capturing closures does not copy without bound, and a shared env keeps its identity instead of becoming two mutable states. An FFI adapter's env is a real struct too — a header plus the borrowed pointer, in the default region, with __copy = NULL meaning "do not copy me". One field answers both questions, and every closure with a non-NULL env can be asked.

  • The block's result is copied out into the enclosing region (per-type deep copy, specialized like the show/== derive family), so returning a value from a block is always safe. A container cannot be a block result — the typer's region-escape check rejects it.

  • Containers own their contents (v0.1.30 copy-on-store): map_set deep-copies the key and value into the map's own region; vec_push / vec_set copy the element. A stored value therefore outlives whoever stored it, no matter where it was allocated. Strings are immutable, so the copies are unobservable.

    • Overwriting a heap element repeatedly leaks the old copies. Because the container's region is bump-allocated, vec_set/map_set copying a new string into a slot that already held one cannot reclaim the old copy — so a hot loop that overwrites the same slot with fresh strings grows O(writes), not O(1) (measured: 4M vec_set of a fresh string into index 0 ≈ 550 MB). Scalar (int/bool) elements are unaffected (their "copy" is a no-op). Eliding the copy for a value already resident in the container's region needs type-level region tracking on str (deferred); until then, prefer scalar slots for hot-overwrite loops, or a fresh per-batch region. (map_set on an existing key copies only the value, v0.1.77, so a counter keyed by repeated strings stays O(distinct keys).)
  • Channel payloads copy through a per-message region: channel_send deep-copies into a malloc-backed message region; channel_recv copies out into the receiver's current region and frees the message. A sender's scratch region can die while the message is in flight.

  • Measured effect: an idiomatic line-at-a-time counter with a per-line region runs at constant ~1.5 MB RSS over 8M lines (previously 246 MB); a Redis-wire KV server with per-command regions holds flat RSS under sustained load.

  • Ready-made streaming combinators. contrib/stream packages the per-line region loop so a tool does not have to hand-roll it: import "contrib/stream/stream.mere"; Stream.each_line path cb (side effect per line) and Stream.count_lines path pred keep peak memory at O(longest line) — measured 1.4 MB vs 61.7 MB for the naive loop on a 57 MB file. The callback must not let the line escape (storing it into an outer container deep-copies it out and reintroduces O(file) growth); this is the ergonomic path a streaming line tool wants.

Backend note: the interpreter is GC-backed (same value semantics, memory behaviour trivially fine).

The LLVM backend gained the same semantics in v0.1.443. It had none of it before: every value allocation named @__lang_default_region whatever region blocks were around it, region R { } was a stack alloca that only explicitly region-typed things went into, and there was no per-type copy-out because nothing it held needed copying out. On test/regionreclaim/pertree.mere -- a tree built inside a region block, only a scalar leaving it -- 100 iterations at depth 16 cost 2.5 MB on C and 316 MB on LLVM; it is 5.8 MB now, and flat in the iteration count like C's.

Both printed the same answer throughout, which is why the parity suite never saw it: parity compares what a program prints, and this was a difference in what it held. scripts/region_reclaim_check.sh is the gate that does see it, and it was written to fail either way -- it pinned the gap while the gap existed, went red when v0.1.443 closed it, and now pins the reclamation.

Two things the LLVM backend refuses as region-block results, both by the same rule the C backend uses: a container (identity -- a copy would be a different object) and a function (its captured environment lives in the block, and this backend has no environment copier). A result whose type never resolved is not copied at all, because the only values that reach a region boundary without a concrete type are the shared nullary nodes of a boxed variant, which live outside every arena.

The Wasm backend reclaims region blocks as of v0.1.37 (mark on the value stack + result copy-out via $__mcopy_<tag>; boxed results -- lists, tuples, records, variants, floats, bytes -- copy out correctly since v0.1.418, before which only scalar results assembled), with one honest difference from C: there is no per-container storage, and therefore no copy-on-store. Until v0.1.458 the answer to that was to make escaping stores compile errors. It was the wrong answer twice over — the same store performed by a callee went through and corrupted memory, and the exemption for unboxed elements let the container's reallocated buffer dangle instead of the element (Q-132) — because a guard that has to be right about every store in the program cannot be syntactic.

The answer now is a high-water mark. A store into a container that predates the innermost open block raises $__lang_hwm to the current bump, and the block's exit restores max(its mark, hwm) instead of its mark. Sound because the value stored, and any buffer the store reallocated, were allocated before that point and lie below the bump; conservative because the block keeps its other garbage too — which is what the C backend does with a __heap value anyway, namely never free it. It costs one global compare when no block is open, and nothing at all for a block that touches only its own containers.

What is still refused inside a region block is what a mark cannot help with: channel_send, spawn, and closure-registering externs. Those hand the value to another thread or to the host, on no schedule ordered with the block's exit.

Measured: the playground 2048 with a per-move region holds its bump pointer constant across 30,000 moves.

The default (program-lifetime) arena grows by chaining blocks rather than aborting when it fills: when an allocation would overrun the current block, a geometrically larger block is malloc'd and chained on, and blocks never move so existing pointers stay valid. The C backend gained this in v0.1.25 (a long-running server exhausted the old fixed-cap arena); the LLVM backend gained the same bounds-check-and-grow in v0.1.82 — before it, its __lang_region_alloc was a pure bump with no bounds check, so an allocation-heavy program (e.g. a per-pixel renderer) silently overran the 4 MB arena and crashed while interp/C/Wasm agreed. Guarded by test/parity/region_growth.mere (a checksum over ~6 MB of live region allocations, matching across all four backends).

Containers grow in place when they can (v0.1.414). vec_push, strbuf_push and bytebuf_push double their buffer when it fills. In a bump arena the old buffer cannot be handed back, so before v0.1.414 a container built by pushing cost twice its final size in cumulative allocation -- the matmul benchmark allocated 12.6 MB for three 2 MiB matrices, to within 248 bytes of exactly 3 x (2 + 2) MiB. Now, when the buffer being grown is the most recent allocation in its region and the current block has room, the bump pointer simply moves and the buffer keeps its address; when a large buffer lives in a dedicated block of its own (the v0.1.307 policy for allocations bigger than a quarter of the bump block), that block is realloc'd and the chain re-linked; otherwise the growth is a fresh allocation plus a copy, as before. The program cannot observe which path ran -- only alloc_total and the footprint change (matmul 12.6 -> 6.3 MB; a 200,000-int Vec 4 -> 2 MiB, which scripts/region_slack_check.sh now holds). The C backend does all three; the LLVM backend does the in-place path for Vec; the Wasm backend does it for Vec only while no region block is open, because its blocks are a rollback of the one bump pointer and extending an outer container's buffer into a block would be undone with it. A Map still copies its arrays on growth.

Lists can be built in order (v0.1.416). ListBuf[R, T] (lb_new / lb_push / lb_to_list, stdlib reference) appends by splicing the previous cell's tail, so a list produced first-to-last costs one cell per element instead of two -- the reversed accumulator an immutable list otherwise needs was 23 MB of the JSON benchmark's 105 MB. Its cells reference the pushed values without copying, which is sound because a push is refused while a different region is current (and after lb_to_list has handed the cells out); the builder itself is a container and obeys the escape rules above.


4. Current state in mere (as of 2026-06-24, Phase 46)

The "Phase 2" section below is a record of the first implementation slices (the region/view syntax layer). Phase 11 → 31 implemented the 4 borrow modes (&R T / &mut R T / &shared write R T / &exclusive R T) + borrow checker + with Drop integration + the 4 Q-010 collections (Vec / OwnedVec / StrBuf / Map) + 4-backend codegen (interp + C / LLVM / Wasm) parity. Details: language-reference.md §3 region/view/with / codegen.md §4.

Added in Phase 36 (2026-06-22): a narrow value restriction in the typer. let v = map_get m k in ...-style let-binds through mutable containers are no longer generalized (so 'a doesn't leak). Details:

  • is_value e decides whether an expression is in value form (literal / fn / Var / Tuple of values / etc.).
  • ty_mentions_mutable_container t decides whether a type contains OwnedVec[T] / Map[R, K, V] / StrBuf[R].
  • Generalization is suppressed only when both fail (non-value AND contains a mutable container).

This is a narrowed variant of ML's standard value restriction (limited to types involving mutable containers) — ordinary fn lets like let inc = fn x -> x + 1 remain polymorphic.

Added in Phase 38.G-1 (2026-06-22): automatic scope-bound Drop for let v = owned_vec_new () in body (Level 1). Implements N1 of the N1/N2/N3 decomposition from the 39_nll_linear_design.md design notes:

  • If body doesn't let v escape lexically, free(v->data) is auto-emitted at scope end (same shape as the Phase 15.13 with).
  • Static analysis: no_value_leak v body (v doesn't appear inside Tuple / Constr / Record_lit / Fun) + tail_does_not_return_v v body (body's tail expression's type doesn't include OwnedVec).
  • If both pass → auto-Drop; if either fails → fall back to the existing registry + main-end sweep (safe-by-default).
  • Implemented in C + LLVM; Wasm uses a bump-arena scheme that doesn't need per-allocation free.
  • Level 2 (NLL Light: drop at last use) and Level 3 (Full Linear: static use-after-move detection) remain deferred (see DEFERRED §1.3).

This is a compromise from Mere's "explicitness > brevity" philosophy: explicit with is still supported and recommended (for custom Drop types), and the typical OwnedVec pattern (build → query → return scalar) gets auto-Drop.

What works (Phase 2: syntax + value expressions + escape check + view declarations + region-enforced construction + field-access region propagation)

  • region R { body } — introduces R as a region name in scope.
  • &R T — region-tagged reference type.
  • &R v — region-tagged value expression.
  • Escape check — if R leaks into the type of region R { body }'s body, it's a compile error.
  • view V[R] of T { fields } declarations.
  • View region enforcement (Phase 2.3) — view construction allowed only inside a region block.
  • Type-level region tag on view values + field-access region propagation (Phase 2.4) — a view value's type carries the construction-time region as Name[R]; field accesses / record updates substitute it with the actual region. The view value itself is subject to escape checking (cannot leave the region).
> region R { 42 }
- : int = 42

> fn (x: &R int) -> x
- : (&R int -> &R int)

> region R { let x = &R 5 in 42 }
- : int = 42                              // &R used inside, but result is int → OK

> region R { &R 5 }
ERROR: region escape: `&R int` cannot leave region `R`

> region R { region S { 100 } }            // nesting OK
- : int = 100

> view Node[R] of int { value: int, next: int };
  region R { let n = Node { value = 1, next = 0 } in n.value }
- : int = 1                                // view construction is only inside a region

> view Slot[R] { item: &R int };
  region S { let s = Slot { item = &S 42 } in 100 }
- : int = 100                              // R is substituted to S

> view Node[R] of int { value: int };
  let n = Node { value = 1 } in n.value    // outside any region
ERROR: view Node must be constructed inside a region block

What doesn't work yet

  • Child regions (region S of R { ... }) — promotion across nested regions.
  • Integration of with + Drop — lifecycle of Drop-bearing caps.

Why "syntax only"

mere is a tree-walking interpreter written in OCaml, and in interpreter mode the actual memory management is done by OCaml's GC.

In Phase 4 codegen (C output), region becomes a real bump allocator (achieved 2026-06-18, Phase 4.17). region R { body } initializes the C runtime's __lang_region; &R v (sugar for R.alloc(v)) bump-allocates inside the region and returns T*; leaving the scope releases it all at once. Combined with the typer's escape check, leaving a region scope frees memory while the type signature guarantees no &R T value has leaked. Details in codegen.md's Phase 4.17.


5. View types — self-referential / cyclic structures inside a region

A region is "a box that shares the same lifetime"; we also need a way to safely express structures that point at each other inside (graphs, linked lists, ASTs, JSON trees, etc.). That's what view types are for.

Motivation: weak spots of ownership

// In Rust this is nearly unwritable: two mutually-referencing nodes
let a = Node { value = 1, next = ??? }  // want ??? to be b
let b = Node { value = 2, next = a }    // but a should also point to b

In ownership-based languages, cyclic references are fundamentally hard (Rc<RefCell<T>>, unsafe, custom arenas, etc.). Inside a region, everyone shares the same lifetime, so cycles are fine. View types capture this "in-region relational structure" as a type.

Three axioms (Q-009 paper-validated)

Axiom Meaning
immutable View values cannot be modified after construction; cannot be reassigned.
region-scoped Always tied to some region R; cannot leave R (the type bakes in [R]).
structural identity by region Same-typed views inside the same region are identified — this is the basis on which cyclic references work safely.

Difference from records

type Point = { x: int, y: int };       // ordinary record: lifetime via GC; region-independent
view Node[R] of int { value: int };    // view: bundle type tied to region R
  • A record is just data. A view is a structure with the region tag baked into the type (the [R] in Node[R]).
  • Field types can include &R T: view Node[R] of int { next: &R Node[R] } for self-reference.
  • View values can only be accessed through &R Node (structural identity is per region).

Why "view"?

The physical layout (the inner type of T) and what the programmer manipulates (the Node type) are viewed as different things. Enables expressions like "internally a sequential int, but viewed as a Node struct" (a planned future feature).

Current state (Phase 2.4, 2026-06-17)

View construction is restricted to inside a region block; the region parameter R at the declaration site is substituted with the innermost active region's name at construction time; and the view value's type itself carries the region tag as Name[R]. Field accesses / record updates propagate the region, and the view value is subject to escape checking.

view Node[R] of int { value: int, next: int };
region R { let n = Node { value = 1, next = 0 } in n.value }    // 1

view Slot[R] { item: &R int };
region S { 
  let s = Slot { item = &S 7 } in
  s.item                                                         // : &S int (R → S propagation)
}                                                                // ERROR: &S int would escape

region S { 
  let s = Slot { item = &S 7 } in
  let take_s = fn (x: &S int) -> 99 in
  take_s s.item                                                  // 99 (s.item is &S int)
}

region S { Cell { v = 1 } }    // ERROR: Cell[S] cannot leave region S
let n = Node { ... }            // ERROR: must be inside a region block

Tightening planned for later phases

  • Cyclic construction within the same region (a two-phase model: mutable construction phase + immutable use phase).
  • Making views reachable only through &R V (currently the view value appears in types directly).
  • Q-009's "structural identity by region" axiom (a strict semantics that identifies same-typed views).

The detailed design lives in the internal design notes (Q-009 resolved).


6. Roadmap

Phase 2 (medium-sized, ~600-800 LoC, multiple slices) — in progress

  • &R v value expressions (Phase 2.1, 2026-06-16).
  • Region escape check (&R T cannot leak outside R, Phase 2.1).
  • view V[R] of T { ... } declarations (Phase 2.2, 2026-06-16, Q-009 paper-validated).
  • Region enforcement on view (construction only inside a region + R is substituted to the active region at construction, Phase 2.3, 2026-06-16).
  • Type-level region tag on view values + field-access / record-update region propagation + escape check on views (Phase 2.4, 2026-06-17).
  • R.alloc(v) syntactic sugar (equivalent to &R v; parser inspects region_stack and desugars; Phase 2.5, 2026-06-17).
  • Trivial[R] type constraint (declare Drop types with drop type Name = ...; type error when a region holds a value containing such a type; Phase 2.6, 2026-06-17).

Phase 3 (larger; some design needs to be revisited)

  • Refined borrow annotations: &shared write / &exclusive write (Q-004 resolved in Phase 11.1-11.3; the borrow checker covers all 4 mode × 4 mode = 10 conflict pairs by Phase 17.1/17.2).
  • Child regions and promotion (region S of R, R.promote(...)).
  • Region std-types like Vec[R, T] / StrBuf[R] (Q-010 narrowed).
  • Mechanizing with + Drop ordering (per Q-011 resolved).

Current state of borrow modes and concurrency safety (2026-06-23 update)

The syntax and borrow checker for all 4 borrow modes are complete (Phase 11-17). However, with no concurrent backend yet, type-level requirements that T be "internally safe" (Rust's Send/Sync) for &shared write R T are not yet enforced:

Mode Concurrency-safety requirement (future)
&R T (default = shared read) Safe — read-only access on immutable T
&shared write R T Requires: T is internally safe (atomic / Mutex etc.) — not yet enforced
&exclusive R T (= exclusive read) Safe — single-thread access only
&mut R T (= exclusive write) Safe — single-thread access only

Concurrent-backend prerequisites are collected in DEFERRED §2.4. Currently Mere is single-threaded interpreter + 3 single-threaded backends (C / LLVM / Wasm); &shared write is only a syntactic distinction and runtime-wise behaves like a plain &R T pointer. Introducing a concurrent backend (e.g. via OCaml domains / Wasm threads) is the trigger for adding Send/Sync-equivalent type bounds for T.

Phase 4 (codegen)

In progress — see codegen.md.

  • C codegen MVP (int + arithmetic + if + let, 2026-06-17).
  • Function lifting + recursion (factorial / fibonacci works, 2026-06-17).
  • Strings + print + concat (hello world, 2026-06-18).
  • Functions taking/returning str (2026-06-18).
  • Tuples + per-AST type annotations (2026-06-18).
  • Records / variants / pattern match (2026-06-18; includes polymorphic monomorphization).
  • Closure conversion + first-class functions (2026-06-18).
  • Region runtime (bump allocator) — the memory model in action, 2026-06-18.
  • with Drop execution codegen (2026-06-18; auto-invokes close at scope end).
  • View construction over region (2026-06-18; view values become bump-alloc + pointer).
  • Default-region closure env (2026-06-18, Phase 4.20; program-lifetime arena __lang_default_region is init/freed in main; closure env alloc moves to bump).
  • Default-region for strings / recursive variant nodes (2026-06-18, Phase 4.21; __lang_str_concat and recursive Constr's malloc go through the default region — user-visible malloc disappears entirely).
  • Move to LLVM IR or Wasm.

7. Design context (in detail)

Specific design decisions live in the internal design notes:

Doc Content Status
00_design_principles.md Mere's philosophy and assumptions
01_memory_model.md Overview of the 5 strategies
02_json_parser_example.md Region's canonical use case
03_lifetime_and_mutability.md Lifetime subtyping
04_fundamental_tradeoffs.md Staged annotations, etc.
08_effect_granularity.md Q-004 borrow refinement narrowed
11_region_vs_arena.md Q-008 unification resolved
12_drop_and_with.md Q-011 Drop ordering resolved
13_region_std_types.md Q-010 region std types narrowed
14_view_types.md Q-009 view-type axioms resolved

8. Academic roots

Reference Content
Tofte & Talpin (1997) "Region-Based Memory Management" — the foundational region calculus
Cyclone (2002) A research language that extends C with lifetimes and regions
Cone (2018-) A modern language with region as a primitive
Vale (2020-) Region + generational references
Mike Acton et al. "Data-Oriented Design" — practical examples of frame-arena patterns

Bottom line: Mere is designed to map memory lifetime onto program structure — looser than ownership but stricter than GC; permits cycles; predictable. Currently mere is at Phase 1 (syntax-only); the real power emerges in Phase 2+'s static checks + codegen.