diff --git a/docs/changelog.md b/docs/changelog.md index d1363605..b6166c6a 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -6,6 +6,19 @@ Major implementation milestones recorded per-slice (newest first). See `git log` ## v0.1.711 — 2026-11-07 +Native C and LLVM programs gain addressed UDP send and receive through +`sock_sendto` and `sock_recvfrom`, including numeric IPv4/IPv6 sender metadata, +binary and empty packets, flags, arena bounds and thread-local errno. A +zero-capacity receive without sender metadata reports EAGAIN; queued packets +retain their sender. The loopback gate checks both native backends. + +The code-generation guide and tutorial now distinguish implemented backends +and builtin values from their historical phase summaries. Map storage checks +bound suffix replacement and small compacted maps, and reject controls that +restore either previous allocation policy. The suffix parity fixture also +keeps a copied snapshot across compaction and replacement. The reference +documents when explicit compaction can invalidate uncopied reads on C. + _A map keeps a value that is the tail of the one it replaces; a compacted map's first block is sized to it._ diff --git a/docs/codegen.md b/docs/codegen.md index 6aeef6bd..b0a50f38 100644 --- a/docs/codegen.md +++ b/docs/codegen.md @@ -1,6 +1,10 @@ # Code generation (codegen) -Mere's code-generation strategy, the current state in `mere`, and what's planned. Because the **memory model** and **effect system** at Mere's core only show their real power once codegen is in place, Phase 4 progresses through this in slices. +Mere's code-generation strategy and backend behavior. C, LLVM IR, Wasm and +RISC-V are implemented. The phase tables below record how they developed; +the [standard-library reference](stdlib-reference.md), +[host support matrix](host-matrix.md) and executable checks describe current +support for individual operations. --- @@ -27,8 +31,10 @@ The last stage of a compiler. Mere-ml started as parser → typer → **eval** ( | Mode | Execution model | |---|---| | Tree-walking interpreter | Traverses the AST each time, looks up env, boxes/unboxes closure values, runs through OCaml's GC | -| C codegen (current) | Runs the native binary produced by the C compiler — register allocation, function calls are one `call` instruction | -| LLVM / Wasm (future) | Adds more optimization (inlining, dead-code elim, loop unrolling) | +| C (`-c`) | Emits C for a native C compiler | +| LLVM IR (`-ll`) | Emits LLVM IR for a native LLVM toolchain | +| Wasm (`-w`) | Emits WAT for a WebAssembly engine and its host imports | +| RISC-V (`-rv` / `-rv64`) | Emits RV32IM / RV64IM machine code without an external assembler or linker | ### Distribution @@ -37,51 +43,60 @@ Codegen mode: compile `program.mere` once and you have a self-contained binary. ### Letting the memory model shine -Mere's **region / view / Trivial[R] / `with` Drop** design ([memory-model.md](memory-model.md)) currently exists as type-level labels; under the hood, OCaml's GC actually manages memory. - -``` -// Current (interpreter): -region R { let n = R.alloc(Node { ... }) in ... } -// Reality: an ordinary object allocated on OCaml's heap, collected by the GC - -// After codegen: -// region R push/pops a bump allocator and frees everything on scope exit -// Drop's LIFO execution becomes a mechanical reverse-order walk over a Drop list -``` - -As memory-model.md says: -> Region's real power (bump allocator, bulk free, cache locality) only emerges with **native codegen**. +The interpreter uses OCaml-managed values. Compiled backends implement region +allocation and reclamation in their runtimes, and `with` performs scope-bound +cleanup. Region results and escaped values have backend-specific handling; +some shapes are refused. See [memory-model.md](memory-model.md) for the +ownership rules, reclamation mechanisms and executable examples. ### Becoming a self-contained language -In interpreter mode, "Mere runs on top of OCaml." Once we can emit native binaries via codegen, "Mere is a self-contained language." This is required by the design goal of "both native (LLVM) and Wasm" in the internal design notes. +The distribution compiler is built with OCaml. Programs compiled to native +binaries run without the compiler or an OCaml installation. Wasm programs +need an engine and the imports they use; RISC-V programs need the corresponding +machine or emulator. See [bare-metal.md](bare-metal.md) for hosted and bare +RISC-V execution. --- ## 3. Mere's codegen strategy -### Multi-stage approach +### Implemented targets -| Phase | Target | Status | Why | -|---|---|---|---| -| 4 (current) | **C** | In progress | The simplest intermediate language; clang/gcc are right there | -| 5 (future) | **LLVM IR** | Not started | More optimization opportunities; supports both native and JIT | -| or | **Wasm** | Not started | Browser execution + WASI for self-contained binaries | +| CLI | Target | Toolchain or host | +|---|---|---| +| `-c` | **C** | Clang or GCC | +| `-ll` | **LLVM IR** | Clang or another LLVM toolchain | +| `-w` | **Wasm text** | `wat2wasm`, then an engine with the required host imports | +| `-rv` / `-rv64` | **RISC-V binary** | RV32IM / RV64IM execution; see the bare-metal guide | ### Why go through C? -- Easier than emitting LLVM / Wasm directly (C is "portable assembly"). -- A C compiler is already on hand (clang/gcc are standard on macOS/Linux). -- Region runtime and GC helpers can be written in C and FFI-bound. -- When we later switch to LLVM IR, the C output serves as a "ground truth" for comparison. +- C output can be inspected and checked with Clang and GCC, including sanitizers. +- The native C and LLVM paths share C runtime support for facilities such as + sockets; LLVM programs using native FFI also link the `--ffi-c` companion. +- Differential checks compare execution with the interpreter and other backends. ### Per-AST type annotations (introduced in Phase 4 slice 5) -`Ast.expr` gains a `mutable ty : ty option`; `Typer.infer` records the inferred type on each node. This lets codegen directly read each node's type — tuple shape inference and fn signature inference become clean. The same foundation will be used for LLVM / Wasm migration. +`Ast.expr` has a `mutable ty : ty option`; `Typer.infer` records the inferred +type on each node. Code generators use these annotations for concrete tuple +shapes, function signatures and specialization. --- -## 4. Current state in mere (as of 2026-06-24, after Phases 4-46) +## 4. Backend capabilities + +The next two phase summaries are historical snapshots from June 2026, not a +current test count or an exhaustive support list. Current regression examples +include [builtin values](../test/parity/builtin_as_value.mere), +[tuple captures](../test/parity/closure_tuple_cap.mere), +[returned closures and aliases](../test/parity/closure_alias_types.mere), +[file reads](../test/parity/read_lines.mere) and +[environment access](../test/parity/dirs_and_env.mere). Run them with +`sh scripts/parity.sh ...` to compare interpreter, C, LLVM and Wasm. + +### Historical overview: June 2026 **Nearly the entire Mere syntax that's expressible in the interpreter** + **Q-010 collections (Vec / OwnedVec / StrBuf / Map)** + **FFI (`extern fn`)** + **float (3 backends)** + **Phase 36's 13 syntactic sugars** is native-compilable and runnable in 3 backends (C / LLVM IR / Wasm). **1573 tests pass**. End-to-end: factorial, fibonacci, linked-list sum, `make_adder` closures, polymorphic variant + record, pattern match (nested / guard / or). Phase 15 added all element types for Vec[R, T] / OwnedVec[T] / StrBuf[R] / Map[R, K, V] (K = int / bool / str / tuple / record / all variants) / higher-order API (vec_set / iter / fold / map / filter) / conversions (vec_to_owned / owned_vec_to_vec / vec_to_list) / `len` ad-hoc polymorphism / with-OwnedVec scope-Drop — all in 3 backends. @@ -150,22 +165,31 @@ In interpreter mode, "Mere runs on top of OCaml." Once we can emit native binari | Polymorphic variant monomorphization | `type 'a list = Nil \| Cons of 'a * 'a list` and friends — concrete instantiations are collected from AST + fn signatures and a specialized struct is emitted per instance (`list_int`, `opt_str`, ...). Recursivity is judged on the substituted payload. So `[1, 2, 3]` literals and `'a list`-typed `sum` etc. run natively | | Polymorphic record monomorphization | `type 'a Box = { v: 'a }` etc. specialize like variants (`Box_int`, `Box_str`, ...); Record_lit emit reads the mono name from `.ty` and emits e.g. `((Box_int){.v = 42})` | | Polymorphic `show` builtin | For each `show` call, the argument type is collected and a specialized `show_T` C fn is auto-generated. Covers int/bool/str/unit/tuple/record/variant (monomorphic + polymorphic instantiations). **`'a list` is special-cased (`[1, 2, 3]` style)**; other recursive variants use `Cons (1, Cons (2, Nil))` style. asprintf-based; heap-allocates | -| Closure (defunctionalized) | Inner `let n = fn x -> body` is defunctionalized to top level. Free vars are prepended to the C fn's params; call sites are rewritten. Captures are restricted to int/bool/str/unit (tuple/record/function-value captures not yet supported). Multi-level nesting is fine | +| Closure (lifted) | Inner functions are lifted and captures are passed in their environment. Tuple, record and function-value captures are supported; concrete representation and region escape rules still apply. The parity corpus includes tuple captures and returned closure aliases | | First-class fns (Phase A + B) | `T1 -> T2` types become closure structs (`{ void* env; T2 (*fn)(void*, T1); }`). Each top-level fn gets a `_closure_fn` adapter + `_as_value` const; anonymous Fun in expression position is lifted into env struct (heap-alloc) + adapter (`__anon_N_fn`) + closure construction, with captures rewritten to `__env_self->name`. Closure dispatch: `({ __auto_type __c = e; __c.fn(__c.env, x); })`. Direct call (Var head on a known top-level) still takes the fast direct-call path | -### What doesn't work (currently Codegen_error) - -- **Phase 35 made nullary factory builtins (vec_new / map_new / strbuf_new etc.) usable as first-class values in all 4 backends** (eta-wrap). But **partial application of multi-arg curried builtins** (`let push_to_v = vec_push v in ...`) is still future work. -- **Auto scope-bound Drop for OwnedVec** — explicit `with v = ...` already frees at scope end; without it, bulk-freed at main exit (Phase 15.8 / 15.13; DEFERRED §1.3). -- Inner-lifted fns (`let h = fn ...`) used as VALUES (currently only direct calls; rewriting as an anonymous Fun works). -- Closure captures of non-primitive types (tuple/record captures; closure-value captures are fine). -- Nested or-patterns (or inside constructor / tuple / record). -- **Placing `while cond do body` directly under top-level main** — top-level Let_rec constraint; inside an fn body is fine. -- **String interpolation with nested string literals** (`"x = {show \"abc\"}"`) — workaround via a let binding. -- Some stdlib builtins (`read_lines` / `env_var` / `file_exists` etc.) are **interpreter-only**. Phase 34 brought float / libm (sqrt / sin / cos / tan / f_pow / atan2) to the 3 backends. `args` is no longer among them: it reaches all four backends as of v0.1.169 (LLVM stores argc/argv from `main` into globals; Wasm folds the host's `arg_count` / `arg_get`). -- GC for strings / closure envs / variant nodes (currently a region arena bulk-freed at main exit — suitable for short-lived runs). -- LLVM / Wasm with payload-mixed variants as Map K (uniform payload only) — C accepts mixed. -- **`of_json` on LLVM**. `to_json` landed there in v0.1.184 by sharing the `show` emitter — the two differ only in literals — but decoding needs a JSON parser written in the target language, and C and Wasm each have their own hand-written one. `scripts/parity.sh` now reports what each backend did not check, which is where this shows up: six of eighty-three, all but one of them `of_json`. +### Support boundaries + +Builtin values are supported when the backend implements the operation and +its use has a concrete callable type. Factory values such as `vec_new` and +`map_new` are supported when their return type is concrete, as in +[factory_value.mere](../examples/factory_value.mere). An unconstrained +`let make = vec_new` may retain a polymorphic return type; annotate it or +pass the builtin to a concretely typed parameter. Wrapping a call in `fn` +remains useful when an operation has a backend-specific restriction. See the +standard-library reference for individual APIs. + +Floats, `read_lines`, `env_var`, `file_exists` and `args` are implemented on +the interpreter, C, LLVM and Wasm paths. Host filesystem and environment +access depend on the selected host. The generated host support matrix and +the standard-library reference describe these requirements. + +Automatic scope cleanup for an ordinary `OwnedVec` binding is not inferred: +use `with v = owned_vec_new () in body` for scope-bound cleanup. LLVM JSON +decoding (`of_json` and its siblings) remains unsupported; `to_json` works +there. Further restrictions, including region-returned functions on LLVM, +are documented in the memory-model guide. The parity harness reports clean +backend refusals separately from compilation failures and output differences. ### CLI @@ -245,21 +269,11 @@ Detailed change history is in [changelog.md](changelog.md). --- -## 6. Roadmap - -### Remaining work (see DEFERRED §1.2 / §1.3) +## 6. LLVM and Wasm implementation history -| Feature | What's needed | -|---|---| -| First-class value use of builtins | Emit per-T closure wrappers for `let f = vec_new in ...` (DEFERRED §1.2). Multi-slice effort | -| Auto scope-bound Drop for OwnedVec | NLL + move semantics (DEFERRED §1.3); design first | -| Nested or-patterns | Expand or inside constructors | -| Inner-lifted fn as value | Generate a closure form alongside `let h = fn ...` when `h` is used as a value (currently rewrite as anonymous Fun) | -| Complex closure captures | Tuple / record captures (currently int / bool / str / unit / function value only) | -| float | `Float_lit` / `f_add` etc. → C `double` + `%g` family | -| Long-running program support | Currently the arena is bulk-freed only at `main` exit. A sub-arena that can drop transient strings (e.g. `show` output) or a proper GC is needed for long-running processes | -| Remaining stdlib builtins | About 70 builtins (sqrt, str_replace, ...) ported to 3 backends | -| Payload-mixed variants as Map K on LLVM / Wasm | Loosen the uniform-payload MVP constraint (a larger change) | +The tables in this section record completed implementation slices. They are +kept as history; use the capability and support sections above when choosing +a backend for a program. ### Phase 5 (LLVM/Wasm migration) diff --git a/docs/stdlib-reference.md b/docs/stdlib-reference.md index e9bce11c..3fc43f07 100644 --- a/docs/stdlib-reference.md +++ b/docs/stdlib-reference.md @@ -1832,6 +1832,17 @@ satisfy it and hide the very row it was describing. | `map_recycle` | `Map[R, K, V] -> unit` | return the table's storage for reuse | | `map_bytes` | `Map[R, K, V] -> int` | bytes this Map currently holds | +**Explicit compaction on C is a reclamation point.** A value read from a +Map or Vec may still refer to its private arena. Finish using such reads +before calling `map_compact`, `vec_compact`, `map_recycle` or `vec_recycle` +on that container: the running stack does not pin those values. To keep an +immutable snapshot across compaction, copy it out as a region result, for +example `let saved = region Copy { map_get m key } in ...`. The copy lives +in the enclosing region. The suspended-coroutine pinning described above +protects stopped stacks separately. The +[suffix fixture](../test/parity/map_set_suffix.mere) checks a copied list +snapshot across replacement and compaction. + **`OwnedVec`**: `owned_vec_get : OwnedVec[T] -> int -> T` joins `owned_vec_new` / `push` / `len` / `to_vec`, which are in language-reference. diff --git a/docs/tutorial.md b/docs/tutorial.md index a955c186..29fd7a94 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -649,18 +649,24 @@ mere -ll examples/map_codegen.mere | clang -x ir - -o map && ./map # → 640 # Wasm codegen (requires wabt / Node.js) mere -w examples/vec_codegen_wasm_typed.mere > v.wat -wat2wasm v.wat -o v.wasm -node -e 'WebAssembly.instantiate(require("fs").readFileSync("v.wasm"), - { env: { puts: () => 0 } }).then(r => console.log(r.instance.exports.main()))' +wat2wasm --enable-tail-call --enable-threads v.wat -o v.wasm +node scripts/run_wasm.js v.wasm # → 252 ``` -Remaining work (see DEFERRED §1.2 / §1.3): +Builtin factories can also be used as values on interpreter, C, LLVM and +Wasm when the factory has a concrete return type, for example +`let make = (vec_new : unit -> Vec[__heap, int]) in ...`. +An unconstrained `let make = vec_new` can retain a polymorphic return type +and be refused by codegen; use an annotation or pass it to a concretely typed +parameter, as in [factory_value.mere](../examples/factory_value.mere). +Other builtin values follow the operation's backend support; +[builtin_as_value.mere](../test/parity/builtin_as_value.mere) is an executable example. -- **First-class value use of builtins** (`let f = vec_new in ...`) is not yet codegen-supported — interpreter only. Workaround: write a wrapper like `fn v -> vec_push v x`. -- **Auto scope-bound Drop for OwnedVec** is not supported — explicit `with v = owned_vec_new () in body` frees at scope end; without it, bulk-freed at main exit. -- **The borrow checker doesn't track Vec internals at element granularity** — the mode at the point of borrowing the Vec is machine-checked; details like borrowing a `vec_get` result come later. -- **Payload-mixed variants as Map K on LLVM / Wasm** are restricted to uniform-payload (MVP); C allows mixed. +For scope-bound `OwnedVec` cleanup, use +`with v = owned_vec_new () in body`; an ordinary binding does not infer a +scope-end drop. See [the memory model](memory-model.md) for borrowing, +container ownership and reclamation rules. For the full Q-010 design see the internal design notes. @@ -926,11 +932,20 @@ For details see [codegen.md](codegen.md). Interpreter mode (`mere file.mere`) and codegen output match for the same program (including formatting like `[1, 2, 3]`). At **4-backend feature parity**, int / functions / strings / tuples / records / variants / closures / polymorphism / recursive variants / complex patterns / show / region / view / `with` Drop / list pretty-printing / Q-010 collections (Vec / OwnedVec / StrBuf / Map) + higher-order API + conversions + len + with-Drop / signature spread / Result helpers / try_or / inner-fn lifting / top-level value-binding globalization / str_compare / FFI (extern fn) all work (reached incrementally through Phases 15-32). -The **feature-parity gap** between interpreter and the 3 codegen backends is now nearly zero; remaining items: -- **First-class value use of builtins** (`let f = vec_new in ...`) is interpreter-only (future work). -- **Auto scope-bound Drop for OwnedVec** isn't supported (explicit `with` or main-exit bulk free only; NLL / linear types are future work). -- `float` / `'a list`-typed builtins (`read_lines` / `args` / `env_var` / `file_exists` etc.) are **interpreter-only** (codegen comes in a separate phase). -- LLVM / Wasm accept only **uniform payload for Map K with payload variants** (C allows mixed). +Backend support depends on the operation and its value's shape. Floats, +builtin factory values, `read_lines`, `args`, `env_var` and `file_exists` +are implemented on interpreter, C, LLVM and Wasm. Filesystem and environment +operations use the selected host's facilities. See the +[standard-library reference](stdlib-reference.md) and +[host support matrix](host-matrix.md) for individual APIs and host requirements. + +Some boundaries remain: `OwnedVec` needs explicit `with` for scope-bound +cleanup, LLVM does not implement `of_json` decoding, and LLVM refuses a +function returned from a region block. The [codegen guide](codegen.md) and +[memory model](memory-model.md) describe these restrictions. Run +`sh scripts/parity.sh ...` to check a concrete program across the four +backends; it distinguishes a named refusal from a compilation failure or +different output. ## 14. Formatting source (`mere fmt`) diff --git a/examples/vec_codegen_c.mere b/examples/vec_codegen_c.mere index eeae5182..d2d993a8 100644 --- a/examples/vec_codegen_c.mere +++ b/examples/vec_codegen_c.mere @@ -9,12 +9,10 @@ // Monomorphization emits a `mere_vec_` struct + 4 helpers (e.g. // `mere_vec_int` / `mere_vec_str` / `mere_vec_tuple_int_int` / `mere_vec_Pt`). // -// Limitations (still): -// - Higher-order APIs (vec_iter / vec_map / vec_fold / vec_filter / -// vec_to_list / vec_to_owned / vec_set) are out of scope. -// - First-class value use of vec_* (`let f = vec_new in ...`) is unsupported. -// - OwnedVec / StrBuf / Map are interpreter-only. -// - LLVM / Wasm backends still reject all Vec usage. +// This example dates from the basic Vec implementation. Higher-order APIs, +// conversions, OwnedVec, StrBuf and Map now also compile on C/LLVM/Wasm. +// A builtin factory used as a value needs a concrete return type; see +// examples/factory_value.mere for typed factories on all four backends. // // Usage: // dune exec ./bin/mere.exe -- -c examples/vec_codegen_c.mere > /tmp/vec.c diff --git a/scripts/map_allocation_check.py b/scripts/map_allocation_check.py new file mode 100644 index 00000000..d7361512 --- /dev/null +++ b/scripts/map_allocation_check.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""Bound Map storage and prove that restoring either old policy fails the bound.""" +from pathlib import Path +import os +import re +import shlex +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parent.parent +MERE = Path(os.environ.get("MERE", ROOT / "_build/default/bin/mere.exe")) +CC = shlex.split(os.environ.get("CC", "cc")) + +SUFFIX = """ +let m = map_new (); +let rec mk = fn (n: int) -> fn (xs: int list) -> + if n <= 0 then xs else mk (n - 1) (Cons (n, xs)); +let _ = map_set m 1 (mk 4000 Nil); +let _ = map_compact m; +let before = map_bytes m; +let rec drain = fn (n: int) -> + if n <= 0 then () else + match map_get m 1 with + | Nil -> () + | Cons (_, tail) -> let _ = map_set m 1 tail in drain (n - 1); +let _ = drain 3990; +let _ = print_int before; +let _ = print_int (map_bytes m); +let _ = print_int (list_sum (map_get m 1)); +""" + +COMPACT = """ +let maps = vec_new (); +let rec keep = fn (n: int) -> fn (bytes: int) -> + if n <= 0 then bytes else + let m = map_new () in + let _ = map_set m 1 n in + let _ = map_compact m in + let _ = vec_push maps m in + keep (n - 1) (bytes + map_bytes m); +let bytes = keep 1000 0; +let rec sum = fn (i: int) -> fn (acc: int) -> + if i >= vec_len maps then acc + else sum (i + 1) (acc + map_get (vec_get maps i) 1); +let _ = print_int bytes; +let _ = print_int (sum 0 0); +""" + + +def run(args, cwd): + result = subprocess.run([str(a) for a in args], cwd=cwd, capture_output=True, timeout=60) + if result.returncode or result.stderr: + raise RuntimeError(f"{Path(str(args[0])).name} failed: {result.stderr.decode(errors='replace')[:1000]}") + return result.stdout + + +def measure(code, directory): + (directory / "probe.c").write_bytes(code) + run([*CC, "-O2", "-w", "probe.c", "-o", "probe", "-lm", "-lpthread"], directory) + return [int(line) for line in run([directory / "probe"], directory).splitlines()] + + +def bounded(name, values): + if name == "suffix": + before, after, answer = values + if answer != 39955 or not 64000 <= before <= 1048576 or after < before: + raise RuntimeError(f"suffix probe lost its values or storage meter: {values}") + return after <= before * 2 + 4096 + held, answer = values + if answer != 500500 or held < 512000: + raise RuntimeError(f"compact probe lost its values or storage meter: {values}") + return held <= 1536000 + + +def main(): + with tempfile.TemporaryDirectory(prefix="map-allocation-") as tmp: + directory = Path(tmp) + for name, source in [("suffix", SUFFIX), ("compact", COMPACT)]: + file = directory / "probe.mere" + file.write_text(source) + code = run([MERE, "-c", file], directory) + values = measure(code, directory) + if not bounded(name, values): + raise RuntimeError(f"{name} storage exceeds its bound: {values}") + if name == "suffix": + # Restore copying even when the replacement is the existing tail. + pattern = rb"if \(__o == __mv\) \{ m->values\[i\] = __mv; return 0; \}" + poisoned, count = re.subn(pattern, b"", code) + else: + # Restore the former minimum block size for compacted maps. + poisoned = code.replace(b"size_t __seed = 512;", b"size_t __seed = 4096;") + count = code.count(b"size_t __seed = 512;") + if count != 1: + raise RuntimeError(f"{name} control lost its single mutation site: {count}") + old_values = measure(poisoned, directory) + if bounded(name, old_values): + raise RuntimeError(f"{name} bound did not detect the restored old policy: {old_values}") + print(f"map_allocation: {name} passed {values}; old-policy control rejected {old_values}", flush=True) + + +if __name__ == "__main__": + main() diff --git a/scripts/region_slack_check.sh b/scripts/region_slack_check.sh index 85e59939..b79f2c29 100755 --- a/scripts/region_slack_check.sh +++ b/scripts/region_slack_check.sh @@ -103,3 +103,7 @@ if [ "$dalloc" -gt 16777216 ]; then exit 1 fi echo "PASS region_slack (delete churn): alloc_total=$dalloc" + +# Map suffix reuse and compacted-map storage have their own public capacity +# meter. Restoring each former policy must fail its deterministic bound. +MERE="$MERE" CC="$CC" python3 "$ROOT/scripts/map_allocation_check.py" || exit 1 diff --git a/test/parity/map_set_suffix.mere b/test/parity/map_set_suffix.mere index f512cc5d..5bfaf7db 100644 --- a/test/parity/map_set_suffix.mere +++ b/test/parity/map_set_suffix.mere @@ -47,3 +47,17 @@ let _ = map_set ms "q" (Cons ("a", Cons ("b", Cons ("c", Nil)))); let _ = (match map_get ms "q" with Cons (_, t) -> map_set ms "q" t | _ -> ()); let _ = map_set ms "q" (Cons ("z", map_get ms "q")); let _ = print (match map_get ms "q" with Cons (x, Cons (y, Cons (w, Nil))) -> x ++ y ++ w | _ -> "?"); // zbc +// A private arena after compaction: copy an older value out through a region +// result, store its suffix, compact again, then put the snapshot back. +// Compaction may reclaim uncopied reads held on the running C stack. +let _ = map_set m 1 (mk 12 Nil); +let _ = map_compact m; +let kept = region Copy { map_get m 1 }; +let _ = take_front 2; +let _ = print_int (sum (map_get m 1) 0); // 75 +let _ = map_compact m; +let _ = print_int (sum kept 0); // 78 +let _ = map_set m 1 kept; +let _ = map_compact m; +let _ = print_int (sum (map_get m 1) 0); // 78 +let _ = print_int (sum kept 0); // 78