← Commit history

Add ownership tests and memory safety features

lennoxrose committed on Oct 18, 2026 at 23:10 UTC

85d6a7eparent7e4705e

161 files changed+4594−48

- Introduced new test cases for ownership management in the Ash language, including OwnFreeLists, OwnFreeLoops, OwnFreeMaps, OwnFreeObj, OwnFreeReturn, and OwnFreeTry.
- Implemented a memory safety check script to ensure bounded memory usage during execution.
- Created a new Rust library, Mnemosyne, with a C ABI for managing memory ownership and flow, including functions for moving and reinitializing variables.
- Added regression tests for the Mnemosyne library to validate the C ABI against expected behavior.
- Updated the build system to include the new Rust library and its dependencies.
- Enhanced existing test cases to cover various ownership scenarios and ensure proper memory management.

CHANGELOG.mdmodified

+32−0
@@ -1,3 +1,35 @@
1+## 2026-10-18 (Mnemosyne: mnemo.a, freeing sole-owner values)
2+- **`bin/lib/mnemo.a`** (`hearth/mnemo`, Rust, C ABI in `mnemo/H/flow/flow.h`) is linked by both `ashvm` and `kiln`. It holds the move-flow engine of the ownership pass: moved sets, dead paths and the snapshots/joins at branches and loops. `front/src/jown` walks the typed AST and reports events to it. Built by `hearth/mnemo/Makefile` (cargo; a missing toolchain is a hard error pointing at `tools/install.sh`), wired into the `ashvm` and `kiln` Makefiles. C-ABI regression: `mnemo/tests/scripts/run_regression.sh`.
3+- **Freeing, `memory-safety: on` projects, `ashvm` only.** `jown` proves which locals are the sole owner of a fresh String (non-constant `+`) or array (`new T[n]`, `{..}`) and are only read (index, `.length`, operands, String methods returning primitives, print args, for-each). Lowering frees those with the new builtin `mem_free(x)` at the end of the block, before `return`/`break`/`continue` (the return value is computed first), and before a reassignment (`s += x`). Concatenation temporaries (an inner `a + b`, `str(int)`) are freed by `mem_cat(a, b, flags)`; `new T[n]` uses `__newarr1` so no dims array leaks. Everything else is left alone: leaking is safe, freeing a shared value is not. `kiln` lowers none of this yet (`jl_drops`), so its IR is unchanged until it has a free-list allocator.
4+- **Objects are freed too.** `T o = new T(..)` is a sole owner when `T` is a plain class (superclass Object, no default-method interfaces, not a Throwable/enum) and neither its constructor, field initializers, nor any method called on `o` lets `this` escape (checked transitively in `jown`; lambdas, `return this`, passing `this`, and call cycles count as escaping). `o` may only be read through fields, those methods, and `==`/`!=`. `mem_free` frees the object's map (keys and tables); field values are left alone. Test: `ashvm/tests/cases/ownership/OwnFreeObj.ash` (also clean under AddressSanitizer).
5+- **Borrows and call moves** (`memory-safety: on`). `&x` is a shared and `&mut x` an exclusive borrow of a local, written only as a call argument (parser `unary`, checker `check_unary`; a borrow lowers to the variable itself, so it costs nothing at run time). Passing a plain owning local to a project method or constructor now moves it; library and native calls only look at their arguments and never move. In one call an exclusive borrow excludes every other use of the same variable (`f(&mut x, &x)`, `f(&mut x, x)`, `x.m(&mut x)` are errors); `&x` anywhere else is an error. A lent variable is never freed by the sole-owner pass, since the callee could keep it. Tests: `OwnBorrow`, `OwnMovedCall`, `OwnBorrowConflict`.
6+- **Ownership transfer through `return`.** A call to project code (static, private, final, or a class nothing extends) whose every `return` hands over a fresh String / array, or a local that is its sole owner, yields a fresh value: the caller's `String s = build(i)` is freed like a local concatenation, and the callee skips freeing the local it returns (`front/src/jown`, `returns_fresh`). A method that can return an alias (a field, a parameter) is never treated as fresh. Test: `OwnFreeReturn.ash`.
7+- **`memory-safety` now defaults to `on`** (goals.md): `JOWN_DEFAULT_ON` is 1, and a project without `ash.pkg` is the entry file's directory. `memory-safety: off` in `ash.pkg` restores the old behaviour exactly. With it on by default the whole `ashvm` regression (all front corpus programs and examples) runs with freeing active, with byte-identical output, also under AddressSanitizer; `kiln`'s suite passes too. Source changes this needed: `examples/Desktop.ash` and `Generics.ash` lend with `&`, `run_oop_j.ash` uses `g` after `b` was moved into it, and `equals`/`compareTo` never consume their argument.
8+- **`System.free(x)` for `memory-safety: off`** (goals.md): lowers to `mem_free(x)`; it is an error in a project with memory safety on. `ashvm` frees the String, array or object; `kiln` has a `mem_free` builtin that does nothing until it has an allocator that can free. Tests: `tests/cases/memoff/` (both engines, own `ash.pkg`), `OwnFreeDenied.ash`.
9+- **Fixes found by adversarial tests** (`OwnFreeTry`, `OwnFreeAlias`, run under AddressSanitizer): `return` inside `try`/`finally` freed locals before the finally body read them (the finally bodies now run first, then the frees); `break`/`continue` under a `finally` no longer free.
10+- **Factory methods hand over objects.** `returns_fresh` also covers a plain object: every `return` must be `new T(..)` of one class (or a sole-owner local of it) and the declared return type exactly that class, so `Point p = make(i)` is freed like `Point p = new Point(..)`; a method returning two classes, a supertype, or an alias (a static, a field) is never fresh. Test: `OwnFreeFactory.ash`.
11+- **`apm` knows `memory-safety`.** It is a header key of `ash.pkg` (`on` | `off`): `apm new` / `apm init` write `memory-safety: on`, and every rewrite of `ash.pkg` (`apm add`, `apm license`, ...) keeps it. Before, any rewrite silently dropped the key, which would have flipped an `off` project to the default.
12+- **Constant folding through finals.** The constant-overflow check now sees through `final` int locals and `static final` int fields with constant initializers, so `Integer.MAX_VALUE + 1`, `final int n = 100000; n * n` and `BIG + BIG` are rejected too (`tests/cases/safety/ConstOverflow.ash`, `ConstIndex.ash`). Constant out-of-bounds indexes are only denied for `final` arrays: programs here demonstrate the run-time IndexOutOfBounds on plain locals.
13+- **`kiln` frees Strings** (static Linux builds). The lowering is shared with `ashvm` (`jl_drops` is now a mask: `ashvm` frees Strings, arrays and objects, `kiln` Strings only). When the IR contains `mem_free` / `mem_cat`, the driver turns on free lists (`codegen/C/runtime/heap.c`): one list per 16-byte size class up to 512 bytes, heads in a new 264-byte slot of the ELF globals (`KILN_FREELIST_ADDR`). Only the two places that make freeable Strings (concatenation, `str()`) use the new `heap_emit_alloc_str`, which rounds the request and pops a recycled block; the plain alloc routine used by arrays, maps and everything else is untouched, so those cost exactly what they did (an array-heavy loop measures the same with and without a free elsewhere in the program). `mem_free(x)` pushes a String block on its class list; `mem_cat(a, b, flags)` concatenates and frees the temporary operands. Programs that never free, Windows and `--link=shared` builds get the old allocator. `System.free` is now `mem_release`: `ashvm` frees, `kiln` ignores it (it cannot know where an arbitrary String came from). A loop of 3 million short-lived strings runs in 12 MB on `kiln` (249 MB with `memory-safety: off`). `tests/scripts/memory_check.sh` (both engines, 3 GB cap) checks this; the ownership tests also run on `kiln`.
14+- **`kiln` frees arrays too.** `new T[n]` (one dimension) compiles to the new `mem_newarr1` builtin with exactly `n` elements of capacity, array literals and the elements block go through the recyclable allocator when the program frees arrays (`mem_free_arr` in the IR), and `mem_free_arr(x)` puts the array object (32-byte class) and its element block (`capacity * 16` bytes, 16 for an empty one) on the free lists. Programs that never free an array keep the plain allocator. A churn loop of `new int[4]` runs 3.7x faster (0.0097 s vs 0.037 s for 400 thousand iterations, no page growth) and 300 thousand array iterations stay in 12 MB. Not freed: the blocks `push` abandons while an array grows, inner arrays of `new T[a][b]`, objects (maps) on `kiln`. The IR names are `mem_free` (Strings, objects), `mem_free_arr` (arrays; an alias of `mem_free` on `ashvm`), `mem_cat`, `mem_release`.
15+- **`kiln` frees objects too.** An instance of a plain class is a map built by one map literal with all its fields, so it never grows: `mem_free_obj(x)` puts its map object (32-byte class) and its entries block (`capacity * 32` bytes) on the free lists, and map literals go through the recyclable allocator when the program frees blocks. 300 thousand `new Point(..)` pairs run in 12 MB (105 MB with safety off); the whole `kiln` suite, including the ownership tests, passes with objects freed. The IR now names `mem_free` (Strings), `mem_free_arr`, `mem_free_obj` (aliases of `mem_free` on `ashvm`). Still leaked on `kiln`: the blocks `push` abandons, inner arrays of `new T[a][b]`, field contents.
16+- **Borrows are inferred for parameters.** A parameter of a project method or constructor that the body only reads (indexing, `.length`, operands, String methods returning primitives, print arguments, fields of a plain class, passing it on to another read-only parameter) is borrowed: passing a local to it neither moves the local nor stops it from being freed (`front/src/jown`, `param_borrowed`). Returning, storing, capturing in a lambda, passing to library code that may keep it, reassigning it from an alias, recursion, and calls through a parameter that may be a subclass all make the parameter escape, so the call moves its argument as before. The target must be fixed at compile time (static, private, final, constructor, or a class nothing extends). So `size(a); size(a);` is no longer a use-after-move, and `a` is still freed. Tests: `OwnBorrowInfer`, `OwnBorrowTricky` (aliasing traps; ASan-clean), `OwnMovedKeep`, `OwnMovedCall`.
17+- **Objects free the fields they own.** A String or array instance field of a plain class is *owned* when its initializer (if any) is fresh, every assignment anywhere in the project stores a fresh value (or `+=`), and every read is a benign one (index, `.length`, operand, String method returning a primitive, print argument, for-each) -- a getter, `a.f = b.f`, a lambda capture or passing the field on makes it shared (`front/src/jown`, `JField.own`; records are never owned). Freeing an object then frees its owned fields first (`mem_free(o["f"])`), and reassigning an owned field frees the old value (`label = label + n`). Reference fields default to `none`, which freeing ignores. Test: `OwnFreeFields.ash` (a shared literal field, a field assigned from a parameter and a field returned by a getter stay alive; ASan-clean, same output on `kiln`).
18+- **Lists are freed.** A local or an owned field of type `List` / `ArrayList` built by `new ArrayList<>()` (or returned fresh by a project method) is freed like an array when it is only used through `size`, `isEmpty`, `get`, `set`, `add(T)`, `remove(int)`, `removeLast`, `contains`, `sort` and for-each. `subList`, `stream`, `iterator`, `forEach`, `add(int, T)`, passing the list on, returning it from a getter or aliasing it keep it alive (`front/src/jown`, `list_type`, `list_method_ok`; the methods are the `@Lower` templates in ash-stdlib `ArrayList.ash`). Only the list's own storage is freed, never its elements. The IR uses `mem_free_arr` for lists. Test: `OwnFreeLists.ash`; the examples that use lists run with them freed (ASan-clean, `kiln` suite green). Maps/sets are not freed: their entries are separate chained arrays.
19+- **Maps and sets are freed on `ashvm`.** A local or an owned field of type `Map` / `HashMap` / `Set` / `HashSet` built by `new HashMap<>()` / `new HashSet<>()` is freed when it is only used through `size`, `isEmpty`, `put`, `get`, `containsKey`, `remove`, `keys`, `values`, `add`, `contains` and `toList` (the `@Lower` templates in ash-stdlib `HashMap.ash` / `HashSet.ash` / `Map.ash` / `Set.ash`; all of them build new lists or return elements, none hands out the container). New builtin `mem_free_map` frees the `[key, value, next]` entry chains the prelude builds, the map's own keys and its tables; keys and values the program stored are left alone. Parameters of list / map type that a method only reads are borrowed like the others. `kiln` does not free maps. Test: `OwnFreeMaps.ash`; 100 thousand iterations of a 3-entry map run in 12 MB (150 MB with safety off).
20+- **Less garbage and a faster `hashmap.ash`** (also with safety off, on both engines): the map prelude no longer builds the element-count key `chr(1) + "n"` on every operation (it is `__KP[2]`, built once), and `type()` returns static strings instead of a malloc'd copy on each call. `hashmap.ash` runs about 5% faster than the original commit; nothing else changed in speed.
21+- **`kiln` frees maps and sets.** `mem_free_map(x)` walks the map's entries, frees each prelude `[key, value, next]` entry array (object and element block, following the collision chain), then the map's entries block and the map object; keys and the program's values are left alone. Map growth (`maps_mutate`) allocates through the recyclable allocator when the program frees blocks and gives back the 16-byte block of an empty literal; blocks of non-empty maps that grow are kept (a loop may still hold them). The same decision as for arrays: `kiln`'s `push` is not made to free its old block, because `each` loops cache the element pointer and a free would change the behaviour of programs that append while iterating. 400 thousand iterations of a three-entry map run in 12 MB (same speed as without freeing); `OwnFreeMaps` and the examples that use maps pass on `kiln` with maps freed.
22+- **Differential fuzzing of the ownership pass** (`ashvm/tests/scripts/ownership_fuzz.sh` + `ownership_fuzz_gen.py`): random programs that mix Strings, arrays, lists, maps and objects with aliasing, moves, factories, keeping helpers, hand-outs of fields, containers, loops with `continue`/`break`, `try`/`catch`/`finally`, reassignment and recursion must print the same with `memory-safety` on and off (and stay AddressSanitizer-clean with `ASAN=` and match `kiln` with `KILN=1`). About 1,000 accepted programs on `ashvm` (plus 180 on `kiln`) ran clean; the move checker rejected roughly 8% of them, as intended. Nothing was found, which is the result to expect from the corpus tests above, not a proof.
23+- **`ashvm` frees the Strings inside lists and String arrays.** A candidate `List<String>` / `String[]` (local, or owned field) whose elements are all fresh concatenations (`xs.add("a" + i)`, `xs[i] = "b" + i`, `xs.set(i, "c" + i)`, `new String[n]`, `{"a" + i, ...}`) and whose elements are only read in harmless places (receiver of String methods returning primitives, print arguments, operands, for-each variables used the same way) gets `mem_free_elems(x)` before it is freed. An element stored from a literal or another variable, `get(i)` / `xs[i]` kept in a local or passed on, a ternary or lambda around a read, or a for-each variable that escapes switches it off for that container; `remove` / `set` hand back the element, which has left the container. Test: `OwnFreeElems.ash`; the fuzzer (`ownership_fuzz_gen.py`) gained element patterns (alias through a local, keep, ternary, loop variable, remove) and ran about 560 accepted programs clean under AddressSanitizer. `kiln` frees them too (next entry).
24+- **`kiln` frees the Strings inside lists and String arrays** (`mem_free_elems`: a loop over the array's elements that puts each String block back on its free list; the same analysis decides when). An eight-element list built 100 thousand times runs in 12 MB (82 MB with safety off). `ownership_fuzz.sh` with `KILN=1` compared about 470 more random programs on `kiln` against `ashvm`: identical. Benchmark note: relinking the same `ashvm` objects in two other orders moves single benchmarks by about 1.5% either way (this build is the fastest on `countdown`, the others tie), so the earlier 1% swings are link-layout noise; `vm/` is unchanged.
25+- **`kiln` frees on Windows too (static builds).** The free-list table moved to the OS-agnostic `runtime_layout.h` (`KILN_FREELIST_SIZE`) and got a slot in the PE globals (`KILN_PE_FREELIST_ADDR`), `heap_enable_free_lists` and the driver now accept any `--link=static` target, and the allocator code is the same on both OSes. Checked under the Unicorn emulator (`windows_emulated_check.sh`): 46 of 48 programs, including all the ownership tests with their frees, print exactly what the Linux build prints; the two that differ are `ProcFork` (Linux-only syscalls) and `FileIo` (the emulator's fake file system), the same two that differ without any freeing. `run_pe_emulated.py` had stopped working with the 4 GB heap reservation (its fake `VirtualAlloc` handed out addresses overlapping the image at 0x140000000); its heap now starts at 0x200000000. `--link=shared` builds still use the old allocator.
26+- **A loan may not be kept.** `&x` / `&mut x` handed to a project method or constructor whose target is known at compile time (static, private, final, constructor, or a class nothing extends) and that stores, returns or passes on that parameter is now an error: "cannot lend 'a' with '&': 'put' keeps this parameter; pass 'a' by move or clone it". This is Rust's rule that a borrow cannot outlive the call. `&` is new in this work, so no existing code is rejected. Calls whose target is not fixed (overridable or library code) still accept a loan, and the loaned variable is never freed. Test: `OwnLendKept.ash`. `OwnFreeFields` now moves its array into the constructor instead of lending it.
27+- A loop building 300 thousand to 3 million short-lived strings and arrays runs in 12 MB instead of 3.4 GB with output unchanged (`ashvm/tests/cases/ownership/OwnFree.ash`, `OwnFreeFlow.ash`; also run under AddressSanitizer). Benchmarks (`fib`, `loop`, `countdown`, `loop_varbound`, `hashmap`) are within noise of the pre-change binary; projects without the flag generate exactly the old IR.
28+
29+## 2026-10-18 (compile-time safety: constant overflow, constant bounds, opt-in ownership)
30+- **Compile-time denials** (`front/src/jtypes/C/check_expr.c`): a constant `int` expression outside the 32-bit range (`2147483647 + 1`, `100000 * 100000`) and a constant array index that is negative, or past the length of an array literal / `final` array local, are errors. Not applied under `--wrap-int` (wrap-around is the defined behaviour there); division by zero stays Java's runtime exception. No generated code changes, so no runtime cost.
31+- **Ownership checker** (new `front/src/jown`, goals.md item 1, first milestone): with `memory-safety: on` in the project's `ash.pkg`, storing an owning local (`a = b`, `T a = b`, `this.f = b`, `xs[i] = b`, `{b}`) moves it and a later use is a compile error until it is reassigned. Calls, constructors and `return` borrow. It follows `if`/`switch`/`try`/loops (a move that survives to the back edge is caught) and `return`/`throw`/`break`/`continue`. Only the project's own sources are checked. Default is off (`JOWN_DEFAULT_ON` in `jown/H/own.h`) because defaulting on would reject today's examples. Not yet: `&` borrows, lambda-capture moves, freeing at scope exit (nothing is reclaimed yet), the Rust `mnemo.a` runtime, `kiln`'s allocator. Tests: `ashvm/tests/cases/ownership/` (own `ash.pkg`).
32+
133 ## 2026-10-18 (process builtins for the Ash OS)
234 - New kiln builtins `sys_fork()`, `sys_wait(pid)` and `sys_exec(path, arg)` (Linux x86-64 syscalls fork, wait4, execve; `codegen/C/builtins/proc_builtins.c`). They are dispatched in `builtins.c`, listed in the builtin arity table (so they work as values) and reserved in the front end's `lower_expr.c`. Programs reach them through `@Lower` natives like the stdlib does. On a Windows target they emit a runtime error. `ashvm` implements them too (`ashvm/src/builtins/C/proc.c`, ids 44-46, POSIX `fork`/`waitpid`/`execve` with an empty environment, stdout flushed before fork/exec). Both projects cover them with `tests/cases/process/ProcFork.ash`.
335

README.mdmodified

+18−3
@@ -103,10 +103,24 @@ roughly the ceiling for anything short of a full optimizing native compiler.
103103
104104 ### Performance notes
105105
106-- Building a long string one piece at a time with `+=` copies each time, so it is quadratic in time and, since nothing is freed by default, in memory. Collect the pieces and join them once (`StringBuilder`, `String.join`).
107-- `kiln`'s heap grows as needed (64MB at a time, `brk` on Linux / `VirtualAlloc` on Windows) rather than crashing at a fixed size, but nothing is freed until the process exits. `ashvm` has an opt-in collector (`make GC=1`).
106+- Building a long string one piece at a time with `+=` copies each time, so it is quadratic in time. Collect the pieces and join them once (`StringBuilder`, `String.join`).
107+- `kiln`'s heap grows as needed (64MB at a time, `brk` on Linux / `VirtualAlloc` on Windows) rather than crashing at a fixed size, but with `memory-safety: on` it gives back the Strings, one-dimensional arrays, objects, maps and sets the compiler can prove unshared (see Memory safety). `ashvm` frees the same kinds and lists; it also has an opt-in collector (`make GC=1`).
108108 - `kiln`'s Windows output is checked without a Windows machine: `hearth/kiln/tests/scripts/windows_emulated_check.sh` runs every test program as a PE under the Unicorn x86-64 emulator (kernel32 faked over the real filesystem) and compares it with the Linux build. `hearth/kiln/tests/scripts/windows_wine_check.sh` does the same under Wine (an independent `kernel32`). Neither is Windows itself.
109109
110+## Memory safety (`memory-safety` in `ash.pkg`)
111+
112+`memory-safety: on` is the default; `off` switches every check and every free below off and generates exactly the code Ash always did. It is read from the project's `ash.pkg` (no `ash.pkg`: the entry file's directory is the project), and only the project's own sources are checked, never the standard library or dependencies.
113+
114+With it on, at compile time:
115+
116+- **Ownership.** Storing an owning variable (`b = a`, `T b = a`, `this.f = a`, `xs[i] = a`, `{a}`) or passing it to project code moves it unless the callee only reads that parameter (the compiler works that out, so `size(a)` twice is fine); using it afterwards is an error until it gets a new value. Library and native calls only look at their arguments. Moves follow `if`, `switch`, `try`, loops, `return`, `throw`, `break` and `continue`.
117+- **Borrows.** `&x` lends a local (shared), `&mut x` lends it exclusively; both are written only as call arguments. Within one call an exclusive borrow excludes every other use of the same variable, and a loan cannot be handed to a method the compiler can see keeps its parameter (stores, returns or passes it on).
118+- **Constants.** A constant `int` expression outside the 32-bit range (`2147483647 + 1`) and a constant array index that is negative or past the length of a literal / `final` array are errors (not under `--wrap-int`).
119+
120+And at run time memory is freed with no collector (`ashvm`: Strings, arrays, lists, objects, maps and sets; `kiln`: Strings, one-dimensional arrays, objects, maps and sets): a local that is the sole owner of a fresh String, array, `ArrayList`, `HashMap` / `HashSet` or plain object (`new T(..)` whose `this` never escapes, or the fresh result of a project method that returns one) and is only read is freed (with the String and array fields that only it ever references) at the end of its scope, before `return`/`break`/`continue`, and before it is reassigned; concatenation temporaries are freed as soon as they are used, and a list or `String[]` of fresh Strings that are only read in harmless ways frees those Strings with it. Everything the compiler cannot prove unshared is simply left alone, so a leak is possible but a use-after-free is not. A loop building millions of short-lived strings and arrays runs in a few MB.
121+
122+With `memory-safety: off`, `System.free(x)` frees a String, array or object yourself in `ashvm` (`kiln` ignores it); it is an error with safety on. `kiln` runs the same checks and frees them too (static builds, Linux and Windows; `--link=shared` builds keep the old allocator). The shared move-flow engine is the Rust library `bin/lib/mnemo.a` (`hearth/mnemo`), linked by both engines.
123+
110124 ## Package manager (`apm`)
111125
112126 A project is described by `ash.pkg`, a flat `key: value` header plus `[section]` blocks (like `package.json`):
@@ -161,13 +175,14 @@ documentation, `examples/` and the build entry point.
161175 ```
162176 hearth/front/ the only frontend: lexer, parser, AST, type checker, lowering, internal IR lexer,
163177 REPL session model and the compiler's core types (src/jcore, compiled in as C data).
164- Built as libashfront.a, linked by ashvm and kiln. There is no front-end executable.
178+ Built as front.a (in bin/lib/), linked by ashvm and kiln. There is no front-end executable.
165179 hearth/ashvm/ the VM engine: compiler (IR -> bytecode), value, vm, builtins, diagnostics, app (CLI + REPL)
166180 hearth/kiln/ the native compiler: IR -> machine code -> ELF/PE (no VM, no libc)
167181 hearth/apm/ the Ash package manager: apm init/add/install/list/run, dependencies fetched from GitHub
168182 into a project-local @ash-modules/ folder; point ASH_PATH at an installed source root
169183 to compile against it (see ideas/ash_modules.md)
170184 hearth/pyre/ GPU/CPU compute backends (C ABI)
185+hearth/mnemo/ Mnemosyne, memory safety: a Rust static library (bin/lib/mnemo.a) with a C ABI, linked by ashvm and kiln
171186 ```
172187
173188 Every subsystem keeps sources in `C/`, headers in `H/`, build output in a mirrored `O/` tree, and

examples/Desktop.ashmodified

+1−1
@@ -65,7 +65,7 @@ public class Desktop {
6565 Window win = new Window("main", "Ash Demo");
6666 Button ok = new Button("ok", "OK");
6767 win.add(new Label("hint", "Press OK"));
68- win.add(ok);
68+ win.add(&ok); // lent: the button is clicked below
6969 ok.click();
7070 ok.click();
7171 System.out.println(win.render(0).trim());

examples/Generics.ashmodified

+1−1
@@ -66,6 +66,6 @@ public class Generics {
6666 List<String> words = new ArrayList<>();
6767 words.add("pear");
6868 words.add("fig");
69- System.out.println(largest(words) + " " + mapAll(words, w -> w.length()).get(0));
69+ System.out.println(largest(&words) + " " + mapAll(&words, w -> w.length()).get(0));
7070 }
7171 }

hearth/apm/src/C/manifest.cmodified

+2−0
@@ -15,6 +15,7 @@ static const struct { const char *key; size_t offset; size_t size; } header_keys
1515 { "bugs", offsetof(Manifest, bugs), sizeof(((Manifest *)0)->bugs) },
1616 { "main", offsetof(Manifest, main), sizeof(((Manifest *)0)->main) },
1717 { "keywords", offsetof(Manifest, keywords), sizeof(((Manifest *)0)->keywords) },
18+ { "memory-safety", offsetof(Manifest, memory_safety), sizeof(((Manifest *)0)->memory_safety) },
1819 };
1920 #define HEADER_KEYS ((int)(sizeof(header_keys) / sizeof(header_keys[0])))
2021
@@ -30,6 +31,7 @@ void manifest_init_default(Manifest *m, const char *project_name) {
3031 memset(m, 0, sizeof(*m));
3132 snprintf(m->name, sizeof(m->name), "%s", project_name);
3233 snprintf(m->version, sizeof(m->version), "0.1.0");
34+ snprintf(m->memory_safety, sizeof(m->memory_safety), "on");
3335 }
3436
3537 // Trims leading/trailing whitespace in place, returns the trimmed start.

hearth/apm/src/H/manifest.hmodified

+2−0
@@ -32,6 +32,7 @@ typedef struct {
3232 // license: MIT
3333 // main: demo.Main main entry class (qualified) for `apm run start`-style tooling
3434 // keywords: ash, cli keywords comma-separated
35+// memory-safety: on memory-safety on (default) | off: ownership/borrow checks and freeing, read by ashvm and kiln
3536 //
3637 // [dependencies] packages fetched into ./@ash-modules/<name>
3738 // ash-stdlib: github:user/ash-stdlib@v0.1.0
@@ -51,6 +52,7 @@ typedef struct {
5152 char bugs[MANIFEST_VALUE_LEN];
5253 char main[MANIFEST_NAME_LEN];
5354 char keywords[MANIFEST_VALUE_LEN];
55+ char memory_safety[8];
5456 EntryList deps;
5557 EntryList dev_deps;
5658 EntryList libs;

hearth/ashvm/Makefilemodified

+8−3
@@ -25,7 +25,7 @@ ifeq ($(GC),1)
2525 GC_FLAGS = -DASH_GC
2626 endif
2727 CFLAGS = -Wall -Wextra -std=c11 -O2 -Isrc -I../front/src -I../pyre/src -fopenmp $(GC_FLAGS)
28-LDFLAGS = -lm ../../bin/libashfront.a ../../bin/extensions/pyre/libpyre.a -fopenmp -ldl -lvulkan -lstdc++ $(CUDA_LIBS)
28+LDFLAGS = -lm ../../bin/lib/front.a ../../bin/lib/mnemo.a ../../bin/extensions/pyre/libpyre.a -fopenmp -ldl -lvulkan -lstdc++ $(CUDA_LIBS)
2929
3030 ASHVM_SRC = \
3131 src/app/C/main.c \
@@ -54,6 +54,7 @@ ASHVM_SRC = \
5454 src/builtins/C/string.c \
5555 src/builtins/C/matrix.c \
5656 src/builtins/C/proc.c \
57+ src/builtins/C/mem.c \
5758 src/diagnostics/C/diagnostics.c
5859
5960 # Objects and dependency files live in trees parallel to each C tree:
@@ -89,12 +90,16 @@ all: $(BIN)
8990
9091 # The shared front end (lexer, parser, type checker, lowering) is the only frontend ashvm has;
9192 # front's own Makefile decides whether the library is up to date.
92-../../bin/libashfront.a: FORCE
93+../../bin/lib/front.a: FORCE
9394 $(MAKE) -C ../front
9495
96+# Mnemosyne (Rust): the ownership pass in front calls into it.
97+../../bin/lib/mnemo.a: FORCE
98+ $(MAKE) -C ../mnemo
99+
95100 FORCE:
96101
97-$(BIN): $(ASHVM_OBJ) ../../bin/libashfront.a ../../bin/extensions/pyre/libpyre.a
102+$(BIN): $(ASHVM_OBJ) ../../bin/lib/front.a ../../bin/lib/mnemo.a ../../bin/extensions/pyre/libpyre.a
98103 mkdir -p ../../bin
99104 $(CC) $(CFLAGS) -o $(BIN) $(ASHVM_OBJ) $(LDFLAGS)
100105

hearth/ashvm/src/app/C/main.cmodified

+2−0
@@ -6,6 +6,7 @@
66 #include "vm/H/vm.h"
77 #include "diagnostics/H/diagnostics.h"
88 #include "jdriver/H/jdriver.h"
9+#include "jlower/H/jlower.h"
910
1011 void ashvm_execute_ir(char *ir) {
1112 diagnostics_set_source(ir, "<internal ir>");
@@ -25,6 +26,7 @@ int main(int argc, char *argv[]) {
2526 if (strcmp(argv[1], "run") == 0) {
2627 int wrap_int = argc == 4 && strcmp(argv[2], "--wrap-int") == 0;
2728 if (argc != 3 + wrap_int) return usage(argv[0]);
29+ jl_drops = JL_DROP_STRINGS | JL_DROP_ARRAYS | JL_DROP_OBJECTS | JL_DROP_MAPS | JL_DROP_ELEMS; // ashvm can free: lower mem_free for the locals jown proved to be sole owners
2830 char *ir = jdriver_compile_file(argv[2 + wrap_int], wrap_int);
2931 if (!ir) return 1;
3032 ashvm_execute_ir(ir);

hearth/ashvm/src/app/D/main.dmodified

+7−1
@@ -1,11 +1,17 @@
11 src/app/O/main.o: src/app/C/main.c src/app/H/repl.h \
22 src/compiler/H/compiler.h src/vm/H/chunk.h src/value/H/value.h \
33 src/vm/H/vm.h src/diagnostics/H/diagnostics.h \
4- ../front/src/jdriver/H/jdriver.h
4+ ../front/src/jdriver/H/jdriver.h ../front/src/jlower/H/jlower.h \
5+ ../front/src/jtypes/H/jtypes.h ../front/src/jparse/H/jast.h \
6+ ../front/src/jcommon/H/jcommon.h
57 src/app/H/repl.h:
68 src/compiler/H/compiler.h:
79 src/vm/H/chunk.h:
810 src/value/H/value.h:
911 src/vm/H/vm.h:
1012 src/diagnostics/H/diagnostics.h:
1113 ../front/src/jdriver/H/jdriver.h:
14+../front/src/jlower/H/jlower.h:
15+../front/src/jtypes/H/jtypes.h:
16+../front/src/jparse/H/jast.h:
17+../front/src/jcommon/H/jcommon.h:

hearth/ashvm/src/builtins/C/builtins.cmodified

+5−2
@@ -13,7 +13,8 @@ static const char *builtin_names[] = {
1313 "rename_file", "delete_file", "make_dir", "list_dir",
1414 "contains", "starts_with", "ends_with", "repeat",
1515 "exit", "matrix_mul",
16- "sys_fork", "sys_wait", "sys_exec"
16+ "sys_fork", "sys_wait", "sys_exec",
17+ "mem_free", "mem_cat", "mem_release", "mem_free_arr", "mem_free_obj", "mem_free_map", "mem_free_elems"
1718 };
1819 // Argument count each builtin is wrapped with when used as a value
1920 // (`map(xs, str)`); same order as builtin_names.
@@ -27,7 +28,8 @@ static const int builtin_arity[] = {
2728 2, 1, 1, 1,
2829 2, 2, 2, 2,
2930 1, 2,
30- 0, 1, 2
31+ 0, 1, 2,
32+ 1, 3, 1, 1, 1, 1, 1
3133 };
3234 #define NUM_BUILTINS (int)(sizeof(builtin_names) / sizeof(builtin_names[0]))
3335
@@ -52,5 +54,6 @@ VMValue vm_call_builtin(int id, VMValue *args, int argc) {
5254 if (id >= 30 && id <= 33) return vm_call_builtin_collection(id, args, argc); // pop, insert, slice, sort
5355 if (id == 43) return vm_call_builtin_matrix(id, args, argc); // matrix_mul
5456 if (id >= 44 && id <= 46) return vm_call_builtin_proc(id, args, argc); // sys_fork, sys_wait, sys_exec
57+ if (id >= 47 && id <= 53) return vm_call_builtin_mem(id, args, argc); // mem_free
5558 vm_runtime_error("unknown builtin id: %d\n", id);
5659 }

hearth/ashvm/src/builtins/C/core.cmodified

+6−2
@@ -73,8 +73,12 @@ VMValue vm_call_builtin_core(int id, VMValue *args, int argc) {
7373 case VM_NIL: name = "none"; break;
7474 default: name = "unknown"; break;
7575 }
76- char *copy = malloc(strlen(name) + 1); strcpy(copy, name);
77- return vm_str(copy);
76+ // The name is a string literal that lives for the whole run: no copy, so the prelude's many type()
77+ // calls cost no allocation (and leave nothing behind). Not registered with the collector, which
78+ // ignores pointers it did not allocate.
79+ VMValue v; v.type = VM_STR; v.number = (double)strlen(name); v.str = (char *)name;
80+ v.array = NULL; v.map = NULL; v.closure = NULL;
81+ return v;
7882 }
7983 case 42: { // exit -- exit(code) ends the program with that status
8084 if (argc != 1 || args[0].type != VM_NUM) { vm_runtime_error("exit(code) expected\n"); }

hearth/ashvm/src/builtins/C/mem.cadded

+61−0
@@ -0,0 +1,61 @@
1+#include <stdlib.h>
2+#include "builtins/H/internal.h"
3+#include "value/H/hashmap.h"
4+#include "vm/H/vm.h"
5+
6+// mem_release(x) is System.free(x) written by the program (memory-safety: off); mem_free(x) is the compiler's.
7+// mem_cat(a, b, flags) concatenates and frees temporary operands; mem_free(x) frees a String or array that the compiler proved to be the sole owner of a fresh heap value
8+// (front/src/jown, `memory-safety: on` in ash.pkg). Only the value's own buffer is freed; elements of an
9+// array may be shared and are left alone. Anything else is ignored. Under the experimental collector
10+// (make GC=1) the collector owns every object, so freeing here would double-free: it does nothing.
11+VMValue vm_call_builtin_mem(int id, VMValue *args, int argc) {
12+ if (id == 48) { // mem_cat(a, b, flags): a + b, then free the operands that were temporaries (bit 1 = a, bit 2 = b)
13+ if (argc != 3 || args[0].type != VM_STR || args[1].type != VM_STR) { vm_runtime_error("mem_cat(a, b, flags) expects strings\n"); }
14+ VMValue r = vm_concat_strings(args[0], args[1]);
15+#ifndef ASH_GC
16+ int fl = (int)args[2].number;
17+ if (fl & 1) free(args[0].str);
18+ if (fl & 2) free(args[1].str);
19+#endif
20+ return r;
21+ }
22+ if (argc != 1) { vm_runtime_error("mem_free(x) expects 1 argument\n"); }
23+#ifndef ASH_GC
24+ VMValue v = args[0];
25+ if (id == 53) { // mem_free_elems(x): the Strings of a String[] / List<String> that only it references
26+ if (v.type == VM_ARRAY && v.array) {
27+ for (int i = 0; i < v.array->count; i++) if (v.array->items[i].type == VM_STR) free(v.array->items[i].str);
28+ }
29+ return vm_nil();
30+ }
31+ if (v.type == VM_STR) {
32+ free(v.str);
33+ } else if (v.type == VM_ARRAY && v.array) {
34+ free(v.array->items);
35+ free(v.array);
36+ } else if (id == 52 && v.type == VM_MAP && v.map) { // a HashMap / HashSet: the entry chains it built, then the map itself
37+ VMMap *m = v.map;
38+ for (int i = 0; i < m->size; i++) {
39+ if (!m->entries[i].used) continue;
40+ VMValue cur = m->entries[i].value; // [key, value, next] entries; the element count is a number
41+ while (cur.type == VM_ARRAY && cur.array && cur.array->count == 3) {
42+ VMValue next = cur.array->items[2];
43+ free(cur.array->items);
44+ free(cur.array);
45+ cur = next;
46+ }
47+ free(m->entries[i].key);
48+ }
49+ free(m->entries);
50+ free(m->index);
51+ free(m);
52+ } else if (v.type == VM_MAP && v.map) { // an object: its keys, tables and the map; field values are left alone
53+ VMMap *m = v.map;
54+ for (int i = 0; i < m->size; i++) if (m->entries[i].used) free(m->entries[i].key);
55+ free(m->entries);
56+ free(m->index);
57+ free(m);
58+ }
59+#endif
60+ return vm_nil();
61+}

hearth/ashvm/src/builtins/D/mem.dadded

+7−0
@@ -0,0 +1,7 @@
1+src/builtins/O/mem.o: src/builtins/C/mem.c src/builtins/H/internal.h \
2+ src/value/H/value.h src/value/H/hashmap.h src/vm/H/vm.h src/vm/H/chunk.h
3+src/builtins/H/internal.h:
4+src/value/H/value.h:
5+src/value/H/hashmap.h:
6+src/vm/H/vm.h:
7+src/vm/H/chunk.h:

hearth/ashvm/src/builtins/H/internal.hmodified

+1−0
@@ -10,5 +10,6 @@ VMValue vm_call_builtin_string(int id, VMValue *args, int argc);
1010 VMValue vm_call_builtin_file(int id, VMValue *args, int argc);
1111 VMValue vm_call_builtin_matrix(int id, VMValue *args, int argc);
1212 VMValue vm_call_builtin_proc(int id, VMValue *args, int argc);
13+VMValue vm_call_builtin_mem(int id, VMValue *args, int argc);
1314
1415 #endif

hearth/ashvm/tests/cases/memoff/FreeIt.ashadded

+11−0
@@ -0,0 +1,11 @@
1+public class FreeIt {
2+ public static void main(String[] args) {
3+ long total = 0;
4+ for (int i = 0; i < 100000; i++) {
5+ String s = "x" + i;
6+ total += s.length();
7+ System.free(s);
8+ }
9+ System.out.println(total);
10+ }
11+}

hearth/ashvm/tests/cases/memoff/FreeIt.outadded

+1−0
@@ -0,0 +1 @@
1+588890

hearth/ashvm/tests/cases/memoff/ash.pkgadded

+3−0
@@ -0,0 +1,3 @@
1+name: o
2+version: 0.1.0
3+memory-safety: off

hearth/ashvm/tests/cases/ownership/OwnBorrow.ashadded

+14−0
@@ -0,0 +1,14 @@
1+public class OwnBorrow {
2+ static int size(String s) { return s.length(); }
3+ static void append(int[] xs, int[] ys) { xs[0] = ys[0]; }
4+
5+ public static void main(String[] args) {
6+ String a = "abc";
7+ int n = size(&a) + size(&a); // shared borrows may overlap
8+ int[] xs = new int[2];
9+ int[] ys = new int[2];
10+ append(&mut xs, &ys); // different variables: fine
11+ append(&mut xs, &mut ys);
12+ System.out.println(n + a + xs.length);
13+ }
14+}

hearth/ashvm/tests/cases/ownership/OwnBorrow.outadded

+1−0
@@ -0,0 +1 @@
1+6abc2

hearth/ashvm/tests/cases/ownership/OwnBorrowConflict.ashadded

+10−0
@@ -0,0 +1,10 @@
1+public class OwnBorrowConflict {
2+ static void both(int[] x, int[] y) { x[0] = y[0]; }
3+
4+ public static void main(String[] args) {
5+ int[] xs = new int[2];
6+ both(&mut xs, &xs); // error: exclusive and shared borrow of xs in one call
7+ both(&mut xs, xs); // error: exclusive borrow plus another use
8+ int[] ys = &xs; // error: a borrow is only a call argument
9+ }
10+}

hearth/ashvm/tests/cases/ownership/OwnBorrowConflict.outadded

+16−0
@@ -0,0 +1,16 @@
1+error: cannot use 'xs' here: it is already borrowed mutably in this call
2+ --> tests/cases/ownership/OwnBorrowConflict.ash:6:23
3+ |
4+ 6| both(&mut xs, &xs); // error: exclusive and shared borrow of xs in one call
5+ | ^
6+error: cannot use 'xs' here: it is already borrowed mutably in this call
7+ --> tests/cases/ownership/OwnBorrowConflict.ash:7:23
8+ |
9+ 7| both(&mut xs, xs); // error: exclusive borrow plus another use
10+ | ^
11+error: a borrow ('&') can only be passed as a call argument
12+ --> tests/cases/ownership/OwnBorrowConflict.ash:8:20
13+ |
14+ 8| int[] ys = &xs; // error: a borrow is only a call argument
15+ | ^
16+3 error(s)

hearth/ashvm/tests/cases/ownership/OwnBorrowInfer.ashadded

+31−0
@@ -0,0 +1,31 @@
1+public class OwnBorrowInfer {
2+ static class Box {
3+ String held;
4+ void put(String s) { held = s; } // keeps its argument: passing moves
5+ }
6+
7+ static int size(String s) { return s.length(); } // only reads: the caller keeps ownership
8+ static int sum(int[] xs) {
9+ int t = 0;
10+ for (int x : xs) t += x;
11+ return t;
12+ }
13+ static int twice(String s) { return size(s) + size(s); } // passes it on to a reader: still a reader
14+ static String same(String s) { return s; } // returns the caller's value: moves
15+
16+ public static void main(String[] args) {
17+ long total = 0;
18+ for (int i = 0; i < 20000; i++) {
19+ String a = "item" + i;
20+ int[] xs = {i, 1, 2};
21+ total += size(a) + size(a) + twice(a) + sum(xs) + sum(xs); // no move, and a / xs are still freed each iteration
22+ }
23+ String keep = "kept";
24+ Box b = new Box();
25+ b.put(keep);
26+ System.out.println(total + " " + b.held);
27+ String r = "r" + total;
28+ String back = same(r);
29+ System.out.println(back);
30+ }
31+}

hearth/ashvm/tests/cases/ownership/OwnBorrowInfer.outadded

+2−0
@@ -0,0 +1,2 @@
1+400775560 kept
2+r400775560

hearth/ashvm/tests/cases/ownership/OwnBorrowTricky.ashadded

+56−0
@@ -0,0 +1,56 @@
1+import ash.util.ArrayList;
2+import ash.util.List;
3+
4+public class OwnBorrowTricky {
5+ static class Animal {
6+ String name = "animal";
7+ String sound() { return "..."; }
8+ }
9+ static class Dog extends Animal {
10+ static String last;
11+ String sound() { last = name; return "woof"; } // an override that keeps a field: parameters of type Animal may be Dogs
12+ }
13+ static class Point {
14+ int x;
15+ int y;
16+ Point(int x, int y) { this.x = x; this.y = y; }
17+ }
18+
19+ static List<String> sink = new ArrayList<>();
20+
21+ static int reads(String s) { return s.length(); }
22+ static void keeps(String s) { sink.add(s); } // passes it to library code that keeps it
23+ static int lam(String s) {
24+ Runnable r = () -> sink.add(s); // captured by a lambda
25+ r.run();
26+ return 1;
27+ }
28+ static int chain(String s, int depth) {
29+ if (depth == 0) return s.length();
30+ return chain(s, depth - 1); // recursion: treated as escaping, never freed by the caller
31+ }
32+ static int pointSum(Point p) { return p.x + p.y; } // reads fields only: a plain class with no subclass is fine
33+ static String call(Animal a) { return a.sound(); } // may run Dog.sound: never borrowed
34+ static int overwrite(String s) {
35+ s = s + "!"; // the parameter is a copy: the caller's value is untouched
36+ return s.length();
37+ }
38+
39+ public static void main(String[] args) {
40+ long total = 0;
41+ for (int i = 0; i < 2000; i++) {
42+ String a = "a" + i;
43+ String b = "b" + i;
44+ String c = "c" + i;
45+ String d = "d" + i;
46+ String e = "e" + i;
47+ Point p = new Point(i, 1);
48+ total += reads(a) + reads(a) + overwrite(b) + chain(e, 3) + pointSum(p);
49+ keeps(c); // c now lives in sink
50+ total += lam(d); // d now lives in sink
51+ total += reads(b) + a.length();
52+ }
53+ Animal pet = new Dog();
54+ System.out.println(total + " " + sink.size() + " " + sink.get(1999) + " " + call(pet) + " " + Dog.last);
55+ }
56+}

hearth/ashvm/tests/cases/ownership/OwnBorrowTricky.outadded

+1−0
@@ -0,0 +1 @@
1+2058340 4000 d999 woof animal

hearth/ashvm/tests/cases/ownership/OwnBranch.ashadded

+9−0
@@ -0,0 +1,9 @@
1+public class OwnBranch {
2+ public static void main(String[] args) {
3+ String a = "hello";
4+ String keep = "";
5+ if (args.length > 0) keep = a; // moved on one path only
6+ System.out.println(a); // error: maybe moved
7+ System.out.println(keep);
8+ }
9+}

hearth/ashvm/tests/cases/ownership/OwnBranch.outadded

+6−0
@@ -0,0 +1,6 @@
1+error: use of moved value 'a': it was moved earlier by an assignment or by passing it to a call; lend it with &a, clone it, or assign a new value first
2+ --> tests/cases/ownership/OwnBranch.ash:6:28
3+ |
4+ 6| System.out.println(a); // error: maybe moved
5+ | ^
6+1 error(s)

hearth/ashvm/tests/cases/ownership/OwnFree.ashadded

+18−0
@@ -0,0 +1,18 @@
1+public class OwnFree {
2+ public static void main(String[] args) {
3+ long total = 0;
4+ for (int i = 0; i < 300000; i++) {
5+ String s = "item-" + i; // fresh String, sole owner: freed at the end of each iteration
6+ int[] xs = new int[16]; // fresh array, sole owner
7+ xs[3] = i;
8+ String t = s + "!";
9+ t += s; // reassigned with a fresh value: the old one is freed first
10+ if (i % 100000 == 0) {
11+ System.out.println(t);
12+ continue; // continue frees what the iteration owns
13+ }
14+ total += s.length() + xs.length + t.length() + xs[3] % 7;
15+ }
16+ System.out.println(total);
17+ }
18+}

hearth/ashvm/tests/cases/ownership/OwnFree.outadded

+4−0
@@ -0,0 +1,4 @@
1+item-0!item-0
2+item-100000!item-100000
3+item-200000!item-200000
4+15566524

hearth/ashvm/tests/cases/ownership/OwnFreeAlias.ashadded

+49−0
@@ -0,0 +1,49 @@
1+import ash.util.ArrayList;
2+import ash.util.List;
3+
4+public class OwnFreeAlias {
5+ static class A {
6+ String get(int i) { return "a" + i; } // fresh here...
7+ }
8+ static class B extends A {
9+ String shared = "shared";
10+ String get(int i) { return shared; } // ...but an override returns an alias: calls through A are never fresh
11+ }
12+
13+ static class Box {
14+ String held;
15+ void keep(String s) { held = s; } // keeps its argument
16+ }
17+
18+ static String grow(int n) {
19+ if (n == 0) return "";
20+ String rest = grow(n - 1); // recursive: never treated as fresh
21+ return rest + n;
22+ }
23+
24+ public static void main(String[] args) {
25+ long total = 0;
26+ List<String> kept = new ArrayList<>();
27+ Box box = new Box();
28+ for (int i = 0; i < 20000; i++) {
29+ A a = (i % 2 == 0) ? new A() : new B();
30+ String g = a.get(i); // virtual: must stay alive when it is B's field
31+ String t = "t" + i;
32+ String u = "u" + i;
33+ bool same = t.equals(u) || t.contains("1"); // reads only: t and u stay sole owners
34+ String v = (i % 3 == 0) ? t : u; // aliases: t and u must not be freed
35+ kept.add("k" + i); // fresh temp handed to a collection: never freed here
36+ if (i % 5000 == 0) box.keep("b" + i); // the argument outlives the iteration
37+ String w = grow(3);
38+ total += g.length() + v.length() + (same ? 1 : 0) + w.length();
39+ int[] row = new int[4];
40+ for (int x : row) total += x; // for-each over a sole-owner array
41+ switch (i % 3) {
42+ case 0 -> { String z = "z" + i; total += z.length(); }
43+ case 1 -> { int[] y = new int[2]; total += y.length; }
44+ default -> total += 1;
45+ }
46+ }
47+ System.out.println(total + " " + kept.size() + " " + kept.get(19999) + " " + box.held + " " + new B().shared);
48+ }
49+}

hearth/ashvm/tests/cases/ownership/OwnFreeAlias.outadded

+1−0
@@ -0,0 +1 @@
1+353070 20000 k19999 b15000 shared

hearth/ashvm/tests/cases/ownership/OwnFreeDenied.ashadded

+11−0
@@ -0,0 +1,11 @@
1+public class OwnFreeDenied {
2+ public static void main(String[] args) {
3+ long total = 0;
4+ for (int i = 0; i < 100000; i++) {
5+ String s = "x" + i;
6+ total += s.length();
7+ System.free(s);
8+ }
9+ System.out.println(total);
10+ }
11+}

hearth/ashvm/tests/cases/ownership/OwnFreeDenied.outadded

+11−0
@@ -0,0 +1,11 @@
1+error: System.free is only available with 'memory-safety: off' in ash.pkg: with it on, memory is freed for you
2+ --> tests/cases/ownership/OwnFreeDenied.ash:7:13
3+ |
4+ 7| System.free(s);
5+ | ^
6+error: System.free is only available with 'memory-safety: off' in ash.pkg: with it on, memory is freed for you
7+ --> tests/cases/ownership/OwnFreeDenied.ash:7:13
8+ |
9+ 7| System.free(s);
10+ | ^
11+2 error(s)

hearth/ashvm/tests/cases/ownership/OwnFreeElems.ashadded

+60−0
@@ -0,0 +1,60 @@
1+import ash.util.ArrayList;
2+import ash.util.List;
3+
4+public class OwnFreeElems {
5+ static class Bag {
6+ List<String> names = new ArrayList<>(); // owned, only fresh adds, only harmless reads: its Strings die with the Bag
7+ void add(int i) { names.add("n" + i); }
8+ int total() {
9+ int t = 0;
10+ for (int i = 0; i < names.size(); i++) t += names.get(i).length();
11+ return t;
12+ }
13+ }
14+
15+ static String last; // keeps an element alive past its list
16+
17+ public static void main(String[] args) {
18+ long total = 0;
19+ for (int i = 0; i < 20000; i++) {
20+ List<String> a = new ArrayList<>(); // fresh adds + harmless reads: elements freed with the list
21+ a.add("a" + i);
22+ a.add("b" + i);
23+ total += a.get(0).length() + a.size();
24+ if (i % 10000 == 0) System.out.println(a.get(1));
25+ for (String s : a) total += s.length(); // the loop variable is only read
26+
27+ List<String> b = new ArrayList<>(); // a literal element: the list is freed, the literal never is
28+ b.add("lit");
29+ b.add("v" + i);
30+ total += b.get(0).length();
31+
32+ List<String> c = new ArrayList<>(); // an element escapes through a local alias
33+ c.add("c" + i);
34+ String kept = c.get(0);
35+ if (i == 7) last = kept;
36+
37+ String[] d = new String[2]; // fresh stores only
38+ d[0] = "d" + i;
39+ d[1] = "e" + i;
40+ total += d[0].length() + d[1].length();
41+
42+ String[] e = new String[2]; // a shared store
43+ String shared = "shared" + i;
44+ total += shared.length();
45+ e[0] = shared;
46+ e[1] = "f" + i;
47+ total += e[0].length();
48+
49+ List<String> f = new ArrayList<>(); // loop variable escapes
50+ f.add("g" + i);
51+ for (String s : f) { if (i == 9) last = s; }
52+
53+ Bag bag = new Bag();
54+ bag.add(i);
55+ bag.add(2);
56+ total += bag.total();
57+ }
58+ System.out.println(total + " " + last);
59+ }
60+}

hearth/ashvm/tests/cases/ownership/OwnFreeElems.outadded

+3−0
@@ -0,0 +1,3 @@
1+b0
2+b10000
3+1211120 g9

hearth/ashvm/tests/cases/ownership/OwnFreeFactory.ashadded

+47−0
@@ -0,0 +1,47 @@
1+public class OwnFreeFactory {
2+ static class Point {
3+ int x;
4+ int y;
5+ Point(int x, int y) { this.x = x; this.y = y; }
6+ int sum() { return x + y; }
7+ }
8+
9+ static class Shape {
10+ int w = 3;
11+ int area() { return w * w; }
12+ }
13+
14+ static class Square extends Shape {
15+ int area() { return w * w * 2; } // Shape has a subclass: its instances are never assumed exact
16+ }
17+
18+ static Point make(int i) {
19+ Point p = new Point(i, 1); // sole owner, handed to the caller
20+ p.x = p.x + 1;
21+ return p;
22+ }
23+
24+ static Point origin() { return new Point(0, 0); }
25+
26+ static Shape shape(int i) {
27+ if (i % 2 == 0) return new Shape();
28+ return new Square(); // two classes: never fresh
29+ }
30+
31+ static Point cached = new Point(5, 5);
32+ static Point shared() {
33+ return cached; // an alias of a static: never fresh
34+ }
35+
36+ public static void main(String[] args) {
37+ long total = 0;
38+ for (int i = 0; i < 100000; i++) {
39+ Point a = make(i); // freed each iteration
40+ Point b = origin(); // freed each iteration
41+ Shape s = shape(i);
42+ Point c = shared(); // alias: must survive
43+ total += a.sum() + b.sum() + s.area() + c.sum();
44+ }
45+ System.out.println(total + " " + shared().sum());
46+ }
47+}

hearth/ashvm/tests/cases/ownership/OwnFreeFactory.outadded

+1−0
@@ -0,0 +1 @@
1+5002500000 10

hearth/ashvm/tests/cases/ownership/OwnFreeFields.ashadded

+32−0
@@ -0,0 +1,32 @@
1+public class OwnFreeFields {
2+ static class Buffer {
3+ int[] cells = new int[4]; // fresh initializer, only indexed: owned
4+ String label; // only ever given fresh concatenations: owned
5+ String shared = "shared"; // a literal initializer is shared: never owned
6+ int[] borrowed; // assigned from a parameter: never owned
7+ String leaked; // returned by a getter: never owned
8+
9+ Buffer(int n, int[] given) {
10+ label = "buf" + n;
11+ borrowed = given;
12+ leaked = "leak" + n;
13+ cells[1] = n;
14+ }
15+ void relabel(int n) { label = label + n; cells = new int[2]; } // reassigning an owned field frees its old value
16+ int total() { return cells[0] + cells[1] + label.length() + borrowed.length + shared.length(); }
17+ String leak() { return leaked; } // lets the caller hold the field's value
18+ }
19+
20+ public static void main(String[] args) {
21+ String kept = "";
22+ long sum = 0;
23+ for (int i = 0; i < 20000; i++) {
24+ int[] given = {1, 2, 3}; // moved into the Buffer, which keeps it in a field
25+ Buffer b = new Buffer(i, given); // freed each iteration together with cells and label
26+ b.relabel(i);
27+ sum += b.total();
28+ if (i == 7) kept = b.leak(); // aliases leaked: it must survive b
29+ }
30+ System.out.println(sum + " " + kept);
31+ }
32+}

hearth/ashvm/tests/cases/ownership/OwnFreeFields.outadded

+1−0
@@ -0,0 +1 @@
1+417780 leak7

hearth/ashvm/tests/cases/ownership/OwnFreeFlow.ashadded

+44−0
@@ -0,0 +1,44 @@
1+public class OwnFreeFlow {
2+ static int keep(String s) { return s.length(); }
3+
4+ static String build(int n) {
5+ String acc = "";
6+ for (int i = 0; i < n; i++) {
7+ acc += i; // sole owner, reassigned with a fresh value each time
8+ if (acc.length() > 6) break; // break frees acc's loop-scoped siblings only
9+ }
10+ String out = acc + "|" + n; // fresh; escapes through return below, so it is never freed
11+ return out;
12+ }
13+
14+ static int early(int n) {
15+ String a = "x" + n;
16+ int[] xs = {n, n + 1, n + 2};
17+ if (n > 5) return xs[1] + a.length(); // return frees a and xs after computing the value
18+ String b = a + a;
19+ return b.length() + xs.length;
20+ }
21+
22+ public static void main(String[] args) {
23+ System.out.println(build(3) + " " + build(9));
24+ System.out.println(early(2) + " " + early(7));
25+ String shared = "s" + args.length;
26+ int k = keep(&shared) + keep(&shared); // lent to a call: not a sole-owner use, never freed
27+ String again = shared; // moved
28+ Runnable r = () -> System.out.println(again + k);
29+ r.run();
30+ for (int i = 0; i < 3; i++) {
31+ String line = "line " + i;
32+ String twice = line + line;
33+ int[] box = new int[2];
34+ box[1] = twice.length();
35+ while (true) {
36+ String inner = twice + box[1];
37+ System.out.println(inner);
38+ break; // break frees inner only, not line/twice/box
39+ }
40+ if (i == 1) continue;
41+ System.out.println(line + box.length);
42+ }
43+ }
44+}

hearth/ashvm/tests/cases/ownership/OwnFreeFlow.outadded

+8−0
@@ -0,0 +1,8 @@
1+012|3 0123456|9
2+7 10
3+s04
4+line 0line 012
5+line 02
6+line 1line 112
7+line 2line 212
8+line 22

hearth/ashvm/tests/cases/ownership/OwnFreeLists.ashadded

+48−0
@@ -0,0 +1,48 @@
1+import ash.util.ArrayList;
2+import ash.util.List;
3+
4+public class OwnFreeLists {
5+ static class Bag {
6+ List<Integer> items = new ArrayList<>(); // owned: only filled and read through list methods
7+ List<Integer> view; // assigned from elsewhere: shared
8+ void put(int v) { items.add(v); }
9+ int sum() {
10+ int t = 0;
11+ for (int i = 0; i < items.size(); i++) t += items.get(i);
12+ return t;
13+ }
14+ }
15+
16+ static List<Integer> build(int n) {
17+ List<Integer> out = new ArrayList<>();
18+ for (int i = 0; i < n; i++) out.add(i);
19+ return out; // handed to the caller
20+ }
21+
22+ static List<Integer> keep;
23+
24+ public static void main(String[] args) {
25+ long total = 0;
26+ for (int i = 0; i < 20000; i++) {
27+ List<Integer> xs = new ArrayList<>(); // sole owner: freed each iteration
28+ xs.add(i);
29+ xs.add(i + 1);
30+ xs.set(0, 5);
31+ total += xs.size() + xs.get(1);
32+ List<Integer> ys = build(3); // fresh from a factory
33+ for (int y : ys) total += y;
34+ List<Integer> zs = new ArrayList<>(); // passed on: not freed
35+ zs.add(7);
36+ if (i == 3) keep = zs;
37+ Bag b = new Bag();
38+ b.put(i);
39+ b.put(2);
40+ total += b.sum();
41+ List<Integer> ws = new ArrayList<>();
42+ ws.add(1);
43+ List<Integer> sub = ws.subList(0, 1); // a copy / view: ws is not a sole owner of what sub sees
44+ total += sub.size();
45+ }
46+ System.out.println(total + " " + keep.size() + " " + keep.get(0));
47+ }
48+}

hearth/ashvm/tests/cases/ownership/OwnFreeLists.outadded

+1−0
@@ -0,0 +1 @@
1+400160000 1 7

hearth/ashvm/tests/cases/ownership/OwnFreeLoops.ashadded

+85−0
@@ -0,0 +1,85 @@
1+public class OwnFreeLoops {
2+ static int classify(int i) {
3+ switch (i % 4) {
4+ case 0:
5+ String z = "zero" + i;
6+ return z.length();
7+ case 1: {
8+ int[] t = new int[i % 5 + 1];
9+ return t.length;
10+ }
11+ default:
12+ return -1;
13+ }
14+ }
15+
16+ static String nested(int n) {
17+ String out = "";
18+ for (int i = 0; i < n; i++) {
19+ String row = "r" + i;
20+ int[] cells = new int[3];
21+ for (int j = 0; j < 3; j++) {
22+ String cell = row + "c" + j;
23+ if (j == 1) continue;
24+ cells[j] = cell.length();
25+ switch (j) {
26+ case 2:
27+ if (i == 1) break;
28+ out += cell;
29+ break;
30+ default:
31+ out += "-";
32+ }
33+ }
34+ if (i == 2) break;
35+ out += cells[0] + ";";
36+ }
37+ return out;
38+ }
39+
40+ static int caught(int n) {
41+ String a = "a" + n;
42+ try {
43+ String b = a + "b";
44+ if (n == 1) throw new RuntimeException(b);
45+ return b.length();
46+ } catch (RuntimeException e) {
47+ String c = a + "c";
48+ return c.length() + 1;
49+ } finally {
50+ String d = a + "d";
51+ System.out.println("fin " + d);
52+ }
53+ }
54+
55+ static int whileLoop() {
56+ int total = 0;
57+ int i = 0;
58+ while (i < 5) {
59+ String s = "w" + i;
60+ i++;
61+ if (s.length() > 100) continue;
62+ do {
63+ int[] k = new int[2];
64+ total += k.length + s.length();
65+ } while (false);
66+ }
67+ return total;
68+ }
69+
70+ public static void main(String[] args) {
71+ long sum = 0;
72+ for (int i = 0; i < 8; i++) sum += classify(i);
73+ System.out.println(sum);
74+ System.out.println(nested(5));
75+ System.out.println(caught(0) + " " + caught(1) + " " + caught(2));
76+ System.out.println(whileLoop());
77+ String[] words = {"p" + 1, "q" + 2, "r" + 3};
78+ String joined = "";
79+ for (String w : words) {
80+ String up = w + "!";
81+ joined += up;
82+ }
83+ System.out.println(joined + words.length);
84+ }
85+}

hearth/ashvm/tests/cases/ownership/OwnFreeLoops.outadded

+8−0
@@ -0,0 +1,8 @@
1+9
2+-r0c24;-4;-r2c2
3+fin a0d
4+fin a1d
5+fin a2d
6+3 4 3
7+20
8+p1!q2!r3!3

hearth/ashvm/tests/cases/ownership/OwnFreeMaps.ashadded

+54−0
@@ -0,0 +1,54 @@
1+import ash.util.ArrayList;
2+import ash.util.HashMap;
3+import ash.util.HashSet;
4+import ash.util.List;
5+import ash.util.Map;
6+import ash.util.Set;
7+
8+public class OwnFreeMaps {
9+ static class Index {
10+ Map<String, Integer> counts = new HashMap<>(); // owned field
11+ void see(String w) {
12+ Integer? n = counts.get(w);
13+ counts.put(w, n == null ? 1 : n + 1);
14+ }
15+ int distinct() { return counts.size(); }
16+ }
17+
18+ static Map<String, Integer> shared = new HashMap<>();
19+
20+ static int lookup(Map<String, Integer> m, String k) { return m.containsKey(k) ? 1 : 0; } // reads only
21+
22+ public static void main(String[] args) {
23+ long total = 0;
24+ Map<String, Integer> kept = new HashMap<>();
25+ for (int i = 0; i < 5000; i++) {
26+ Map<String, Integer> m = new HashMap<>(); // sole owner: freed with its entry chains each iteration
27+ m.put("a" + i, i);
28+ m.put("b", 2);
29+ m.put("b", 3);
30+ total += m.size() + (m.containsKey("b") ? 1 : 0);
31+ if (m.containsKey("a" + i)) total += 1;
32+ m.remove("b");
33+ List<String> ks = m.keys();
34+ total += ks.size();
35+ Set<Integer> s = new HashSet<>();
36+ s.add(i % 7);
37+ s.add(3);
38+ s.add(3);
39+ total += s.size();
40+ if (s.contains(3)) total += 1;
41+ total += lookup(m, "a" + i); // a callee that only reads: still a sole owner
42+ Index ix = new Index();
43+ ix.see("x");
44+ ix.see("x");
45+ ix.see("y");
46+ total += ix.distinct();
47+ Map<String, Integer> k2 = new HashMap<>();
48+ k2.put("k", i);
49+ if (i == 4) kept = k2; // escapes: not freed
50+ if (i == 5) shared = m;
51+ }
52+ System.out.println(total + " " + kept.get("k") + " " + shared.size());
53+ }
54+}

hearth/ashvm/tests/cases/ownership/OwnFreeMaps.outadded

+1−0
@@ -0,0 +1 @@
1+54286 4 1

hearth/ashvm/tests/cases/ownership/OwnFreeObj.ashadded

+36−0
@@ -0,0 +1,36 @@
1+public class OwnFreeObj {
2+ static class Point {
3+ int x;
4+ int y;
5+ Point(int x, int y) { this.x = x; this.y = y; }
6+ int sum() { return x + y; }
7+ void bump() { x = x + 1; }
8+ }
9+
10+ static class Leaky {
11+ int v;
12+ Leaky(int v) { this.v = v; }
13+ Leaky self() { return this; } // `this` escapes: instances are never freed
14+ }
15+
16+ static Point kept;
17+
18+ public static void main(String[] args) {
19+ long total = 0;
20+ for (int i = 0; i < 200000; i++) {
21+ Point p = new Point(i, 2); // sole owner: freed at the end of each iteration
22+ p.bump();
23+ Point q = new Point(1, 1);
24+ q.x = p.sum();
25+ if (i % 100000 == 0) {
26+ System.out.println(q.sum());
27+ continue;
28+ }
29+ Leaky l = new Leaky(i);
30+ Leaky same = l.self(); // l is not a sole owner
31+ if (i == 5) kept = new Point(7, 8);
32+ total += q.sum() + same.v % 3;
33+ }
34+ System.out.println(total + " " + kept.sum());
35+ }
36+}

hearth/ashvm/tests/cases/ownership/OwnFreeObj.outadded

+3−0
@@ -0,0 +1,3 @@
1+4
2+100004
3+20000799990 15

hearth/ashvm/tests/cases/ownership/OwnFreeReturn.ashadded

+32−0
@@ -0,0 +1,32 @@
1+public class OwnFreeReturn {
2+ static String label(int i) {
3+ String s = "n" + i; // sole owner handed to the caller by `return s`
4+ return s;
5+ }
6+
7+ static String pick(int i) {
8+ if (i % 2 == 0) return "even" + i;
9+ return "odd" + i; // both returns are fresh
10+ }
11+
12+ static String name = "shared";
13+ static String alias(int i) { return name; } // not fresh: callers must never free it
14+
15+ static int[] table(int n) {
16+ int[] t = new int[n];
17+ t[0] = n;
18+ return t;
19+ }
20+
21+ public static void main(String[] args) {
22+ long total = 0;
23+ for (int i = 0; i < 200000; i++) {
24+ String a = label(i); // fresh result: sole owner, freed each iteration
25+ String b = pick(i);
26+ String c = alias(i); // aliases a static: never freed
27+ int[] t = table(8);
28+ total += a.length() + b.length() + c.length() + t[0];
29+ }
30+ System.out.println(total + " " + name);
31+ }
32+}

hearth/ashvm/tests/cases/ownership/OwnFreeReturn.outadded

+1−0
@@ -0,0 +1 @@
1+5877780 shared

hearth/ashvm/tests/cases/ownership/OwnFreeTry.ashadded

+61−0
@@ -0,0 +1,61 @@
1+public class OwnFreeTry {
2+ static int guarded(int n) {
3+ String s = "guard" + n;
4+ int[] box = new int[2];
5+ try {
6+ if (n > 2) return s.length(); // finally still reads s after the return value is computed
7+ box[0] = n;
8+ } finally {
9+ System.out.println("finally " + s + " " + box.length);
10+ }
11+ return -1;
12+ }
13+
14+ static void loopy() {
15+ for (int i = 0; i < 3; i++) {
16+ String s = "loop" + i;
17+ try {
18+ if (i == 1) continue; // finally runs after continue
19+ if (i == 2) break;
20+ System.out.println("body " + s);
21+ } finally {
22+ System.out.println("fin " + s);
23+ }
24+ }
25+ }
26+
27+ static int thrower(int n) {
28+ String s = "t" + n;
29+ try {
30+ if (n > 0) throw new RuntimeException("boom");
31+ return s.length();
32+ } catch (RuntimeException e) {
33+ System.out.println("caught " + s); // s must still be alive in the handler
34+ return -2;
35+ }
36+ }
37+
38+ static String pickCase(int k) {
39+ switch (k) {
40+ case 0: {
41+ String a = "zero" + k;
42+ return a + "!";
43+ }
44+ case 1: {
45+ int[] t = new int[3];
46+ t[0] = k;
47+ return "one" + t.length;
48+ }
49+ default:
50+ String d = "dflt" + k;
51+ return d;
52+ }
53+ }
54+
55+ public static void main(String[] args) {
56+ System.out.println(guarded(5) + " " + guarded(1));
57+ loopy();
58+ System.out.println(thrower(0) + " " + thrower(1));
59+ System.out.println(pickCase(0) + pickCase(1) + pickCase(9));
60+ }
61+}

hearth/ashvm/tests/cases/ownership/OwnFreeTry.outadded

+8−0
@@ -0,0 +1,8 @@
1+finally guard5 2
2+finally guard1 2
3+6 -1
4+body loop0
5+fin loop0
6+caught t1
7+2 -2
8+zero0!one3dflt9

hearth/ashvm/tests/cases/ownership/OwnLendKept.ashadded

+19−0
@@ -0,0 +1,19 @@
1+public class OwnLendKept {
2+ static class Box {
3+ String held;
4+ void put(String s) { held = s; } // keeps its parameter
5+ }
6+ static String same(String s) { return s; } // returns its parameter
7+ static int size(String s) { return s.length(); }
8+
9+ public static void main(String[] args) {
10+ Box b = new Box();
11+ String a = "abc";
12+ b.put(&a); // error: put keeps a, so it cannot be a loan (Box has no subclass, so the target is known)
13+ String c = "def";
14+ String d = same(&c); // error: same returns its parameter
15+ String e = "ghi";
16+ int n = size(&e); // fine: size only reads
17+ System.out.println(n + b.held + d);
18+ }
19+}

hearth/ashvm/tests/cases/ownership/OwnLendKept.outadded

+11−0
@@ -0,0 +1,11 @@
1+error: cannot lend 'a' with '&': 'put' keeps this parameter (stores, returns or passes it on); pass 'a' by move or clone it
2+ --> tests/cases/ownership/OwnLendKept.ash:12:15
3+ |
4+ 12| b.put(&a); // error: put keeps a, so it cannot be a loan (Box has no subclass, so the target is known)
5+ | ^
6+error: cannot lend 'c' with '&': 'same' keeps this parameter (stores, returns or passes it on); pass 'c' by move or clone it
7+ --> tests/cases/ownership/OwnLendKept.ash:14:25
8+ |
9+ 14| String d = same(&c); // error: same returns its parameter
10+ | ^
11+2 error(s)

hearth/ashvm/tests/cases/ownership/OwnLoop.ashadded

+10−0
@@ -0,0 +1,10 @@
1+public class OwnLoop {
2+ public static void main(String[] args) {
3+ String a = "hello";
4+ String keep = "";
5+ for (int i = 0; i < 3; i++) {
6+ keep = a; // error: moved in the previous iteration
7+ }
8+ System.out.println(keep);
9+ }
10+}

hearth/ashvm/tests/cases/ownership/OwnLoop.outadded

+6−0
@@ -0,0 +1,6 @@
1+error: use of moved value 'a': it was moved earlier by an assignment or by passing it to a call; lend it with &a, clone it, or assign a new value first
2+ --> tests/cases/ownership/OwnLoop.ash:6:20
3+ |
4+ 6| keep = a; // error: moved in the previous iteration
5+ | ^
6+1 error(s)

hearth/ashvm/tests/cases/ownership/OwnMoved.ashadded

+8−0
@@ -0,0 +1,8 @@
1+public class OwnMoved {
2+ public static void main(String[] args) {
3+ String a = "hello";
4+ String b = a;
5+ System.out.println(a); // error: a was moved
6+ System.out.println(b);
7+ }
8+}

hearth/ashvm/tests/cases/ownership/OwnMoved.outadded

+6−0
@@ -0,0 +1,6 @@
1+error: use of moved value 'a': it was moved earlier by an assignment or by passing it to a call; lend it with &a, clone it, or assign a new value first
2+ --> tests/cases/ownership/OwnMoved.ash:5:28
3+ |
4+ 5| System.out.println(a); // error: a was moved
5+ | ^
6+1 error(s)

hearth/ashvm/tests/cases/ownership/OwnMovedCall.ashadded

+9−0
@@ -0,0 +1,9 @@
1+public class OwnMovedCall {
2+ static String same(String s) { return s; } // hands its parameter back: the caller's value is moved in
3+
4+ public static void main(String[] args) {
5+ String a = "abc";
6+ String n = same(a); // moves a into the callee
7+ System.out.println(a + n); // error: a was moved by the call
8+ }
9+}

hearth/ashvm/tests/cases/ownership/OwnMovedCall.outadded

+6−0
@@ -0,0 +1,6 @@
1+error: use of moved value 'a': it was moved earlier by an assignment or by passing it to a call; lend it with &a, clone it, or assign a new value first
2+ --> tests/cases/ownership/OwnMovedCall.ash:7:28
3+ |
4+ 7| System.out.println(a + n); // error: a was moved by the call
5+ | ^
6+1 error(s)

hearth/ashvm/tests/cases/ownership/OwnMovedKeep.ashadded

+17−0
@@ -0,0 +1,17 @@
1+public class OwnMovedKeep {
2+ static String same(String s) { return s; }
3+ static class Box {
4+ String held;
5+ void put(String s) { held = s; }
6+ }
7+
8+ public static void main(String[] args) {
9+ String a = "abc";
10+ String back = same(a); // returns its parameter: a is moved
11+ System.out.println(a + back); // error
12+ Box b = new Box();
13+ String c = "def";
14+ b.put(c); // stores its parameter: c is moved
15+ System.out.println(c); // error
16+ }
17+}

hearth/ashvm/tests/cases/ownership/OwnMovedKeep.outadded

+11−0
@@ -0,0 +1,11 @@
1+error: use of moved value 'a': it was moved earlier by an assignment or by passing it to a call; lend it with &a, clone it, or assign a new value first
2+ --> tests/cases/ownership/OwnMovedKeep.ash:11:28
3+ |
4+ 11| System.out.println(a + back); // error
5+ | ^
6+error: use of moved value 'c': it was moved earlier by an assignment or by passing it to a call; lend it with &c, clone it, or assign a new value first
7+ --> tests/cases/ownership/OwnMovedKeep.ash:15:28
8+ |
9+ 15| System.out.println(c); // error
10+ | ^
11+2 error(s)

hearth/ashvm/tests/cases/ownership/OwnOk.ashadded

+31−0
@@ -0,0 +1,31 @@
1+public class OwnOk {
2+ static int len(String s) { return s.length(); }
3+
4+ public static void main(String[] args) {
5+ String a = "hello";
6+ int n = len(&a) + len(&a); // &a lends it: a stays usable
7+ String b = a; // a moved into b
8+ a = "again"; // reinitialised: usable again
9+ System.out.println(n + b + a);
10+
11+ String c = "x";
12+ if (n > 100) {
13+ String d = c; // moved on a path that returns
14+ System.out.println(d);
15+ return;
16+ }
17+ System.out.println(c);
18+
19+ for (int i = 0; i < 3; i++) {
20+ String e = "iter" + i; // a fresh variable every iteration
21+ String f = e;
22+ System.out.println(f);
23+ }
24+ String g = "g";
25+ while (true) {
26+ String h = g; // moved, then the loop is left
27+ System.out.println(h);
28+ break;
29+ }
30+ }
31+}

hearth/ashvm/tests/cases/ownership/OwnOk.outadded

+6−0
@@ -0,0 +1,6 @@
1+10helloagain
2+x
3+iter0
4+iter1
5+iter2
6+g

hearth/ashvm/tests/cases/ownership/ash.pkgadded

+3−0
@@ -0,0 +1,3 @@
1+name: ownership-tests
2+version: 0.1.0
3+memory-safety: on

hearth/ashvm/tests/cases/safety/ConstIndex.ashadded

+11−0
@@ -0,0 +1,11 @@
1+public class ConstIndex {
2+ public static void main(String[] args) {
3+ final int[] xs = new int[3];
4+ int[] lit = {1, 2};
5+ int a = xs[3]; // past the end of a final array
6+ int b = xs[-1]; // negative
7+ int c = 0;
8+ int d = xs[2];
9+ System.out.println(a + b + c + d);
10+ }
11+}

hearth/ashvm/tests/cases/safety/ConstIndex.outadded

+11−0
@@ -0,0 +1,11 @@
1+error: array index 3 is out of bounds for an array of length 3
2+ --> tests/cases/safety/ConstIndex.ash:5:17
3+ |
4+ 5| int a = xs[3]; // past the end of a final array
5+ | ^
6+error: array index -1 is negative
7+ --> tests/cases/safety/ConstIndex.ash:6:17
8+ |
9+ 6| int b = xs[-1]; // negative
10+ | ^
11+2 error(s)

hearth/ashvm/tests/cases/safety/ConstOverflow.ashadded

+13−0
@@ -0,0 +1,13 @@
1+public class ConstOverflow {
2+ static final int BIG = 2000000000;
3+ public static void main(String[] args) {
4+ int a = Integer.MAX_VALUE + 1;
5+ final int n = 100000;
6+ int b = n * n;
7+ int c = BIG + BIG;
8+ int d = Integer.MIN_VALUE - 1;
9+ int ok = BIG - BIG + n * 2;
10+ long fine = Integer.MAX_VALUE + 1L;
11+ System.out.println(a + b + c + d + ok + fine);
12+ }
13+}

hearth/ashvm/tests/cases/safety/ConstOverflow.outadded

+21−0
@@ -0,0 +1,21 @@
1+error: arithmetic overflow: constant expression is outside the range of int; use an 'L' suffix for long
2+ --> tests/cases/safety/ConstOverflow.ash:4:17
3+ |
4+ 4| int a = Integer.MAX_VALUE + 1;
5+ | ^
6+error: arithmetic overflow: constant expression is outside the range of int; use an 'L' suffix for long
7+ --> tests/cases/safety/ConstOverflow.ash:6:17
8+ |
9+ 6| int b = n * n;
10+ | ^
11+error: arithmetic overflow: constant expression is outside the range of int; use an 'L' suffix for long
12+ --> tests/cases/safety/ConstOverflow.ash:7:17
13+ |
14+ 7| int c = BIG + BIG;
15+ | ^
16+error: arithmetic overflow: constant expression is outside the range of int; use an 'L' suffix for long
17+ --> tests/cases/safety/ConstOverflow.ash:8:17
18+ |
19+ 8| int d = Integer.MIN_VALUE - 1;
20+ | ^
21+4 error(s)

hearth/ashvm/tests/scripts/memory_check.shadded

+51−0
@@ -0,0 +1,51 @@
1+#!/usr/bin/env bash
2+# Memory stays bounded: a loop that builds three million short-lived strings and then 300 thousand arrays must finish in a
3+# few MB with memory-safety on, and the same loop with it off keeps everything (hundreds of MB), which shows
4+# the check is measuring the freeing and not just a small program.
5+set -uo pipefail
6+# Hard cap on every program this script starts (virtual memory, 3 GB): the off-mode run keeps everything
7+# on purpose, and a bug must not be able to take the machine down with it.
8+ulimit -v 3000000
9+cd "$(dirname "$0")/../.." # -> project root
10+export ASH_PATH="${ASH_PATH:-$PWD/../../../ash-stdlib/src/main/ash}"
11+make -s || { echo "build failed"; exit 1; }
12+TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
13+mkdir "$TMP/on" "$TMP/off"
14+printf 'name: m\nversion: 0.1.0\nmemory-safety: on\n' > "$TMP/on/ash.pkg"
15+printf 'name: m\nversion: 0.1.0\nmemory-safety: off\n' > "$TMP/off/ash.pkg"
16+for d in on off; do
17+cat > "$TMP/$d/Churn.ash" <<'JAVA'
18+public class Churn {
19+ public static void main(String[] args) {
20+ long total = 0;
21+ for (int i = 0; i < 3000000; i++) {
22+ String s = "item-" + i;
23+ String t = s + "!";
24+ t += s;
25+ total += s.length() + t.length();
26+ }
27+ for (int i = 0; i < 300000; i++) {
28+ int[] xs = new int[16];
29+ int[] ys = {i, i + 1, i + 2};
30+ xs[3] = i;
31+ total += xs[3] + xs.length + ys[2];
32+ }
33+ System.out.println(total);
34+ }
35+}
36+JAVA
37+done
38+peak() { python3 - "$@" <<'PY'
39+import resource, subprocess, sys
40+r = subprocess.run(sys.argv[1:], capture_output=True, text=True)
41+print(resource.getrusage(resource.RUSAGE_CHILDREN).ru_maxrss // 1024, r.stdout.strip().splitlines()[-1] if r.stdout.strip() else r.stderr[:200])
42+PY
43+}
44+on=$(peak ../../bin/ashvm run "$TMP/on/Churn.ash")
45+off=$(peak ../../bin/ashvm run "$TMP/off/Churn.ash")
46+on_mb=${on%% *}; off_mb=${off%% *}
47+echo "memory-safety on: ${on} off: ${off} (peak MB, last line of output)"
48+[ "${on#* }" == "${off#* }" ] || { echo "FAIL: outputs differ"; exit 1; }
49+[ "$on_mb" -le 64 ] || { echo "FAIL: on-mode peak ${on_mb} MB is not bounded"; exit 1; }
50+[ "$off_mb" -ge $((on_mb * 4)) ] || { echo "FAIL: off-mode peak ${off_mb} MB is not clearly larger"; exit 1; }
51+echo "memory check ok"

hearth/ashvm/tests/scripts/ownership_fuzz.shadded

+38−0
@@ -0,0 +1,38 @@
1+#!/usr/bin/env bash
2+# ashvm/tests/scripts/ownership_fuzz.sh [first_seed] [last_seed]
3+# Differential test of the ownership pass and the freeing it enables: for each seed a random program is
4+# generated (ownership_fuzz_gen.py), run with memory-safety on and off, and the outputs must be identical.
5+# Programs the move checker rejects in "on" mode are skipped (the generator may reuse a moved value on purpose).
6+# With ASAN=/path/to/ashvm-built-with-address-sanitizer every accepted program also runs under it, and with
7+# KILN=1 it is compiled with kiln too and its output must match as well. Programs are tiny; each run is capped.
8+set -uo pipefail
9+cd "$(dirname "$0")/../.." # -> ashvm/
10+export ASH_PATH="${ASH_PATH:-$PWD/../../../ash-stdlib/src/main/ash}"
11+make -s || { echo "build failed"; exit 1; }
12+VM="$PWD/../../bin/ashvm"; KILN_BIN="$PWD/../../bin/kiln"; GEN="$PWD/tests/scripts/ownership_fuzz_gen.py"
13+FIRST="${1:-1}"; LAST="${2:-200}"
14+TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
15+mkdir "$TMP/on" "$TMP/off"
16+printf 'name: f\nversion: 0.1.0\nmemory-safety: on\n' > "$TMP/on/ash.pkg"
17+printf 'name: f\nversion: 0.1.0\nmemory-safety: off\n' > "$TMP/off/ash.pkg"
18+ulimit -v 4000000
19+ok=0; skipped=0; bad=0
20+for seed in $(seq "$FIRST" "$LAST"); do
21+ python3 "$GEN" "$seed" > "$TMP/on/F$seed.ash"
22+ cp "$TMP/on/F$seed.ash" "$TMP/off/"
23+ on_out=$("$VM" run "$TMP/on/F$seed.ash" 2>&1)
24+ if echo "$on_out" | grep -q '^error'; then skipped=$((skipped + 1)); continue; fi
25+ off_out=$("$VM" run "$TMP/off/F$seed.ash" 2>&1)
26+ fail=""
27+ [ "$on_out" == "$off_out" ] || fail="on/off differ"
28+ if [ -n "${ASAN:-}" ] && ASAN_OPTIONS=detect_leaks=0 timeout 60 "$ASAN" run "$TMP/on/F$seed.ash" 2>&1 | grep -q AddressSanitizer; then fail="$fail asan"; fi
29+ if [ -n "${KILN:-}" ]; then
30+ if "$KILN_BIN" "$TMP/on/F$seed.ash" -o "$TMP/k$seed" > /dev/null 2>&1; then
31+ k_out=$(timeout 30 "$TMP/k$seed" 2>&1); rm -f "$TMP/k$seed"
32+ [ "$k_out" == "$on_out" ] || fail="$fail kiln differs"
33+ else fail="$fail kiln compile"; fi
34+ fi
35+ if [ -n "$fail" ]; then echo "FAIL seed $seed:$fail"; cp "$TMP/on/F$seed.ash" "/tmp/ownership_fuzz_fail_$seed.ash"; bad=$((bad + 1)); else ok=$((ok + 1)); fi
36+done
37+echo "ownership fuzz: ok=$ok skipped=$skipped bad=$bad"
38+[ "$bad" -eq 0 ]

hearth/ashvm/tests/scripts/ownership_fuzz_gen.pyadded

+290−0
@@ -0,0 +1,290 @@
1+#!/usr/bin/env python3
2+# Random Ash programs that mix ownership patterns (aliasing, moves, factories, containers, try/finally, loops).
3+# Used by ownership_fuzz.sh: the output of a program must be identical with memory-safety on and off.
4+import random, sys
5+
6+HELPERS = '''
7+ static String SK; // statics that keep values alive
8+ static int[] AK;
9+ static List<String> LK;
10+ static Box BK;
11+ static Map<String, Integer> MK;
12+ static int rdS(String s) { return s.length(); }
13+ static int rdA(int[] a) { int t = 0; for (int x : a) t += x; return t; }
14+ static int rdL(List<String> l) { int t = 0; for (int i = 0; i < l.size(); i++) t += l.get(i).length(); return t; }
15+ static int rdB(Box b) { return b.n.length() + b.d.length; }
16+ static int rdM(Map<String, Integer> m) { return m.size() + (m.containsKey("k") ? 1 : 0); }
17+ static void keepS(String s) { SK = s; }
18+ static void keepA(int[] a) { AK = a; }
19+ static void keepL(List<String> l) { LK = l; }
20+ static void keepB(Box b) { BK = b; }
21+ static void keepM(Map<String, Integer> m) { MK = m; }
22+ static String retS(String s) { return s; }
23+ static int[] retA(int[] a) { return a; }
24+ static List<String> retL(List<String> l) { return l; }
25+ static Box retB(Box b) { return b; }
26+ static String mkS(int i) { String s = "m" + i; return s; }
27+ static int[] mkA(int i) { int[] a = new int[3]; a[0] = i; return a; }
28+ static List<String> mkL(int i) { List<String> l = new ArrayList<>(); l.add("e" + i); return l; }
29+ static Box mkB(int i) { Box b = new Box("b" + i); return b; }
30+ static String pickS(bool c, String a) { if (c) return a; String t = "alt" + a; return t; }
31+ static int deep(String s, int d) { return d == 0 ? s.length() : deep(s, d - 1) + 1; }
32+'''
33+
34+CLASSES = '''
35+ static class Box {
36+ String n;
37+ int[] d = new int[2];
38+ List<String> items = new ArrayList<>();
39+ Box(String n) { d[0] = n.length(); items.add("i" + n); this.n = n; }
40+ int size() { return n.length() + d[0] + items.size(); }
41+ String name() { return n; }
42+ void rename(String x) { n = "r" + x; }
43+ List<String> list() { return items; }
44+ }
45+'''
46+
47+class Gen:
48+ def __init__(self, seed):
49+ self.r = random.Random(seed)
50+ self.n = 0
51+ self.lines = []
52+ self.out_i = 0
53+
54+ def fresh(self):
55+ self.n += 1
56+ return self.n
57+
58+ def emit(self, ind, s):
59+ self.lines.append(" " * ind + s)
60+
61+ def stmt_block(self, ind, vars, depth, budget):
62+ vars = dict(vars) # name -> kind, usable (not moved)
63+ for _ in range(budget):
64+ self.stmt(ind, vars, depth)
65+ return vars
66+
67+ def pick(self, vars, kinds=None):
68+ c = [v for v, k in vars.items() if kinds is None or k in kinds]
69+ return self.r.choice(c) if c else None
70+
71+ def use(self, ind, v, k):
72+ # a read of v that would show corruption
73+ e = {"S": f"{v}.length()", "A": f"rdA({v})", "L": f"rdL({v})", "B": f"rdB({v})", "M": f"rdM({v})"}[k]
74+ self.emit(ind, f"total += {e};")
75+
76+ def stmt(self, ind, vars, depth):
77+ r = self.r
78+ op = r.randrange(30)
79+ if op <= 3: # declare something fresh
80+ i = self.fresh()
81+ k = r.choice("SALBM")
82+ v = f"v{i}"
83+ if k == "S":
84+ self.emit(ind, f'String {v} = "p" + {i} + total;')
85+ elif k == "A":
86+ self.emit(ind, f"int[] {v} = new int[{r.randrange(1, 5)}];"); self.emit(ind, f"{v}[0] = {i};")
87+ elif k == "L":
88+ self.emit(ind, f"List<String> {v} = new ArrayList<>();"); self.emit(ind, f'{v}.add("x" + {i});')
89+ elif k == "B":
90+ self.emit(ind, f'Box {v} = new Box("n" + {i});')
91+ else:
92+ self.emit(ind, f"Map<String, Integer> {v} = new HashMap<>();"); self.emit(ind, f'{v}.put("k", {i});')
93+ vars[v] = k
94+ elif op == 4: # from a factory
95+ i = self.fresh(); k = r.choice("SALB"); v = f"v{i}"
96+ self.emit(ind, {"S": f"String {v} = mkS({i});", "A": f"int[] {v} = mkA({i});",
97+ "L": f"List<String> {v} = mkL({i});", "B": f"Box {v} = mkB({i});"}[k])
98+ vars[v] = k
99+ elif op <= 7: # read something
100+ v = self.pick(vars)
101+ if v: self.use(ind, v, vars[v])
102+ elif op == 8: # call a reader helper
103+ v = self.pick(vars)
104+ if v:
105+ k = vars[v]
106+ self.emit(ind, f"total += rd{k}({v});")
107+ elif op == 9: # keep it (moves in on-mode)
108+ v = self.pick(vars, "SALBM")
109+ if v:
110+ k = vars[v]
111+ self.emit(ind, f"keep{k}({v});")
112+ del vars[v]
113+ elif op == 10: # return-it helper then use result
114+ v = self.pick(vars, "SALB")
115+ if v:
116+ k = vars[v]; i = self.fresh(); w = f"v{i}"
117+ ty = {"S": "String", "A": "int[]", "L": "List<String>", "B": "Box"}[k]
118+ self.emit(ind, f"{ty} {w} = ret{k}({v});")
119+ del vars[v]; vars[w] = k
120+ elif op == 11: # alias
121+ v = self.pick(vars)
122+ if v:
123+ k = vars[v]; i = self.fresh(); w = f"v{i}"
124+ ty = {"S": "String", "A": "int[]", "L": "List<String>", "B": "Box", "M": "Map<String, Integer>"}[k]
125+ self.emit(ind, f"{ty} {w} = {v};")
126+ del vars[v]; vars[w] = k
127+ elif op == 12 and depth < 2: # loop
128+ i = self.fresh()
129+ self.emit(ind, f"for (int j{i} = 0; j{i} < {r.randrange(1, 4)}; j{i}++) {{")
130+ inner = self.stmt_block(ind + 1, vars, depth + 1, r.randrange(1, 5))
131+ self.emit(ind, "}")
132+ # variables declared inside are gone; outer ones may have been moved inside the loop: drop any moved
133+ for v in list(vars):
134+ if v not in inner: del vars[v]
135+ elif op == 13 and depth < 2: # if / else
136+ self.emit(ind, f"if (total % {r.randrange(2, 5)} == 0) {{")
137+ a = self.stmt_block(ind + 1, vars, depth + 1, r.randrange(1, 4))
138+ self.emit(ind, "} else {")
139+ b = self.stmt_block(ind + 1, vars, depth + 1, r.randrange(1, 4))
140+ self.emit(ind, "}")
141+ for v in list(vars):
142+ if v not in a or v not in b: del vars[v]
143+ elif op == 14: # string work
144+ v = self.pick(vars, "S")
145+ if v:
146+ i = self.fresh(); w = f"v{i}"
147+ self.emit(ind, f'String {w} = {v} + "!" + total;')
148+ vars[w] = "S"
149+ elif op == 16: # hand out a field of a Box
150+ v = self.pick(vars, "B")
151+ if v:
152+ c = r.randrange(4)
153+ if c == 0: self.emit(ind, f"keepL({v}.list());")
154+ elif c == 1: self.emit(ind, f"keepS({v}.name());")
155+ elif c == 2: self.emit(ind, f"keepA({v}.d);")
156+ else: self.emit(ind, f"keepL({v}.items);")
157+ elif op == 17: # put a value into a container
158+ a = self.pick(vars, "S"); l = self.pick(vars, "L")
159+ if a and l:
160+ self.emit(ind, f"{l}.add({a});"); del vars[a]
161+ elif op == 18: # store a string into a Box's list
162+ a = self.pick(vars, "S"); b = self.pick(vars, "B")
163+ if a and b:
164+ self.emit(ind, f"{b}.items.add({a});"); del vars[a]
165+ elif op == 19: # string array of fresh and shared strings
166+ i = self.fresh(); w = f"v{i}"
167+ a = self.pick(vars, "S")
168+ self.emit(ind, f"String[] {w} = new String[2];")
169+ self.emit(ind, f'{w}[0] = "q" + {i};')
170+ if a: self.emit(ind, f"{w}[1] = {a};"); del vars[a]
171+ else: self.emit(ind, f'{w}[1] = "z" + {i};')
172+ self.emit(ind, f"total += {w}[0].length() + {w}[1].length();")
173+ elif op == 20 and depth < 2: # loop with continue / break
174+ i = self.fresh()
175+ self.emit(ind, f"for (int j{i} = 0; j{i} < 4; j{i}++) {{")
176+ self.emit(ind + 1, f"String t{i} = \"t\" + j{i};")
177+ self.emit(ind + 1, f"if (j{i} == 1) continue;")
178+ self.emit(ind + 1, f"total += t{i}.length();")
179+ self.emit(ind + 1, f"if (j{i} == 2) break;")
180+ self.emit(ind, "}")
181+ elif op == 21 and depth < 2: # try / finally touching outer locals
182+ v = self.pick(vars, "SALB")
183+ if v:
184+ k = vars[v]
185+ e = {"S": f"{v}.length()", "A": f"rdA({v})", "L": f"rdL({v})", "B": f"rdB({v})"}[k]
186+ self.emit(ind, "try {")
187+ self.emit(ind + 1, f"total += {e};")
188+ self.emit(ind + 1, f"if (total % 3 == 0) throw new RuntimeException(\"x\");")
189+ self.emit(ind, "} catch (RuntimeException e) {")
190+ self.emit(ind + 1, f"total += {e} + 1;")
191+ self.emit(ind, "} finally {")
192+ self.emit(ind + 1, f"total += {e};")
193+ self.emit(ind, "}")
194+ elif op == 22: # reassign a string local
195+ v = self.pick(vars, "S")
196+ if v: self.emit(ind, f'{v} = {v} + "+" + total;')
197+ elif op == 23: # conditional helper returning one of two
198+ v = self.pick(vars, "S")
199+ if v:
200+ i = self.fresh(); w = f"v{i}"
201+ self.emit(ind, f"String {w} = pickS(total % 2 == 0, {v});")
202+ del vars[v]; vars[w] = "S"
203+ elif op == 24: # fresh element, then read it in harmless and in escaping ways
204+ l = self.pick(vars, "L")
205+ if l:
206+ i = self.fresh()
207+ self.emit(ind, f'{l}.add("e" + {i} + total);')
208+ c = r.randrange(5)
209+ if c == 0: self.emit(ind, f"total += {l}.get(0).length();")
210+ elif c == 1: self.emit(ind, f"keepS({l}.get(0));")
211+ elif c == 2: self.emit(ind, f"String w{i} = {l}.get({l}.size() - 1); total += w{i}.length();")
212+ elif c == 3: self.emit(ind, f"for (String x{i} : {l}) {{ total += x{i}.length(); }}")
213+ else: self.emit(ind, f"for (String x{i} : {l}) {{ if (total % 5 == 0) SK = x{i}; }}")
214+ elif op == 25: # String[] with fresh / shared stores and reads
215+ i = self.fresh(); w = f"v{i}"
216+ self.emit(ind, f"String[] {w} = new String[3];")
217+ self.emit(ind, f'{w}[0] = "a" + {i} + total;')
218+ self.emit(ind, f'{w}[1] = "b" + {i};')
219+ c = r.randrange(5)
220+ if c == 0: self.emit(ind, f'{w}[2] = "lit";')
221+ elif c == 1: self.emit(ind, f'{w}[2] = {w}[0];')
222+ elif c == 2: self.emit(ind, f'{w}[2] = "c" + {i};')
223+ elif c == 3: self.emit(ind, f"keepS({w}[1]);")
224+ else: self.emit(ind, f"total += {w}[0].length();")
225+ self.emit(ind, f"total += {w}[1].length();")
226+ elif op == 26: # a list of strings stored in a field and read later
227+ b = self.pick(vars, "B")
228+ if b:
229+ i = self.fresh()
230+ self.emit(ind, f'{b}.items.add("it" + {i} + total);')
231+ if r.randrange(2): self.emit(ind, f"total += {b}.items.get(0).length();")
232+ else: self.emit(ind, f"keepS({b}.items.get(0));")
233+ elif op == 27: # ternary / conditional use of an element
234+ l = self.pick(vars, "L")
235+ if l: self.emit(ind, f'total += ({l}.size() > 0 ? {l}.get(0) : "none").length();')
236+ elif op == 28: # remove / set return the old element
237+ l = self.pick(vars, "L")
238+ if l:
239+ i = self.fresh()
240+ self.emit(ind, f'{l}.add("r" + {i});')
241+ self.emit(ind, f"String o{i} = {l}.remove({l}.size() - 1); total += o{i}.length();")
242+ elif op == 29: # string array from a loop
243+ i = self.fresh(); w = f"v{i}"
244+ self.emit(ind, f"String[] {w} = new String[3];")
245+ self.emit(ind, f'for (int q{i} = 0; q{i} < 3; q{i}++) {{ {w}[q{i}] = "s" + q{i} + total; }}')
246+ self.emit(ind, f"for (String y{i} : {w}) {{ total += y{i}.length(); }}")
247+ elif op == 15: # box mutation / field reads
248+ v = self.pick(vars, "B")
249+ if v:
250+ c = r.randrange(3)
251+ if c == 0: self.emit(ind, f'{v}.rename("{r.randrange(100)}");')
252+ elif c == 1: self.emit(ind, f"total += {v}.size();")
253+ else:
254+ i = self.fresh(); w = f"v{i}"
255+ self.emit(ind, f"String {w} = {v}.name();"); self.emit(ind, f"total += {w}.length();")
256+ # w aliases a field: only read it right here
257+
258+ def program(self, name):
259+ self.emit(1, "public static void main(String[] args) {")
260+ self.emit(2, "long total = 0;")
261+ vars = {}
262+ self.stmt_block(2, vars, 0, self.r.randrange(6, 18))
263+ # observe everything that survived
264+ for v, k in vars.items(): self.use(2, v, k)
265+ self.emit(2, 'String? sk = SK; if (sk != null) total += sk.length();')
266+ self.emit(2, 'int[]? ak = AK; if (ak != null) total += rdA(ak);')
267+ self.emit(2, 'List<String>? lk = LK; if (lk != null) total += rdL(lk);')
268+ self.emit(2, 'Box? bk = BK; if (bk != null) total += rdB(bk);')
269+ self.emit(2, 'Map<String, Integer>? mk = MK; if (mk != null) total += rdM(mk);')
270+ self.emit(2, "System.out.println(total);")
271+ self.emit(1, "}")
272+ body = "\n".join(self.lines)
273+ helpers = HELPERS.replace("static String SK;", "static String? SK;").replace("static int[] AK;", "static int[]? AK;") \
274+ .replace("static List<String> LK;", "static List<String>? LK;").replace("static Box BK;", "static Box? BK;") \
275+ .replace("static Map<String, Integer> MK;", "static Map<String, Integer>? MK;")
276+ return f"""import ash.util.ArrayList;
277+import ash.util.HashMap;
278+import ash.util.List;
279+import ash.util.Map;
280+
281+public class {name} {{
282+{CLASSES}
283+{helpers}
284+{body}
285+}}
286+"""
287+
288+if __name__ == "__main__":
289+ seed = int(sys.argv[1]); name = f"F{seed}"
290+ sys.stdout.write(Gen(seed).program(name))

hearth/front/Makefilemodified

+5−4
@@ -1,9 +1,9 @@
11 CC = gcc
22 .DEFAULT_GOAL := all
3-CFLAGS = -Wall -Wextra -std=c11 -O2 -Isrc
3+CFLAGS = -Wall -Wextra -std=c11 -O2 -Isrc -I..
44
55 # Shared front end for Ash: lexer, parser, AST, type checker, lowering and the internal IR
6-# lexer, built as libashfront.a and linked by both ashvm and kiln.
6+# lexer, built as front.a and linked by both ashvm and kiln.
77 # There is no front-end executable: ashvm and kiln are the user-facing tools.
88 # Same layout rule as the other subprojects: src/<sub>/C/x.c -> src/<sub>/O/x.o.
99 FRONT_SRC = \
@@ -22,6 +22,7 @@ FRONT_SRC = \
2222 src/jtypes/C/check_expr.c \
2323 src/jtypes/C/check_stmt.c \
2424 src/jtypes/C/check_class.c \
25+ src/jown/C/own.c \
2526 src/jlower/C/lower_expr.c \
2627 src/jlower/C/lower_stmt.c \
2728 src/jlower/C/lower_class.c \
@@ -43,12 +44,12 @@ $(call obj_of,$(1)): $(1)
4344 endef
4445 $(foreach s,$(FRONT_SRC),$(eval $(call compile_rule,$(s))))
4546
46-LIB = ../../bin/libashfront.a
47+LIB = ../../bin/lib/front.a
4748
4849 all: $(LIB)
4950
5051 $(LIB): $(FRONT_OBJ)
51- mkdir -p ../../bin
52+ mkdir -p ../../bin/lib
5253 ar rcs $@ $(FRONT_OBJ)
5354
5455 -include $(ALL_DEP)

hearth/front/src/jcore/C/core_lang.cmodified

+3−0
@@ -533,6 +533,9 @@ const JCoreEntry jcore_lang[] = {
533533 " public static final InputStream in = new InputStream();\n"
534534 " /// Ends the program with the given status.\n"
535535 " @Lower(\"exit($1)\") public static native void exit(int status);\n"
536+ " /// Frees a String, array or object. Only for projects with `memory-safety: off`; the value must be\n"
537+ " /// unreachable afterwards, or the program is on its own.\n"
538+ " @Lower(\"mem_release($1)\") public static native void free(Object o);\n"
536539 "}\n" },
537540 { "ash/lang/Throwable",
538541 "package ash.lang;\n"

hearth/front/src/jdriver/C/jdriver.cmodified

+2−0
@@ -3,6 +3,7 @@
33 #include "jcommon/H/jcommon.h"
44 #include "jdriver/H/jdriver.h"
55 #include "jlower/H/jlower.h"
6+#include "jown/H/own.h"
67 #include "jparse/H/jparse.h"
78 #include "jtypes/H/jtypes.h"
89
@@ -11,6 +12,7 @@ static JClass *load_and_check(const char *path, const char *src) {
1112 jt_init_default_roots(path, head->name);
1213 JClass *entry = jt_load_unit(path, src);
1314 jt_check_all();
15+ if (!j_error_count()) jown_check_project(path);
1416 if (j_error_count()) {
1517 fprintf(stderr, "%d error(s)\n", j_error_count());
1618 return NULL;

hearth/front/src/jlower/C/lower_expr.cmodified

+70−2
@@ -4,11 +4,49 @@
44 #include <string.h>
55
66 #include "jlower/H/jlower_internal.h"
7+#include "jown/H/own.h"
78
89 /* Expression lowering: new-Ash typed AST -> old-Ash expression text. */
910
1011 LState L;
1112 int jl_wrap_int = 0;
13+int jl_drops = 0;
14+
15+static int is_map_type(JType *t) {
16+ if (!t || t->kind != TY_CLASS) return 0;
17+ const char *q = t->cls->qname;
18+ return !strcmp(q, "ash.util.HashMap") || !strcmp(q, "ash.util.Map") || !strcmp(q, "ash.util.HashSet") || !strcmp(q, "ash.util.Set");
19+}
20+
21+const char *jl_drop_fn(JType *t) {
22+ if (is_map_type(t)) return "mem_free_map";
23+ if (t && (t->kind == TY_ARRAY || is_array_list(t))) return "mem_free_arr";
24+ if (t && t->kind == TY_CLASS && !ty_is_string(t)) return "mem_free_obj";
25+ return "mem_free";
26+}
27+
28+void jl_emit_drop(JBuf *b, int ind, JType *t, const char *var, int elems) {
29+ if (t && t->kind == TY_CLASS && !ty_is_string(t) && t->cls && !is_array_list(t) && !is_map_type(t)) {
30+ /* the object exclusively owns these fields' values, so they die with it */
31+ JField **fs;
32+ int nf = jown_owned_fields(t->cls, &fs);
33+ for (int i = 0; i < nf; i++) {
34+ if (!jl_drop_ok(fs[i]->type)) continue;
35+ if (jown_field_elems(fs[i]) && (jl_drops & JL_DROP_ELEMS)) emit_line(b, ind, "mem_free_elems(%s[%s]);", var, quote_str(fs[i]->name));
36+ emit_line(b, ind, "%s(%s[%s]);", jl_drop_fn(fs[i]->type), var, quote_str(fs[i]->name));
37+ }
38+ }
39+ if (elems && (jl_drops & JL_DROP_ELEMS)) emit_line(b, ind, "mem_free_elems(%s);", var); /* the Strings only this container holds */
40+ emit_line(b, ind, "%s(%s);", jl_drop_fn(t), var);
41+}
42+
43+int jl_drop_ok(JType *t) {
44+ if (!jl_drops || !L.cls || !L.cls->mem_on || !t) return 0;
45+ if (is_map_type(t)) return (jl_drops & JL_DROP_MAPS) != 0;
46+ if (t->kind == TY_ARRAY || (t->kind == TY_CLASS && is_array_list(t))) return (jl_drops & JL_DROP_ARRAYS) != 0;
47+ if (ty_is_string(t)) return (jl_drops & JL_DROP_STRINGS) != 0;
48+ return t->kind == TY_CLASS && (jl_drops & JL_DROP_OBJECTS) != 0;
49+}
1250
1351 void lerr(Node *at, const char *fmt, ...) {
1452 va_list ap;
@@ -28,7 +66,7 @@ static const char *reserved[] = {
2866 "keys", "values", "has", "map", "filter", "reduce", "split", "join", "substring", "indexOf", "contains",
2967 "starts_with", "ends_with", "replace", "repeat", "upper", "lower", "trim", "input", "read_file",
3068 "write_file", "append_file", "file_exists", "rename_file", "delete_file", "make_dir", "list_dir",
31- "sys_fork", "sys_wait", "sys_exec", NULL
69+ "sys_fork", "sys_wait", "sys_exec", "mem_free", "mem_free_arr", "mem_free_obj", "mem_free_map", "mem_free_elems", "mem_newarr1", "mem_cat", "mem_release", NULL
3270 };
3371
3472 char *sname(const char *n) {
@@ -412,6 +450,15 @@ static char *binop(const char *op, JType *lt, JType *rt, const char *a, const ch
412450 if (!strcmp(op, ">>>")) return j_fmt("__wg(__ushr(%s, %s & 31))", a, b);
413451 }
414452 if (!strcmp(op, "+") && (ty_is_string(lt) || ty_is_string(rt))) {
453+ if ((jl_drops & JL_DROP_STRINGS) && L.cls && L.cls->mem_on && at) {
454+ /* operands that are temporaries (an inner concatenation, str(int)) die with this one: free them */
455+ Node *ln = at->kind == N_BINARY ? at->a : NULL, *rn = at->kind == N_BINARY || at->kind == N_ASSIGN ? at->b : NULL;
456+ int fl = ln && jown_is_fresh_string(ln) ? 1 : 0;
457+ JType *ur = ty_unbox(rt);
458+ if (rn && ((jown_is_fresh_string(rn) && ty_is_string(rt)) ||
459+ (!ty_is_string(rt) && !rt->nullable && (ur->kind == TY_INT || ur->kind == TY_SHORT || ur->kind == TY_BYTE)))) fl |= 2;
460+ if (fl && ty_is_string(lt) && !lt->nullable) return j_fmt("mem_cat(%s, %s, %d)", a, str_of(rt, b), fl);
461+ }
415462 return j_fmt("(%s + %s)", str_of(lt, a), str_of(rt, b));
416463 }
417464 if (!strcmp(op, "/") && integral_t(lt) && integral_t(rt)) return j_fmt("__idiv(%s, %s)", a, b);
@@ -528,6 +575,23 @@ void emit_assign_stmt(JBuf *b, Node *e, int ind) {
528575 rhs = binop(op, e->a->type, e->b->type, lhs, rhs, e);
529576 }
530577 flush_pre(b, ind);
578+ if (e->a->kind == N_NAME && e->a->rk == R_LOCAL && e->a->sym && ((JLocal *)e->a->sym)->drop && jl_drop_ok(((JLocal *)e->a->sym)->type)) {
579+ /* sole owner: the old value is unreachable once the new one is computed */
580+ char *t = new_temp();
581+ emit_line(b, ind, "local %s = %s;", t, unparen(rhs));
582+ jl_emit_drop(b, ind, ((JLocal *)e->a->sym)->type, lhs, jown_local_elems(e->a->sym));
583+ emit_line(b, ind, "%s = %s;", lhs, t);
584+ return;
585+ }
586+ if (jown_field_assign_frees(e->a) && jl_drop_ok(((JField *)e->a->sym)->type)) {
587+ /* the object owns the field's value exclusively: the old one dies when the new one is stored */
588+ char *t = new_temp();
589+ emit_line(b, ind, "local %s = %s;", t, unparen(rhs));
590+ if (jown_field_elems(e->a->sym) && (jl_drops & JL_DROP_ELEMS)) emit_line(b, ind, "mem_free_elems(%s);", lhs);
591+ emit_line(b, ind, "%s(%s);", jl_drop_fn(((JField *)e->a->sym)->type), lhs);
592+ emit_line(b, ind, "%s = %s;", lhs, t);
593+ return;
594+ }
531595 emit_line(b, ind, "%s = %s;", lhs, unparen(rhs));
532596 return;
533597 }
@@ -722,6 +786,10 @@ static char *lower_array(Node *n) {
722786 JType *t = n->type;
723787 for (int i = 0; i < n->l1.count; i++) t = t->elem;
724788 char *dims = NULL;
789+ if (n->l1.count == 1 && (jl_drops & JL_DROP_ARRAYS) && L.cls && L.cls->mem_on) {
790+ /* no temporary dims array to leak: the array itself is what a sole owner frees */
791+ return j_fmt("%s(%s, %s)", (jl_drops & JL_NATIVE_NEWARR) ? "mem_newarr1" : "__newarr1", lx(n->l1.items[0]), default_value(t));
792+ }
725793 JBuf b = {0};
726794 j_buf_addc(&b, '[');
727795 for (int i = 0; i < n->l1.count; i++) {
@@ -866,7 +934,7 @@ char *lx(Node *e) {
866934 case N_UNARY:
867935 if (!strcmp(e->name, "!")) return j_fmt("(not %s)", lx(e->a));
868936 if (!strcmp(e->name, "++") || !strcmp(e->name, "--")) return assign_value(e);
869- if (!strcmp(e->name, "+")) return lx(e->a);
937+ if (!strcmp(e->name, "+") || e->name[0] == '&') return lx(e->a); /* a borrow is the variable itself at run time */
870938 if (jl_wrap_int && !strcmp(e->name, "-") && ty_unbox(e->a->type)->kind == TY_INT) return j_fmt("__w(-%s)", lx(e->a));
871939 if (jl_wrap_int && !strcmp(e->name, "-") && is_long_t(e->a->type)) return j_fmt("__lneg(%s)", lx(e->a));
872940 if (jl_wrap_int && !strcmp(e->name, "~") && is_long_t(e->a->type)) return j_fmt("__lxor(%s, -1)", lx(e->a));

hearth/front/src/jlower/C/lower_prelude.cmodified

+11−5
@@ -13,7 +13,7 @@ static char *ex_new(const char *qname) {
1313 static void raw(JBuf *b, const char *s) { j_buf_add(b, s); j_buf_addc(b, '\n'); }
1414
1515 void emit_prelude(JBuf *b) {
16- raw(b, "local __KP = [chr(1), 0];");
16+ raw(b, "local __KP = [chr(1), 0, chr(1) + \"n\"];"); // [key prefix, object id counter, the element-count key]
1717 raw(b,
1818 "forge __bs(x) { given x { yield \"true\"; } yield \"false\"; }\n"
1919 "forge __p10(n) { local r = 1; local i = 0; during i < n { r = r * 10; i += 1; } yield r; }\n"
@@ -402,6 +402,12 @@ void emit_prelude(JBuf *b) {
402402 " yield r;\n"
403403 "}\n"
404404 "forge __newarr(dims, d) { yield __na(dims, 0, d); }\n"
405+ "forge __newarr1(n, d) {\n"
406+ " local r = [];\n"
407+ " local j = 0;\n"
408+ " during j < n { push(r, d); j += 1; }\n"
409+ " yield r;\n"
410+ "}\n"
405411 "forge __lset(l, i, v) { local o = l[i]; l[i] = v; yield o; }\n"
406412 "forge __lremove(l, i) { local o = l[i]; delete(l, i); yield o; }\n"
407413 "forge __lcontains(l, x) { each v in l { given __eq(v, x) { yield yes; } } yield no; }\n"
@@ -433,13 +439,13 @@ void emit_prelude(JBuf *b) {
433439 " yield none;\n"
434440 "}\n"
435441 "forge __mcnt(m, d) {\n"
436- " local c = __KP[0] + \"n\";\n"
442+ " local c = __KP[2];\n"
437443 " local n = 0;\n"
438444 " given has(m, c) { n = m[c]; }\n"
439445 " m[c] = n + d;\n"
440446 "}\n"
441447 "forge __msize(m) {\n"
442- " local c = __KP[0] + \"n\";\n"
448+ " local c = __KP[2];\n"
443449 " given has(m, c) { yield m[c]; }\n"
444450 " yield 0;\n"
445451 "}\n"
@@ -491,7 +497,7 @@ void emit_prelude(JBuf *b) {
491497 "}\n"
492498 "forge __mkeys(m) {\n"
493499 " local r = [];\n"
494- " local c = __KP[0] + \"n\";\n"
500+ " local c = __KP[2];\n"
495501 " each sk in keys(m) {\n"
496502 " given sk != c {\n"
497503 " local e = m[sk];\n"
@@ -502,7 +508,7 @@ void emit_prelude(JBuf *b) {
502508 "}\n"
503509 "forge __mvals(m) {\n"
504510 " local r = [];\n"
505- " local c = __KP[0] + \"n\";\n"
511+ " local c = __KP[2];\n"
506512 " each sk in keys(m) {\n"
507513 " given sk != c {\n"
508514 " local e = m[sk];\n"

hearth/front/src/jlower/C/lower_stmt.cmodified

+44−2
@@ -3,6 +3,7 @@
33 #include <string.h>
44
55 #include "jlower/H/jlower_internal.h"
6+#include "jown/H/own.h"
67
78 /* Statement lowering: control flow, switch (as if-chains), try/catch/finally via
89 * attempt/handle, lambdas, and the immediately-invoked closure used by switch expressions. */
@@ -38,9 +39,32 @@ void flush_pre(JBuf *b, int ind) {
3839 if (L.pre.data) L.pre.data[0] = 0;
3940 }
4041
42+/* frees the live sole-owner locals from index `from` up (they stay live: the caller decides about popping) */
43+static void emit_drops_except(JBuf *b, int from, int ind, void *keep) {
44+ if (!jl_drops || !L.cls || !L.cls->mem_on) return;
45+ for (int i = L.drop_n - 1; i >= from && i >= L.drop_base; i--) {
46+ if (L.drops[i] != keep) jl_emit_drop(b, ind, L.drops[i]->type, sname(L.drops[i]->name), jown_local_elems(L.drops[i]));
47+ }
48+}
49+
50+static void emit_drops(JBuf *b, int from, int ind) { emit_drops_except(b, from, ind, NULL); }
51+
52+static int ends_path(Node *s) {
53+ return s->kind == N_RETURN || s->kind == N_THROW || s->kind == N_BREAK || s->kind == N_CONTINUE;
54+}
55+
4156 void lblock_body(JBuf *b, Node *block, int ind) {
4257 if (block->kind != N_BLOCK) { ls(b, block, ind); return; }
43- for (int i = 0; i < block->l1.count; i++) ls(b, block->l1.items[i], ind);
58+ int mark = L.drop_n;
59+ for (int i = 0; i < block->l1.count; i++) {
60+ Node *s = block->l1.items[i];
61+ ls(b, s, ind);
62+ /* registered only here, so every entry is popped by the block that declared it */
63+ if (s->kind == N_LOCAL && s->sym && ((JLocal *)s->sym)->drop && jl_drop_ok(((JLocal *)s->sym)->type) && L.drop_n < 64) L.drops[L.drop_n++] = s->sym;
64+ }
65+ int n = block->l1.count;
66+ if (!n || !ends_path(block->l1.items[n - 1])) emit_drops(b, mark, ind);
67+ L.drop_n = mark;
4468 }
4569
4670 static void lbody(JBuf *b, Node *s, int ind) { lblock_body(b, s, ind); }
@@ -224,6 +248,7 @@ static void lower_switch_general(JBuf *b, Node *n, char *sel, int ind) {
224248 for (int d = depth - 1; d >= 0; d--) emit_line(b, ind + d, "}");
225249 emit_line(b, ind, "local %s = no;", go);
226250 LoopCtx ctx = {0};
251+ ctx.drop_mark = L.drop_n;
227252 ctx.is_switch = 1;
228253 ctx.wrapper = 1;
229254 ctx.cont_flag = new_temp();
@@ -260,6 +285,7 @@ static void lower_switch_stmt(JBuf *b, Node *n, int ind) {
260285 return;
261286 }
262287 LoopCtx ctx = {0};
288+ ctx.drop_mark = L.drop_n;
263289 ctx.is_switch = 1;
264290 ctx.up = L.loop;
265291 L.loop = &ctx;
@@ -290,6 +316,8 @@ char *lower_lambda_body(Node *lam, JMethod *sam, int ind) {
290316 memset(&L.pre, 0, sizeof L.pre);
291317 LoopCtx *saved_loop = L.loop;
292318 int saved_tf = L.fin_n;
319+ int saved_base = L.drop_base;
320+ L.drop_base = L.drop_n;
293321 L.loop = NULL;
294322 L.fin_n = 0;
295323 JBuf b = {0};
@@ -310,6 +338,7 @@ char *lower_lambda_body(Node *lam, JMethod *sam, int ind) {
310338 }
311339 L.loop = saved_loop;
312340 L.fin_n = saved_tf;
341+ L.drop_base = saved_base;
313342 L.pre = saved_pre;
314343 return b.data;
315344 }
@@ -380,6 +409,7 @@ static void lower_foreach(JBuf *b, Node *n, int ind) {
380409 char *it = lx(n->b);
381410 char *name = sname(n->name);
382411 LoopCtx ctx = {0};
412+ ctx.drop_mark = L.drop_n;
383413 ctx.up = L.loop;
384414 flush_pre(b, ind);
385415 if (native_iterable(n->b->type)) {
@@ -435,6 +465,7 @@ static void lower_for(JBuf *b, Node *n, int ind) {
435465 char *cond = n->a ? loop_cond(n->a, &pre) : "yes";
436466 flush_pre(b, ind + 1);
437467 LoopCtx ctx = {0};
468+ ctx.drop_mark = L.drop_n;
438469 ctx.up = L.loop;
439470 ctx.update = update_text(&n->l2);
440471 if (pre) {
@@ -497,6 +528,8 @@ void ls(JBuf *b, Node *s, int ind) {
497528 char *pre;
498529 char *c = loop_cond(s->a, &pre);
499530 LoopCtx ctx = {0};
531+ ctx.drop_mark = L.drop_n;
532+ ctx.drop_mark = L.drop_n;
500533 ctx.up = L.loop;
501534 if (pre) {
502535 emit_line(b, ind, "during yes {");
@@ -518,6 +551,8 @@ void ls(JBuf *b, Node *s, int ind) {
518551 char *pre;
519552 char *c = loop_cond(s->a, &pre);
520553 LoopCtx ctx = {0};
554+ ctx.drop_mark = L.drop_n;
555+ ctx.drop_mark = L.drop_n;
521556 ctx.up = L.loop;
522557 emit_line(b, ind, "local %s = yes;", first);
523558 if (pre) {
@@ -543,7 +578,10 @@ void ls(JBuf *b, Node *s, int ind) {
543578 case N_RETURN: {
544579 char *v = s->a ? lx(s->a) : "none";
545580 flush_pre(b, ind);
546- if (L.fin_n) {
581+ int drops = jl_drops && L.cls && L.cls->mem_on && L.drop_n > L.drop_base;
582+ if (drops || L.fin_n) {
583+ /* the value may read a local that is about to be freed, and finally bodies run before the
584+ * function leaves: compute the value first, then finally, then free */
547585 char *rv = new_temp();
548586 emit_line(b, ind, "local %s = %s;", rv, v);
549587 int saved = L.fin_n;
@@ -552,6 +590,8 @@ void ls(JBuf *b, Node *s, int ind) {
552590 lbody(b, L.fin[i], ind);
553591 }
554592 L.fin_n = saved;
593+ /* `return x` hands x to the caller */
594+ if (drops) emit_drops_except(b, 0, ind, s->a && s->a->kind == N_NAME && s->a->rk == R_LOCAL ? s->a->sym : NULL);
555595 v = rv;
556596 }
557597 emit_line(b, ind, "yield %s;", v);
@@ -562,6 +602,7 @@ void ls(JBuf *b, Node *s, int ind) {
562602 lerr(s, "break inside a switch must be the last statement of its case");
563603 break;
564604 }
605+ if (L.loop && !L.fin_n) emit_drops(b, L.loop->drop_mark, ind); /* not under a finally: it may still read them */
565606 emit_line(b, ind, "stop;");
566607 break;
567608 }
@@ -574,6 +615,7 @@ void ls(JBuf *b, Node *s, int ind) {
574615 break;
575616 }
576617 LoopCtx *l = real_loop();
618+ if (l && !L.fin_n) emit_drops(b, l->drop_mark, ind);
577619 if (l && l->update) each_line(b, ind, l->update), (void)0;
578620 emit_line(b, ind, "next;");
579621 break;

hearth/front/src/jlower/H/jlower.hmodified

+11−0
@@ -15,6 +15,17 @@
1515 * it roughly halves the speed of integer-heavy loops, and the old VM's numbers are doubles. */
1616 extern int jl_wrap_int;
1717
18+/* Bit mask set by engines that can free memory: lowering then emits `mem_free(x)` for the locals the
19+ * ownership pass (jown, `memory-safety: on`) proved to be sole owners, of the kinds enabled here.
20+ * ashvm frees all three; kiln frees Strings only (its allocator can only take those back). */
21+#define JL_DROP_STRINGS 1
22+#define JL_DROP_ARRAYS 2
23+#define JL_DROP_OBJECTS 4
24+#define JL_DROP_MAPS 16 /* HashMap / HashSet locals and fields (ashvm only: its map entries are chains it knows how to free) */
25+#define JL_DROP_ELEMS 32 /* free the fresh Strings inside a String[] / List<String> that only it references (ashvm only) */
26+#define JL_NATIVE_NEWARR 8 /* kiln builds `new T[n]` with the mem_newarr1 builtin (exact capacity) instead of the push loop */
27+extern int jl_drops;
28+
1829 /* Lowers every loaded class; `entry` supplies the class holding main. Returns the module text. */
1930 char *jlower_program(JClass *entry);
2031

hearth/front/src/jlower/H/jlower_internal.hmodified

+8−0
@@ -2,6 +2,7 @@
22 #define JLOWER_INTERNAL_H
33
44 #include "jlower/H/jlower.h"
5+#include "jtypes/H/check.h"
56
67 /* State of the statement currently being lowered. */
78 typedef struct LoopCtx {
@@ -10,6 +11,7 @@ typedef struct LoopCtx {
1011 int wrapper; /* a switch lowered as `during yes {...; stop;}`: break is `stop`, continue leaves via cont_flag */
1112 char *cont_flag; /* set before stop when `continue` crosses the wrapper */
1213 int cont_used;
14+ int drop_mark; /* L.drop_n when the loop began: break/continue free the droppables above it */
1315 struct LoopCtx *up;
1416 } LoopCtx;
1517
@@ -22,8 +24,14 @@ typedef struct {
2224 LoopCtx *loop;
2325 Node *fin[16]; /* enclosing finally bodies, innermost last (inlined before each return) */
2426 int fin_n;
27+ JLocal *drops[64]; /* live sole-owner locals (see JLocal.drop), innermost last */
28+ int drop_n;
29+ int drop_base; /* entries below belong to an enclosing function; a lambda's return must not free them */
2530 } LState;
2631 extern LState L;
32+int jl_drop_ok(JType *t);
33+void jl_emit_drop(JBuf *b, int ind, JType *t, const char *var, int elems); /* frees `var`, an owned local of type t, the fields its object owns, and its elements when `elems` */
34+const char *jl_drop_fn(JType *t); /* builtin that frees a local of this type: mem_free_arr arrays, mem_free_obj objects, mem_free Strings */ /* the engine can free a sole-owner local of this type */
2735 extern int jl_wrap_int; /* --wrap-int: Java 32-bit int overflow (costs about 2x on arithmetic-heavy loops) */
2836
2937 /* lower_expr.c */

hearth/front/src/jown/C/own.cadded

+923−0
Show diff (923 changed lines)
@@ -0,0 +1,923 @@
1+#include <stdint.h>
2+#include <stdio.h>
3+#include <string.h>
4+
5+#include "jcommon/H/jcommon.h"
6+#include "jown/H/own.h"
7+#include "mnemo/H/flow/flow.h"
8+#include "jtypes/H/check.h"
9+#include "jtypes/H/jtypes.h"
10+
11+/* ---------------------------------------------------------------- project flag */
12+
13+/* Finds the nearest ash.pkg at or above the entry file. Returns its directory (arena string) or NULL. */
14+static char *find_project_root(const char *entry_path) {
15+ char *dir = j_strdup(entry_path);
16+ char *slash = strrchr(dir, '/');
17+ if (slash) *slash = 0; else strcpy(dir, ".");
18+ char *entry_dir = j_strdup(dir);
19+ for (;;) {
20+ char *pkg = j_fmt("%s/ash.pkg", dir);
21+ FILE *f = fopen(pkg, "r");
22+ if (f) { fclose(f); return dir; }
23+ slash = strrchr(dir, '/');
24+ if (!slash || slash == dir) break;
25+ *slash = 0;
26+ }
27+ return entry_dir; /* no ash.pkg: the entry file's directory is the project, with the default setting */
28+}
29+
30+static int memory_safety_on(const char *root) {
31+ char *text = j_read_file(j_fmt("%s/ash.pkg", root));
32+ if (!text) return JOWN_DEFAULT_ON;
33+ for (char *line = strtok(text, "\n"); line; line = strtok(NULL, "\n")) {
34+ if (line[0] == '[') break; /* the flat header ends at the first section */
35+ if (strncmp(line, "memory-safety:", 14) != 0) continue;
36+ char *v = line + 14;
37+ while (*v == ' ' || *v == '\t') v++;
38+ return strncmp(v, "off", 3) != 0;
39+ }
40+ return JOWN_DEFAULT_ON;
41+}
42+
43+/* ---------------------------------------------------------------- flow state (kept in mnemo.a) */
44+
45+typedef struct Target {
46+ struct Target *up;
47+ int is_switch; /* `break` ends a switch; `continue` belongs to the nearest loop */
48+ uint32_t brk, cont; /* mnemo snapshots accumulating the states that leave through break / continue */
49+} Target;
50+
51+typedef struct {
52+ MnemoWalk *m;
53+ Target *targets;
54+ JList reported; /* Node* already diagnosed, so the second loop pass stays quiet */
55+} Walk;
56+
57+static uint64_t id_of(JLocal *l) { return (uint64_t)(uintptr_t)l; }
58+
59+static int owning(JType *t) {
60+ if (!t) return 0;
61+ t = ty_unbox(t);
62+ if (t->kind == TY_ARRAY || t->kind == TY_TVAR) return 1;
63+ if (t->kind != TY_CLASS) return 0;
64+ return t->cls && t->cls->kind != N_ENUM;
65+}
66+
67+/* ---------------------------------------------------------------- expressions */
68+
69+static int param_borrowed(JMethod *m, int i);
70+static int fixed_project_target(JMethod *m);
71+static void expr(Walk *w, Node *n);
72+static void stmt(Walk *w, Node *n);
73+
74+static void use(Walk *w, Node *n) {
75+ JLocal *l = n->sym;
76+ if (!l || !mnemo_is_moved(w->m, id_of(l))) return;
77+ for (int i = 0; i < w->reported.count; i++) if (w->reported.items[i] == n) return;
78+ j_list_push(&w->reported, n);
79+ cerr(n, "use of moved value '%s': it was moved earlier by an assignment or by passing it to a call; lend it with &%s, clone it, or assign a new value first", n->name, n->name);
80+}
81+
82+/* evaluates `e` as a value that is being stored: a bare owning local is moved out of */
83+static void store(Walk *w, Node *e) {
84+ expr(w, e);
85+ if (e->kind == N_NAME && e->rk == R_LOCAL && e->sym && owning(((JLocal *)e->sym)->type)) {
86+ mnemo_move(w->m, id_of(e->sym));
87+ }
88+}
89+
90+/* ---- calls: arguments are moved into project code unless written `&x` / `&mut x` ---- */
91+
92+typedef struct { JLocal *l; int mut; Node *at; } Borrow; /* a local named by a call's receiver or arguments */
93+
94+static int is_borrow(Node *a) {
95+ return a->kind == N_UNARY && a->name[0] == '&' && a->a->kind == N_NAME && a->a->rk == R_LOCAL && a->a->sym;
96+}
97+
98+/* project code owns what it is given; library and native code only looks at it */
99+static int callee_takes(JMethod *m) {
100+ /* equals / compareTo only compare: they never consume their argument */
101+ if (m && (!strcmp(m->name, "equals") || !strcmp(m->name, "compareTo"))) return 0;
102+ return m && !m->tmpl && m->cls && m->cls->mem_on;
103+}
104+
105+static void call_args(Walk *w, Node *n, JMethod *m, Node *recv, JList *args) {
106+ Borrow seen[32];
107+ int ns = 0;
108+ if (m && m->tmpl && !strncmp(m->tmpl, "mem_release(", 12)) {
109+ cerr(n, "System.free is only available with 'memory-safety: off' in ash.pkg: with it on, memory is freed for you");
110+ }
111+ if (recv) {
112+ expr(w, recv);
113+ if (recv->kind == N_NAME && recv->rk == R_LOCAL && recv->sym) seen[ns++] = (Borrow){recv->sym, 0, recv};
114+ }
115+ int takes = callee_takes(m);
116+ for (int i = 0; i < args->count; i++) {
117+ Node *a = args->items[i];
118+ if (is_borrow(a)) {
119+ expr(w, a->a);
120+ /* a borrow may not outlive the call: a callee we can see that keeps its parameter cannot be lent to */
121+ if (fixed_project_target(m) && !param_borrowed(m, i) && !(m->decl && m->decl->b == NULL)) {
122+ int dup = 0;
123+ for (int k = 0; k < w->reported.count; k++) if (w->reported.items[k] == a) dup = 1;
124+ if (!dup) {
125+ j_list_push(&w->reported, a);
126+ cerr(a, "cannot lend '%s' with '%s': '%s' keeps this parameter (stores, returns or passes it on); pass '%s' by move or clone it",
127+ a->a->name, a->name, m->name, a->a->name);
128+ }
129+ }
130+ if (ns < 32) seen[ns++] = (Borrow){a->a->sym, a->name[1] == 'm', a};
131+ } else if (takes && !param_borrowed(m, i)) {
132+ store(w, a);
133+ if (a->kind == N_NAME && a->rk == R_LOCAL && a->sym && ns < 32) seen[ns++] = (Borrow){a->sym, 0, a};
134+ } else {
135+ expr(w, a);
136+ if (a->kind == N_NAME && a->rk == R_LOCAL && a->sym && ns < 32) seen[ns++] = (Borrow){a->sym, 0, a};
137+ }
138+ }
139+ /* Rust's rule: an exclusive borrow excludes every other use of the same variable in the call */
140+ for (int i = 0; i < ns; i++) {
141+ for (int j = i + 1; j < ns; j++) {
142+ if (seen[i].l != seen[j].l || (!seen[i].mut && !seen[j].mut)) continue;
143+ for (int k = 0; k < w->reported.count; k++) if (w->reported.items[k] == seen[j].at) goto next_pair;
144+ j_list_push(&w->reported, seen[j].at);
145+ cerr(seen[j].at, "cannot use '%s' here: it is already %s in this call", seen[i].l->name,
146+ seen[i].mut ? "borrowed mutably" : "borrowed, and this use is mutable");
147+ next_pair:;
148+ }
149+ }
150+ (void)n;
151+}
152+
153+static void children(Walk *w, Node *n) {
154+ if (n->a) expr(w, n->a);
155+ if (n->b) expr(w, n->b);
156+ if (n->c) expr(w, n->c);
157+ JList *ls[] = {&n->l1, &n->l2, &n->l3, &n->l4, &n->l5};
158+ for (int k = 0; k < 5; k++) for (int i = 0; i < ls[k]->count; i++) expr(w, ls[k]->items[i]);
159+}
160+
161+static void expr(Walk *w, Node *n) {
162+ MnemoWalk *m = w->m;
163+ switch (n->kind) {
164+ case N_NAME:
165+ if (n->rk == R_LOCAL) use(w, n);
166+ return;
167+ case N_ASSIGN:
168+ if (!strcmp(n->name, "=")) {
169+ store(w, n->b);
170+ if (n->a->kind == N_NAME && n->a->rk == R_LOCAL && n->a->sym) mnemo_reinit(m, id_of(n->a->sym));
171+ else expr(w, n->a);
172+ } else {
173+ expr(w, n->a);
174+ expr(w, n->b);
175+ }
176+ return;
177+ case N_ARRAYINIT:
178+ for (int i = 0; i < n->l1.count; i++) store(w, n->l1.items[i]);
179+ return;
180+ case N_CALL: call_args(w, n, n->sym, n->a, &n->l1); return;
181+ case N_NEW: call_args(w, n, n->sym, NULL, &n->l1); return;
182+ case N_UNARY:
183+ if (n->name[0] == '&') {
184+ cerr(n, "a borrow ('%s') can only be passed as a call argument", n->name);
185+ return;
186+ }
187+ break;
188+ case N_BINARY:
189+ if (!strcmp(n->name, "&&") || !strcmp(n->name, "||")) { /* right side may not run */
190+ expr(w, n->a);
191+ uint32_t after_a = mnemo_save(m);
192+ expr(w, n->b);
193+ mnemo_join(m, after_a);
194+ mnemo_release(m, after_a);
195+ return;
196+ }
197+ break;
198+ case N_COND: {
199+ expr(w, n->a);
200+ uint32_t entry = mnemo_save(m);
201+ expr(w, n->b);
202+ uint32_t then_s = mnemo_save(m);
203+ mnemo_restore(m, entry);
204+ expr(w, n->c);
205+ mnemo_join(m, then_s);
206+ mnemo_release(m, entry);
207+ mnemo_release(m, then_s);
208+ return;
209+ }
210+ case N_LAMBDA: { /* the body runs later: check its uses, keep its moves local */
211+ uint32_t entry = mnemo_save(m);
212+ Target *saved = w->targets;
213+ w->targets = NULL;
214+ expr(w, n->b);
215+ w->targets = saved;
216+ mnemo_restore(m, entry);
217+ mnemo_release(m, entry);
218+ return;
219+ }
220+ default:
221+ break;
222+ }
223+ children(w, n);
224+}
225+
226+/* ---------------------------------------------------------------- statements */
227+
228+static void stmts(Walk *w, JList *l) {
229+ for (int i = 0; i < l->count; i++) stmt(w, l->items[i]);
230+}
231+
232+static Target *find_target(Walk *w, int for_continue) {
233+ for (Target *t = w->targets; t; t = t->up) if (!for_continue || !t->is_switch) return t;
234+ return NULL;
235+}
236+
237+static void jump(Walk *w, int is_continue) {
238+ Target *t = find_target(w, is_continue);
239+ if (t) mnemo_join_into(w->m, is_continue ? t->cont : t->brk);
240+ mnemo_kill(w->m);
241+}
242+
243+static void push_target(Walk *w, Target *t, int is_switch) {
244+ t->up = w->targets;
245+ t->is_switch = is_switch;
246+ t->brk = mnemo_save_dead(w->m);
247+ t->cont = mnemo_save_dead(w->m);
248+ w->targets = t;
249+}
250+
251+static void pop_target(Walk *w, Target *t) {
252+ w->targets = t->up;
253+ mnemo_release(w->m, t->brk);
254+ mnemo_release(w->m, t->cont);
255+}
256+
257+/* Loops run their body again when a move survives to the back edge, so every loop is walked twice;
258+ * the second pass starts from the entry state joined with the end of the first body. */
259+static void loop(Walk *w, Node *n) {
260+ MnemoWalk *m = w->m;
261+ Target t;
262+ push_target(w, &t, 0);
263+ if (n->kind == N_FOREACH) expr(w, n->b); /* the iterable is evaluated once */
264+ uint32_t entry = mnemo_save(m);
265+ uint32_t leave = mnemo_save_dead(m); /* states that leave through the condition */
266+ for (int pass = 0; pass < 2; pass++) {
267+ if (n->kind == N_DO) {
268+ stmt(w, n->b);
269+ mnemo_join(m, t.cont);
270+ if (n->a) expr(w, n->a);
271+ mnemo_join_into(m, leave);
272+ } else {
273+ if (n->kind == N_FOREACH) { if (n->sym) mnemo_reinit(m, id_of(n->sym)); }
274+ else if (n->a) expr(w, n->a);
275+ mnemo_join_into(m, leave);
276+ stmt(w, n->kind == N_FOREACH ? n->c : n->b);
277+ mnemo_join(m, t.cont);
278+ if (n->kind == N_FOR) for (int i = 0; i < n->l2.count; i++) expr(w, n->l2.items[i]);
279+ }
280+ uint32_t end = mnemo_save(m);
281+ mnemo_restore(m, entry);
282+ mnemo_join(m, end); /* back edge */
283+ mnemo_release(m, end);
284+ }
285+ mnemo_restore(m, leave);
286+ mnemo_join(m, t.brk);
287+ mnemo_release(m, entry);
288+ mnemo_release(m, leave);
289+ pop_target(w, &t);
290+}
291+
292+static void stmt(Walk *w, Node *n) {
293+ MnemoWalk *m = w->m;
294+ switch (n->kind) {
295+ case N_BLOCK: case N_SEQ: stmts(w, &n->l1); return;
296+ case N_LOCAL:
297+ if (n->b) store(w, n->b);
298+ if (n->sym) mnemo_reinit(m, id_of(n->sym)); /* a fresh variable, also on every loop iteration */
299+ return;
300+ case N_EXPRSTMT: expr(w, n->a); return;
301+ case N_IF: {
302+ expr(w, n->a);
303+ uint32_t entry = mnemo_save(m);
304+ stmt(w, n->b);
305+ uint32_t then_s = mnemo_save(m);
306+ mnemo_restore(m, entry);
307+ if (n->c) stmt(w, n->c);
308+ mnemo_join(m, then_s);
309+ mnemo_release(m, entry);
310+ mnemo_release(m, then_s);
311+ return;
312+ }
313+ case N_WHILE: case N_DO: case N_FOR: case N_FOREACH:
314+ if (n->kind == N_FOR) stmts(w, &n->l1);
315+ loop(w, n);
316+ return;
317+ case N_RETURN: if (n->a) expr(w, n->a); mnemo_kill(m); return;
318+ case N_THROW: expr(w, n->a); mnemo_kill(m); return;
319+ case N_BREAK: jump(w, 0); return;
320+ case N_CONTINUE: jump(w, 1); return;
321+ case N_TRY: {
322+ uint32_t entry = mnemo_save(m);
323+ stmt(w, n->b);
324+ uint32_t out = mnemo_save(m);
325+ mnemo_restore(m, entry);
326+ mnemo_join(m, out); /* the try block may stop anywhere */
327+ uint32_t into_catch = mnemo_save(m);
328+ for (int i = 0; i < n->l1.count; i++) {
329+ mnemo_restore(m, into_catch);
330+ stmt(w, n->l1.items[i]);
331+ mnemo_join_into(m, out);
332+ }
333+ mnemo_restore(m, out);
334+ if (n->c) stmt(w, n->c);
335+ mnemo_release(m, entry);
336+ mnemo_release(m, out);
337+ mnemo_release(m, into_catch);
338+ return;
339+ }
340+ case N_CATCH: stmt(w, n->b); return;
341+ case N_SWITCH: {
342+ expr(w, n->a);
343+ uint32_t entry = mnemo_save(m);
344+ Target t;
345+ push_target(w, &t, 1);
346+ uint32_t out = mnemo_save_dead(m);
347+ uint32_t fall = mnemo_save_dead(m);
348+ int has_default = 0;
349+ for (int i = 0; i < n->l1.count; i++) {
350+ Node *c = n->l1.items[i];
351+ if (c->flags == 1) has_default = 1;
352+ mnemo_restore(m, entry);
353+ mnemo_join(m, fall); /* colon-form cases fall through */
354+ mnemo_release(m, fall);
355+ if (c->b) {
356+ if (c->b->kind == N_BLOCK) stmt(w, c->b); else expr(w, c->b);
357+ mnemo_join_into(m, out);
358+ fall = mnemo_save_dead(m);
359+ } else {
360+ stmts(w, &c->l2);
361+ fall = mnemo_save(m);
362+ }
363+ }
364+ mnemo_restore(m, out);
365+ mnemo_join(m, fall);
366+ mnemo_join(m, t.brk);
367+ if (!has_default) mnemo_join(m, entry);
368+ mnemo_release(m, entry);
369+ mnemo_release(m, out);
370+ mnemo_release(m, fall);
371+ pop_target(w, &t);
372+ return;
373+ }
374+ default: return;
375+ }
376+}
377+
378+/* ---------------------------------------------------------------- sole owners (what may be freed) */
379+
380+/* A local is the sole owner of a fresh heap value, and gets freed at the end of its scope, when it is
381+ * declared directly in a block with a fresh String (a non-constant `+`) or array and is only ever
382+ * read in ways that cannot keep a second reference: indexing, `.length`, operands of binary operators,
383+ * String methods returning primitives, print arguments, the iterable of a for-each. Any other use
384+ * (passing it on, returning it, storing it, capturing it in a lambda, `T b = a`) leaves it to the
385+ * runtime, which never frees it: leaking is safe, freeing a shared value is not. Reassignments must
386+ * store another fresh value (or `+=`), so the old one can be freed first. */
387+
388+static int const_expr(Node *e) {
389+ switch (e->kind) {
390+ case N_LIT_INT: case N_LIT_LONG: case N_LIT_FLOAT: case N_LIT_DOUBLE: case N_LIT_CHAR:
391+ case N_LIT_STRING: case N_LIT_BOOL: case N_LIT_NULL:
392+ return 1;
393+ case N_BINARY: return const_expr(e->a) && const_expr(e->b);
394+ case N_UNARY: return const_expr(e->a);
395+ default: return 0;
396+ }
397+}
398+
399+/* ---- objects: `T o = new T(..)` where T is a plain class whose code never lets `this` escape ---- */
400+
401+static int esc_walk(Node *n, JClass *cls);
402+static int this_escapes(JMethod *m, JClass *cls);
403+
404+static int esc_list(JList *l, JClass *cls) {
405+ for (int i = 0; i < l->count; i++) if (esc_walk(l->items[i], cls)) return 1;
406+ return 0;
407+}
408+
409+/* 1 when `this` of `cls` may be kept by the code in `n`: used as a value, captured, or handed to
410+ * code we cannot see. Field reads and writes through `this`, and calls to other non-escaping methods
411+ * of the same class, are fine. */
412+static int esc_walk(Node *n, JClass *cls) {
413+ if (!n) return 0;
414+ switch (n->kind) {
415+ case N_THIS: case N_SUPER: case N_LAMBDA: case N_METHODREF: return 1;
416+ case N_NAME: return n->rk == R_CAPTURED_THIS;
417+ case N_CTORCALL: return strcmp(n->name, "super") != 0 || n->l1.count != 0; /* only `super()` of Object */
418+ case N_FIELDACC:
419+ return n->a && n->a->kind == N_THIS ? 0 : esc_walk(n->a, cls);
420+ case N_CALL: {
421+ JMethod *cm = n->sym;
422+ int on_this = !n->a || n->a->kind == N_THIS;
423+ if (on_this && cm && !(cm->flags & M_STATIC)) {
424+ if (cm->cls != cls || cm->tmpl || n->flags == 1 || this_escapes(cm, cls)) return 1;
425+ } else if (esc_walk(n->a, cls)) {
426+ return 1;
427+ }
428+ return esc_list(&n->l1, cls);
429+ }
430+ default: break;
431+ }
432+ if (esc_walk(n->a, cls) || esc_walk(n->b, cls) || esc_walk(n->c, cls)) return 1;
433+ return esc_list(&n->l1, cls) || esc_list(&n->l2, cls) || esc_list(&n->l3, cls) || esc_list(&n->l4, cls) || esc_list(&n->l5, cls);
434+}
435+
436+static int this_escapes(JMethod *m, JClass *cls) {
437+ if (m->this_esc == 1) return 0;
438+ if (m->this_esc == 2 || m->this_esc == 3) return 1; /* a cycle is treated as escaping */
439+ if (m->synthetic || !m->decl) return 0;
440+ m->this_esc = 3;
441+ int e = esc_walk(m->decl->b, cls);
442+ m->this_esc = e ? 2 : 1;
443+ return e;
444+}
445+
446+/* an object nothing else can reference once `new` returns */
447+static int fresh_object(Node *e) {
448+ if (e->kind != N_NEW || !e->sym) return 0;
449+ JMethod *k = e->sym;
450+ JClass *c = k->cls;
451+ if (!c || !c->mem_on || k->tmpl || c->native_value || (c->kind != N_CLASS && c->kind != N_RECORD)) return 0;
452+ if (c->super && c->super->cls != jt_object()) return 0;
453+ if (c->decl && c->decl->l3.count) { /* interfaces: default methods are code we do not track */
454+ for (int i = 0; i < c->ifaces.count; i++) {
455+ JClass *ic = ((JType *)c->ifaces.items[i])->cls;
456+ for (int j = 0; ic && j < ic->methods.count; j++) if (((JMethod *)ic->methods.items[j])->flags & M_DEFAULT) return 0;
457+ }
458+ }
459+ if (this_escapes(k, c)) return 0;
460+ for (int i = 0; i < c->fields.count; i++) { /* field initializers run inside the constructor */
461+ JField *f = c->fields.items[i];
462+ if (!(f->flags & M_STATIC) && f->decl && esc_walk(f->decl->b, c)) return 0;
463+ }
464+ return 1;
465+}
466+
467+/* ash.util.List / ArrayList: represented by a plain array at run time */
468+static int list_type(JType *t) {
469+ return t && t->kind == TY_CLASS && t->cls && (!strcmp(t->cls->qname, "ash.util.ArrayList") || !strcmp(t->cls->qname, "ash.util.List"));
470+}
471+
472+/* ash.util.Map / HashMap / Set / HashSet: an Ash map whose values are chains of [key, value, next] entries */
473+static int map_type(JType *t) {
474+ if (!t || t->kind != TY_CLASS || !t->cls) return 0;
475+ const char *q = t->cls->qname;
476+ return !strcmp(q, "ash.util.HashMap") || !strcmp(q, "ash.util.Map") || !strcmp(q, "ash.util.HashSet") || !strcmp(q, "ash.util.Set");
477+}
478+
479+static int obj_type(JType *t) {
480+ return t && t->kind == TY_CLASS && t->cls && !ty_is_string(t) && !t->cls->native_value && !list_type(t) && !map_type(t);
481+}
482+
483+/* `new HashMap<>()` / `new HashSet<>()` */
484+static int fresh_map(Node *e) {
485+ JMethod *k = e->kind == N_NEW ? e->sym : NULL;
486+ return k && k->tmpl && e->l1.count == 0 && k->cls &&
487+ (!strcmp(k->cls->qname, "ash.util.HashMap") || !strcmp(k->cls->qname, "ash.util.HashSet"));
488+}
489+
490+/* map / set methods that neither return the container nor keep a reference to it (ash-stdlib HashMap.ash,
491+ * HashSet.ash: keys() / values() / toList() build new lists; iterator() walks such a list) */
492+static int map_method_ok(JMethod *m) {
493+ static const char *ok[] = {"size", "isEmpty", "put", "get", "containsKey", "remove", "keys", "values", "add", "contains", "toList", NULL};
494+ if (!m || !m->cls || !m->tmpl) return 0;
495+ for (int i = 0; ok[i]; i++) if (!strcmp(m->name, ok[i])) return 1;
496+ return 0;
497+}
498+
499+/* `new ArrayList<>()`: an empty list nothing else references */
500+static int fresh_list(Node *e) {
501+ JMethod *k = e->kind == N_NEW ? e->sym : NULL;
502+ return k && k->tmpl && e->l1.count == 0 && k->cls && !strcmp(k->cls->qname, "ash.util.ArrayList");
503+}
504+
505+/* methods of a list whose result and arguments cannot hand the list itself to anyone (templates in
506+ * ash-stdlib ArrayList.ash); sublists / streams / iterators copy or point back at it and are not here */
507+static int list_method_ok(JMethod *m) {
508+ static const char *ok[] = {"size", "isEmpty", "get", "set", "add", "remove", "removeLast", "contains", "sort", NULL};
509+ if (!m || !m->cls || !m->tmpl) return 0;
510+ for (int i = 0; ok[i]; i++) if (!strcmp(m->name, ok[i])) return !(!strcmp(m->name, "add") && m->params.count == 2); /* add(int, T) may replace the list */
511+ return 0;
512+}
513+
514+int jown_is_fresh_string(Node *e) {
515+ return e->kind == N_BINARY && !strcmp(e->name, "+") && e->type && ty_is_string(e->type) && !const_expr(e);
516+}
517+
518+static int returns_fresh(JMethod *m);
519+
520+/* a call to project code whose every return hands over a fresh String / array, so the caller becomes
521+ * its sole owner. The target must be fixed at compile time: no overriding is possible. */
522+static int fresh_call(Node *e) {
523+ if (e->kind != N_CALL || !e->sym) return 0;
524+ JMethod *m = e->sym;
525+ if (!m->cls || !m->cls->mem_on || m->tmpl || m->cls->kind == N_INTERFACE) return 0;
526+ if (!(m->flags & (M_STATIC | M_PRIVATE | M_FINAL)) && m->cls->subclasses.count) return 0;
527+ if (e->flags == 1) return 0; /* super.m() */
528+ return returns_fresh(m);
529+}
530+
531+static int is_fresh(Node *e) {
532+ return e->kind == N_NEWARRAY || e->kind == N_ARRAYINIT || jown_is_fresh_string(e) || fresh_object(e) || fresh_list(e) || fresh_map(e) || fresh_call(e);
533+}
534+
535+static int is_cand(Node *n) {
536+ return n->kind == N_NAME && n->rk == R_LOCAL && n->sym && ((JLocal *)n->sym)->drop;
537+}
538+
539+/* an instance field of a plain class, read by name or as `x.f` */
540+static int field_node(Node *n) {
541+ return (n->kind == N_NAME || n->kind == N_FIELDACC) && n->sym && (n->rk == R_FIELD || n->rk == R_THIS_FIELD);
542+}
543+
544+/* a local that is still a sole-owner candidate, or a field that is still exclusively owned */
545+static int is_tracked(Node *n) {
546+ return is_cand(n) || (field_node(n) && ((JField *)n->sym)->own);
547+}
548+
549+static JType *tracked_type(Node *n) {
550+ return field_node(n) ? ((JField *)n->sym)->type : ((JLocal *)n->sym)->type;
551+}
552+
553+static void untrack(Node *n) {
554+ if (field_node(n)) ((JField *)n->sym)->own = 0;
555+ else if (n->sym) ((JLocal *)n->sym)->drop = 0;
556+}
557+
558+/* ---- elements: a String[] / List<String> whose every stored element is a fresh String ---- */
559+
560+static int str_elems(JType *t) {
561+ if (!t) return 0;
562+ if (t->kind == TY_ARRAY) return t->elem && ty_is_string(t->elem);
563+ return list_type(t) && t->nargs == 1 && t->args && ty_is_string(t->args[0]);
564+}
565+
566+/* the `elems` flag of a tracked container node, or NULL */
567+static int *elems_flag(Node *n) {
568+ if (!is_tracked(n)) return NULL;
569+ return field_node(n) ? &((JField *)n->sym)->elems : &((JLocal *)n->sym)->elems;
570+}
571+
572+static void elems_off(Node *n) {
573+ int *f = elems_flag(n);
574+ if (f) *f = 0;
575+}
576+
577+/* a container expression whose elements are known fresh Strings (or that has none yet) */
578+static int init_elems_fresh(Node *e) {
579+ if (e->kind == N_NEWARRAY) {
580+ if (!e->b) return 1; /* new String[n]: elements start as none */
581+ e = e->b;
582+ }
583+ if (e->kind == N_ARRAYINIT) {
584+ for (int i = 0; i < e->l1.count; i++) if (!jown_is_fresh_string(e->l1.items[i])) return 0;
585+ return 1;
586+ }
587+ return fresh_list(e);
588+}
589+
590+static void alias_touch(Node *n) { /* a use of a for-each variable over an elems container */
591+ if (n->kind == N_NAME && n->rk == R_LOCAL && n->sym && ((JLocal *)n->sym)->elem_of) *((JLocal *)n->sym)->elem_of = 0;
592+}
593+
594+static void collect(Node *n, int in_lambda) {
595+ if (!n) return;
596+ if (n->kind == N_LAMBDA) in_lambda = 1;
597+ if (n->kind == N_BLOCK && !in_lambda) {
598+ for (int i = 0; i < n->l1.count; i++) {
599+ Node *s = n->l1.items[i];
600+ JLocal *l = s->kind == N_LOCAL ? s->sym : NULL;
601+ if (l && s->b && is_fresh(s->b) &&
602+ (l->type->kind == TY_ARRAY || ty_is_string(l->type) || (list_type(l->type) && (fresh_list(s->b) || s->b->kind == N_CALL)) || (map_type(l->type) && fresh_map(s->b)) ||
603+ (obj_type(l->type) && ((s->b->kind == N_NEW && l->type->cls == ((JMethod *)s->b->sym)->cls) ||
604+ (s->b->kind == N_CALL && ((JMethod *)s->b->sym)->fresh_cls == l->type->cls))))) { l->drop = 1; l->elems = str_elems(l->type) && init_elems_fresh(s->b); }
605+ }
606+ }
607+ collect(n->a, in_lambda); collect(n->b, in_lambda); collect(n->c, in_lambda);
608+ JList *ls[] = {&n->l1, &n->l2, &n->l3, &n->l4, &n->l5};
609+ for (int k = 0; k < 5; k++) for (int i = 0; i < ls[k]->count; i++) collect(ls[k]->items[i], in_lambda);
610+}
611+
612+static const char *safe_methods[] = {"length", "charAt", "isEmpty", "equals", "equalsIgnoreCase", "startsWith",
613+ "endsWith", "contains", "indexOf", "lastIndexOf", "compareTo", "hashCode", NULL};
614+
615+static int safe_string_method(JMethod *m) {
616+ if (!m || m->cls != jt_string()) return 0;
617+ for (int i = 0; safe_methods[i]; i++) if (!strcmp(m->name, safe_methods[i])) return 1;
618+ return 0;
619+}
620+
621+static int print_method(JMethod *m) {
622+ return m && m->tmpl && (!strncmp(m->tmpl, "__println(", 10) || !strncmp(m->tmpl, "__print(", 8));
623+}
624+
625+static void scan(Node *n, int in_lambda);
626+
627+/* a use of `n` that cannot create a second reference */
628+static void fieldacc_base(Node *n, int in_lambda);
629+
630+static int is_get_call(Node *n) {
631+ JMethod *m = n->kind == N_CALL ? n->sym : NULL;
632+ return m && n->a && m->tmpl && !strcmp(m->name, "get") && m->params.count == 1 && is_tracked(n->a) && list_type(tracked_type(n->a));
633+}
634+
635+static void benign(Node *n, int in_lambda) {
636+ if (n->kind == N_INDEX && elems_flag(n->a)) { /* xs[i] read where it cannot be kept */
637+ benign(n->a, in_lambda);
638+ scan(n->b, in_lambda);
639+ return;
640+ }
641+ if (is_get_call(n)) { /* xs.get(i), likewise */
642+ benign(n->a, in_lambda);
643+ for (int i = 0; i < n->l1.count; i++) scan(n->l1.items[i], in_lambda);
644+ return;
645+ }
646+ if (n->kind == N_FIELDACC && field_node(n)) { /* `x.f`: reading the field is fine, x is judged on its own */
647+ if (in_lambda && is_tracked(n)) untrack(n);
648+ fieldacc_base(n, in_lambda);
649+ return;
650+ }
651+ if (n->kind != N_NAME) { scan(n, in_lambda); return; }
652+ if (in_lambda) { if (is_tracked(n)) untrack(n); alias_touch(n); } /* captured */
653+}
654+
655+/* the target of `x.f` / `x.length` */
656+static void fieldacc_base(Node *n, int in_lambda) {
657+ if (is_cand(n->a) && obj_type(((JLocal *)n->a->sym)->type)) benign(n->a, in_lambda);
658+ else if (n->name && !strcmp(n->name, "length") && n->a->kind == N_NAME) benign(n->a, in_lambda);
659+ else scan(n->a, in_lambda);
660+}
661+
662+static void scan(Node *n, int in_lambda) {
663+ if (!n) return;
664+ switch (n->kind) {
665+ case N_NAME:
666+ if (is_tracked(n)) untrack(n); /* any other use may alias */
667+ alias_touch(n);
668+ return;
669+ case N_INDEX:
670+ elems_off(n->a); /* an element read outside the harmless contexts may keep it */
671+ benign(n->a, in_lambda);
672+ scan(n->b, in_lambda);
673+ return;
674+ case N_FIELDACC:
675+ if (is_tracked(n)) untrack(n); /* a field read outside the benign contexts may alias */
676+ fieldacc_base(n, in_lambda);
677+ return;
678+ case N_BINARY: {
679+ /* + on an object calls toString, which could keep it; identity tests cannot */
680+ int ident = !strcmp(n->name, "==") || !strcmp(n->name, "!=");
681+ Node *ops[2] = {n->a, n->b};
682+ for (int i = 0; i < 2; i++) {
683+ if (!ident && ops[i]->kind == N_NAME && is_cand(ops[i]) && obj_type(((JLocal *)ops[i]->sym)->type)) scan(ops[i], in_lambda);
684+ else benign(ops[i], in_lambda);
685+ }
686+ return;
687+ }
688+ case N_CALL: {
689+ JMethod *m = n->sym;
690+ if (n->a && n->a->kind == N_NAME && is_cand(n->a) && obj_type(((JLocal *)n->a->sym)->type)) {
691+ JClass *c = ((JLocal *)n->a->sym)->type->cls;
692+ JLocal *recv = n->a->sym;
693+ int fixed = !recv->is_param || (c->kind != N_INTERFACE && !c->subclasses.count); /* a parameter may be a subclass */
694+ if (fixed && m && m->cls == c && !m->tmpl && !(m->flags & M_STATIC) && !this_escapes(m, c)) benign(n->a, in_lambda);
695+ else scan(n->a, in_lambda);
696+ } else if (n->a && is_tracked(n->a) && (list_type(tracked_type(n->a)) || map_type(tracked_type(n->a)))) {
697+ if (list_type(tracked_type(n->a)) ? list_method_ok(m) : map_method_ok(m)) benign(n->a, in_lambda); else scan(n->a, in_lambda);
698+ if (list_type(tracked_type(n->a)) && m && m->tmpl && elems_flag(n->a)) {
699+ int last = n->l1.count - 1;
700+ if (!strcmp(m->name, "get")) elems_off(n->a); /* reached here: not in a harmless context */
701+ else if ((!strcmp(m->name, "add") && n->l1.count == 1) || (!strcmp(m->name, "set") && n->l1.count == 2)) {
702+ if (!jown_is_fresh_string(n->l1.items[last]) || in_lambda) elems_off(n->a); /* a shared / literal element */
703+ }
704+ }
705+ } else if (n->a) { if (safe_string_method(m)) benign(n->a, in_lambda); else scan(n->a, in_lambda); }
706+ for (int i = 0; i < n->l1.count; i++) {
707+ Node *arg = n->l1.items[i];
708+ int obj_arg = arg->kind == N_NAME && is_cand(arg) && obj_type(((JLocal *)arg->sym)->type);
709+ if ((print_method(m) || safe_string_method(m)) && !obj_arg) benign(arg, in_lambda);
710+ else if (param_borrowed(m, i) && is_borrow(arg)) benign(arg->a, in_lambda);
711+ else if (param_borrowed(m, i)) benign(arg, in_lambda);
712+ else scan(n->l1.items[i], in_lambda);
713+ }
714+ return;
715+ }
716+ case N_EXPRSTMT:
717+ if (n->a->kind == N_ASSIGN && n->a->a->kind == N_INDEX && elems_flag(n->a->a->a)) {
718+ Node *as = n->a; /* xs[i] = value */
719+ if (strcmp(as->name, "=") != 0 || !jown_is_fresh_string(as->b) || in_lambda) elems_off(as->a->a);
720+ benign(as->a->a, in_lambda);
721+ scan(as->a->b, in_lambda);
722+ scan(as->b, in_lambda);
723+ return;
724+ }
725+ if (n->a->kind == N_ASSIGN && is_tracked(n->a->a)) {
726+ Node *as = n->a;
727+ if (elems_flag(as->a) && (strcmp(as->name, "=") != 0 || !init_elems_fresh(as->b))) elems_off(as->a); /* a new container with unknown elements */
728+ int ok = (!strcmp(as->name, "=") && is_fresh(as->b)) || (!strcmp(as->name, "+=") && ty_is_string(tracked_type(as->a)));
729+ if (!ok || in_lambda) untrack(as->a);
730+ if (as->a->kind == N_FIELDACC) fieldacc_base(as->a, in_lambda);
731+ if (!strcmp(as->name, "+=")) benign(as->b, in_lambda); else scan(as->b, in_lambda);
732+ return;
733+ }
734+ scan(n->a, in_lambda);
735+ return;
736+ case N_RETURN:
737+ /* a local's value goes to the caller (lowering skips freeing it on this path); a parameter's value
738+ * is the caller's own, so returning it is an escape */
739+ if (n->a && is_tracked(n->a) && (field_node(n->a) || ((JLocal *)n->a->sym)->is_param)) scan(n->a, in_lambda);
740+ else if (n->a) benign(n->a, in_lambda);
741+ return;
742+ case N_NEW: {
743+ JMethod *k = n->sym;
744+ for (int i = 0; i < n->l1.count; i++) {
745+ Node *arg = n->l1.items[i];
746+ if (is_borrow(arg) && param_borrowed(k, i)) benign(arg->a, in_lambda);
747+ else if (param_borrowed(k, i)) benign(arg, in_lambda);
748+ else scan(arg, in_lambda);
749+ }
750+ scan(n->a, in_lambda);
751+ return;
752+ }
753+ case N_FOREACH:
754+ if (n->sym && elems_flag(n->b)) ((JLocal *)n->sym)->elem_of = elems_flag(n->b); /* the loop variable aliases the elements */
755+ benign(n->b, in_lambda);
756+ scan(n->c, in_lambda);
757+ return;
758+ case N_LAMBDA:
759+ scan(n->b, 1);
760+ return;
761+ default:
762+ break;
763+ }
764+ scan(n->a, in_lambda); scan(n->b, in_lambda); scan(n->c, in_lambda);
765+ JList *ls[] = {&n->l1, &n->l2, &n->l3, &n->l4, &n->l5};
766+ for (int k = 0; k < 5; k++) for (int i = 0; i < ls[k]->count; i++) scan(ls[k]->items[i], in_lambda);
767+}
768+
769+/* ---------------------------------------------------------------- entry */
770+
771+static int param_cand(JType *t) {
772+ return t && (t->kind == TY_ARRAY || ty_is_string(t) || obj_type(t) || list_type(t) || map_type(t));
773+}
774+
775+static void analyze_method(JMethod *m) {
776+ if (m->own_done || !m->decl || !m->decl->b) return;
777+ m->own_done = 1;
778+ collect(m->decl->b, 0);
779+ /* parameters are analysed like sole-owner locals, with one difference: returning one hands the
780+ * caller's own value back, which is an escape */
781+ for (int i = 0; i < m->decl->l1.count; i++) {
782+ Node *pn = m->decl->l1.items[i];
783+ JLocal *l = pn->kind == N_PARAM ? pn->sym : NULL;
784+ if (l && param_cand(l->type) && !(m->flags & M_VARARGS)) l->drop = 1;
785+ }
786+ scan(m->decl->b, 0);
787+ for (int i = 0; i < m->decl->l1.count; i++) {
788+ Node *pn = m->decl->l1.items[i];
789+ JLocal *l = pn->kind == N_PARAM ? pn->sym : NULL;
790+ if (l) { l->param_ok = l->drop; l->drop = 0; } /* a parameter is never freed by the callee */
791+ }
792+}
793+
794+/* project code whose body we can see and whose target is fixed at compile time (no override can run instead) */
795+static int fixed_project_target(JMethod *m) {
796+ if (!m || m->tmpl || !m->cls || !m->cls->mem_on || !m->decl || !m->decl->b || (m->flags & M_VARARGS)) return 0;
797+ if (!m->is_ctor && !(m->flags & (M_STATIC | M_PRIVATE | M_FINAL)) && (m->cls->kind == N_INTERFACE || m->cls->subclasses.count)) return 0;
798+ return 1;
799+}
800+
801+/* 1 when argument `i` of a call to `m` is only read by the callee, so the caller keeps owning what it passes
802+ * (and nothing is moved). The target must be fixed at compile time (no override can run instead). */
803+static int param_borrowed(JMethod *m, int i) {
804+ if (!m || m->tmpl || !m->cls || !m->cls->mem_on || !m->decl || !m->decl->b || (m->flags & M_VARARGS)) return 0;
805+ if (!m->is_ctor && !(m->flags & (M_STATIC | M_PRIVATE | M_FINAL)) && (m->cls->kind == N_INTERFACE || m->cls->subclasses.count)) return 0;
806+ if (i >= m->decl->l1.count) return 0;
807+ analyze_method(m);
808+ Node *pn = m->decl->l1.items[i];
809+ JLocal *l = pn->kind == N_PARAM ? pn->sym : NULL;
810+ return l && l->param_ok;
811+}
812+
813+/* every `return e` outside lambdas hands over a fresh value, or a local that is its sole owner; for
814+ * objects they must all be the same exact class so the caller knows what it owns */
815+static int all_returns_fresh(Node *n, int *seen, JClass **cls, int *cls_set) {
816+ if (!n || n->kind == N_LAMBDA) return 1;
817+ if (n->kind == N_RETURN) {
818+ (*seen)++;
819+ Node *e = n->a;
820+ if (!e) return 0;
821+ JClass *c = NULL;
822+ if (fresh_object(e)) c = ((JMethod *)e->sym)->cls;
823+ else if (e->kind == N_NAME && is_cand(e)) { JType *t = ((JLocal *)e->sym)->type; c = obj_type(t) ? t->cls : NULL; }
824+ else if (!is_fresh(e)) return 0;
825+ if (*cls_set && *cls != c) return 0;
826+ *cls = c;
827+ *cls_set = 1;
828+ return 1;
829+ }
830+ if (!all_returns_fresh(n->a, seen, cls, cls_set) || !all_returns_fresh(n->b, seen, cls, cls_set) ||
831+ !all_returns_fresh(n->c, seen, cls, cls_set)) return 0;
832+ JList *ls[] = {&n->l1, &n->l2, &n->l3, &n->l4, &n->l5};
833+ for (int k = 0; k < 5; k++) for (int i = 0; i < ls[k]->count; i++) if (!all_returns_fresh(ls[k]->items[i], seen, cls, cls_set)) return 0;
834+ return 1;
835+}
836+
837+static int returns_fresh(JMethod *m) {
838+ if (m->fresh_ret == 1) return 1;
839+ if (m->fresh_ret >= 2) return 0; /* no, or a recursive cycle */
840+ if (!m->decl || !m->decl->b || m->is_ctor || !m->ret || !(m->ret->kind == TY_ARRAY || ty_is_string(m->ret) || list_type(m->ret) || obj_type(m->ret))) return 0;
841+ m->fresh_ret = 3;
842+ analyze_method(m);
843+ int seen = 0, cls_set = 0;
844+ JClass *cls = NULL;
845+ int ok = all_returns_fresh(m->decl->b, &seen, &cls, &cls_set) && seen > 0;
846+ if (ok && obj_type(m->ret) && (!cls || cls != m->ret->cls)) ok = 0; /* an object must be exactly the declared class */
847+ if (ok && !obj_type(m->ret) && cls) ok = 0;
848+ m->fresh_cls = ok ? cls : NULL;
849+ m->fresh_ret = ok ? 1 : 2;
850+ return ok;
851+}
852+
853+static void check_method(JMethod *m, int flow) {
854+ if (!m->decl || !m->decl->b) return;
855+ analyze_method(m);
856+ if (!flow) return;
857+ Walk w = {0};
858+ w.m = mnemo_walk_new();
859+ stmt(&w, m->decl->b);
860+ mnemo_walk_free(w.m);
861+}
862+
863+static int under(const char *path, const char *root) {
864+ if (path && !strcmp(root, ".")) return !strchr(path, '/'); /* entry file in the working directory */
865+ size_t n = strlen(root);
866+ return path && strncmp(path, root, n) == 0 && (path[n] == '/' || path[n] == 0);
867+}
868+
869+int jown_owned_fields(JClass *c, JField ***out) {
870+ static JField *buf[64];
871+ int n = 0;
872+ for (int k = 0; k < c->fields.count && n < 64; k++) {
873+ JField *f = c->fields.items[k];
874+ if (f->own && !(f->flags & M_STATIC)) buf[n++] = f;
875+ }
876+ *out = buf;
877+ return n;
878+}
879+
880+int jown_local_elems(JLocal *l) { return l->drop && l->elems; }
881+int jown_field_elems(JField *f) { return f->own && f->elems; }
882+
883+int jown_field_assign_frees(Node *n) {
884+ if (!field_node(n) || !((JField *)n->sym)->own) return 0;
885+ if (n->kind == N_NAME) return 1;
886+ return n->a && (n->a->kind == N_THIS || (n->a->kind == N_NAME && n->a->rk == R_LOCAL)); /* no side effects when evaluated twice */
887+}
888+
889+void jown_check_project(const char *entry_path) {
890+ char *root = find_project_root(entry_path);
891+ if (!root || !memory_safety_on(root)) return;
892+ JClass **all;
893+ int n = jt_all_classes(&all);
894+ for (int i = 0; i < n; i++) if (under(all[i]->path, root)) all[i]->mem_on = 1; /* before any analysis reads it */
895+ /* fields start as "exclusively owned" candidates (a plain class's String / array instance fields) and lose
896+ * the status at the first use that could alias or replace them with a shared value */
897+ for (int i = 0; i < n; i++) {
898+ JClass *c = all[i];
899+ if (!c->mem_on || c->kind != N_CLASS) continue;
900+ for (int k = 0; k < c->fields.count; k++) {
901+ JField *f = c->fields.items[k];
902+ f->own = !(f->flags & M_STATIC) && f->type && (f->type->kind == TY_ARRAY || ty_is_string(f->type) || list_type(f->type) || map_type(f->type));
903+ f->elems = f->own && str_elems(f->type) && !(f->decl && f->decl->b); /* no initializer: elements start empty */
904+ }
905+ }
906+ for (int i = 0; i < n; i++) {
907+ JClass *c = all[i];
908+ if (!c->mem_on || c->kind != N_CLASS) continue;
909+ for (int k = 0; k < c->fields.count; k++) {
910+ JField *f = c->fields.items[k];
911+ if (!f->own || !f->decl || !f->decl->b) continue;
912+ if (!is_fresh(f->decl->b)) f->own = 0; /* a literal / alias initializer: the value is shared */
913+ f->elems = f->own && str_elems(f->type) && init_elems_fresh(f->decl->b);
914+ scan(f->decl->b, 0);
915+ }
916+ }
917+ for (int i = 0; i < n; i++) {
918+ JClass *c = all[i];
919+ if (!c->mem_on) continue;
920+ for (int k = 0; k < c->methods.count; k++) check_method(c->methods.items[k], 1);
921+ for (int k = 0; k < c->ctors.count; k++) check_method(c->ctors.items[k], 1);
922+ }
923+}

hearth/front/src/jown/D/.gitkeepadded

+0−0

No content changes. View the file on GitHub

hearth/front/src/jown/H/own.hadded

+42−0
@@ -0,0 +1,42 @@
1+#ifndef JOWN_H
2+#define JOWN_H
3+
4+/* Ownership checking for projects that turn it on (`memory-safety: on` in ash.pkg).
5+ *
6+ * Runs after the type checker, on the typed AST, and only on the sources of the project that owns the
7+ * entry file (never on the standard library or dependencies, whose code the user cannot change).
8+ * Rules (goals.md item 1, first milestone):
9+ * - A local of an owning type (any class, String or array; not primitives, boxes or enums) is MOVED
10+ * when it is stored: `a = b`, `T a = b`, `this.f = b`, `xs[i] = b`, or `{b, ...}`. Using `b`
11+ * afterwards is an error until it is assigned a new value.
12+ * - Passing a value to a call, constructor or return is a borrow, not a move.
13+ * - Control flow is followed: a move in one branch of an `if`/`switch`, or in a loop body that can
14+ * run again, counts afterwards; `return`, `throw`, `break` and `continue` end a path.
15+ * Errors: reported with cerr(), so compilation fails like any other type error.
16+ * Default when ash.pkg is missing or has no `memory-safety:` key: on (JOWN_DEFAULT_ON); with no ash.pkg
17+ * at all the entry file's directory is the project. */
18+
19+#include "jparse/H/jast.h"
20+#include "jtypes/H/jtypes.h"
21+#include "jtypes/H/check.h"
22+
23+#define JOWN_DEFAULT_ON 1
24+
25+/* Checks the project that contains `entry_path`. Does nothing when memory safety is off. */
26+void jown_check_project(const char *entry_path);
27+
28+/* A String built by a non-constant `+`: a new heap value that nothing else references yet. */
29+int jown_is_fresh_string(Node *e);
30+
31+/* The String / array instance fields of plain class `c` that its objects exclusively own: nothing else can
32+ * hold their values, so freeing an object frees them too. Valid after jown_check_project. `*out` is a static buffer. */
33+int jown_owned_fields(JClass *c, JField ***out);
34+/* `n` names an owned field through a plain base (`f`, `this.f`, `local.f`): its old value can be freed on assignment. */
35+int jown_field_assign_frees(Node *n);
36+
37+/* A freed String[] / List<String> (local or owned field) whose elements are all fresh Strings only it references:
38+ * the elements are freed with it. */
39+int jown_local_elems(JLocal *l);
40+int jown_field_elems(JField *f);
41+
42+#endif

hearth/front/src/jown/O/.gitkeepadded

+0−0

No content changes. View the file on GitHub

hearth/front/src/jparse/C/jparse_expr.cmodified

+13−0
@@ -273,6 +273,19 @@ static Node *unary(Parser *p) {
273273 n->a = unary(p);
274274 return n;
275275 }
276+ if (t->kind == JT_AMP) {
277+ /* `&x` shared borrow, `&mut x` exclusive borrow: only meaningful as a call argument (checked by jown) */
278+ Node *n = p_node(p, N_UNARY);
279+ n->name = "&";
280+ p_next(p);
281+ JToken *m = p_peek(p);
282+ if (m->kind == JT_IDENT && !strcmp(m->text, "mut") && (p_peek_at(p, 1)->kind == JT_IDENT || p_peek_at(p, 1)->kind == JT_THIS)) {
283+ n->name = "&mut";
284+ p_next(p);
285+ }
286+ n->a = unary(p);
287+ return n;
288+ }
276289 if (t->kind == JT_LPAREN && !lambda_ahead(p)) {
277290 Node *line_node = p_node(p, N_CAST);
278291 Node *type = try_cast(p);

hearth/front/src/jtypes/C/check_class.cmodified

+1−0
@@ -210,6 +210,7 @@ void check_method_body(JClass *c, JMethod *m) {
210210 Node *pn = (d->l1.count > i) ? d->l1.items[i] : d;
211211 JLocal *l = jt_declare_local(&cx, pn, p->name, p->type, (d->l1.count > i) ? pn->flags : 0, 1);
212212 l->has_init = 1;
213+ if (d->l1.count > i) pn->sym = l; /* jown finds a parameter's local through its declaration */
213214 }
214215 Node *body = d->b;
215216 int explicit_call = body->l1.count > 0 && ((Node *)body->l1.items[0])->kind == N_CTORCALL;

hearth/front/src/jtypes/C/check_expr.cmodified

+107−1
@@ -1,6 +1,7 @@
11 #include <stdlib.h>
22 #include <string.h>
33
4+#include "jlower/H/jlower.h"
45 #include "jtypes/H/check.h"
56
67 /* Expression checking: names, members, calls with overload resolution and generic
@@ -968,6 +969,103 @@ static void non_null_operand(Node *n, Node *e, JType *t) {
968969 if (t->nullable && ty_is_prim(ty_unbox(t))) cerr(e, "possible null: nullable %s cannot be used with operator '%s'; check for null first", ty_str(t), n->name);
969970 }
970971
972+/* Compile-time arithmetic safety, as Rust denies `arithmetic_overflow`: a constant int expression that
973+ * leaves the 32-bit range is an error rather than a silent wrap or widening (not under --wrap-int,
974+ * where wrap-around is the defined behaviour; division by zero stays Java's runtime exception).
975+ * fold_int returns 1 when `e` folds to *out, 0 when it is not a constant, 2 when folding hit an overflow (*culprit is the node where it happened). */
976+static int fold_int(Node *e, long long *out, Node **culprit) {
977+ if (e->kind == N_LIT_INT) {
978+ unsigned long long v = strtoull(e->name, NULL, 0);
979+ if (v > 2147483647ULL && v <= 0xFFFFFFFFULL && e->name[0] == '0') *out = (long long)v - 4294967296LL; /* hex int literal wraps */
980+ else *out = (long long)v;
981+ return 1;
982+ }
983+ /* a final int local or static final int field with a constant initializer is that constant
984+ * (`Integer.MAX_VALUE + 1`, `final int N = 100000; N * N`) */
985+ if ((e->kind == N_NAME || e->kind == N_FIELDACC) && e->sym) {
986+ static int depth;
987+ Node *init = NULL;
988+ if (e->rk == R_STATIC_FIELD) {
989+ JField *f = e->sym;
990+ if ((f->flags & M_FINAL) && f->type && f->type->kind == TY_INT && f->decl) init = f->decl->b;
991+ } else if (e->rk == R_LOCAL) {
992+ JLocal *l = e->sym;
993+ if ((l->flags & M_FINAL) && l->type && l->type->kind == TY_INT && l->decl && l->decl->kind == N_LOCAL) init = l->decl->b;
994+ }
995+ if (!init || depth > 16) return 0;
996+ depth++;
997+ int r = fold_int(init, out, culprit);
998+ depth--;
999+ return r == 2 ? 0 : r; /* an overflow inside the initializer is reported at the initializer, not at every use */
1000+ }
1001+ if (e->kind == N_UNARY && !strcmp(e->name, "-")) {
1002+ long long v;
1003+ int r = fold_int(e->a, &v, culprit);
1004+ if (r != 1) return r;
1005+ *out = -v;
1006+ if (*out < -2147483648LL || *out > 2147483647LL) { *culprit = e; return 2; }
1007+ return 1;
1008+ }
1009+ if (e->kind == N_BINARY && e->name[1] == 0 && strchr("+-*/%", e->name[0])) {
1010+ long long a, b;
1011+ int ra = fold_int(e->a, &a, culprit);
1012+ if (ra == 2) return 2;
1013+ int rb = fold_int(e->b, &b, culprit);
1014+ if (rb == 2) return 2;
1015+ if (ra != 1 || rb != 1) return 0;
1016+ long long v;
1017+ switch (e->name[0]) {
1018+ case '+': v = a + b; break;
1019+ case '-': v = a - b; break;
1020+ case '*': v = a * b; break; /* both |operands| <= 2^31, so this fits in 64 bits */
1021+ case '/': if (b == 0) return 0; v = a / b; break;
1022+ default: if (b == 0) return 0; v = a % b; break;
1023+ }
1024+ if (v < -2147483648LL || v > 2147483647LL) { *culprit = e; return 2; }
1025+ *out = v;
1026+ return 1;
1027+ }
1028+ return 0;
1029+}
1030+
1031+static void check_const_arith(Node *n, JType *result) {
1032+ if (jl_wrap_int || result->kind != TY_INT) return; /* --wrap-int defines overflow as Java wrap-around */
1033+ long long v;
1034+ Node *culprit = NULL;
1035+ if (fold_int(n, &v, &culprit) == 2 && culprit == n) {
1036+ cerr(n, "arithmetic overflow: constant expression is outside the range of int; use an 'L' suffix for long");
1037+ }
1038+}
1039+
1040+/* Length of an array expression known at compile time: a `new T[k]` / `{...}` literal, or a `final` local
1041+ * initialised with one (other locals can still be reassigned, and programs rely on the run-time
1042+ * IndexOutOfBounds for those). Returns -1 when unknown. */
1043+static long const_array_len(Node *e) {
1044+ if (e->kind == N_NAME && e->rk == R_LOCAL && e->sym) {
1045+ JLocal *l = e->sym;
1046+ if (!(l->flags & M_FINAL) || !l->decl || l->decl->kind != N_LOCAL || !l->decl->b) return -1;
1047+ e = l->decl->b;
1048+ }
1049+ if (e->kind == N_ARRAYINIT) return e->l1.count;
1050+ if (e->kind == N_NEWARRAY) {
1051+ if (e->l1.count) {
1052+ long long v; Node *c = NULL;
1053+ return fold_int(e->l1.items[0], &v, &c) == 1 && v >= 0 ? (long)v : -1;
1054+ }
1055+ if (e->b && e->b->kind == N_ARRAYINIT) return e->b->l1.count;
1056+ }
1057+ return -1;
1058+}
1059+
1060+/* a[k] with constant k outside a known length (or negative) can never succeed: deny it. */
1061+static void check_const_index(Node *n) {
1062+ long long k; Node *c = NULL;
1063+ if (fold_int(n->b, &k, &c) != 1) return;
1064+ if (k < 0) { cerr(n, "array index %lld is negative", k); return; }
1065+ long len = const_array_len(n->a);
1066+ if (len >= 0 && k >= len) cerr(n, "array index %lld is out of bounds for an array of length %ld", k, len);
1067+}
1068+
9711069 static JType *check_binary(Ctx *cx, Node *n) {
9721070 const char *op = n->name;
9731071 if (!strcmp(op, "&&") || !strcmp(op, "||")) {
@@ -996,6 +1094,7 @@ static JType *check_binary(Ctx *cx, Node *n) {
9961094 if (!r) { bad_operands(n, a, b); return ty_error(); }
9971095 non_null_operand(n, n->a, a);
9981096 non_null_operand(n, n->b, b);
1097+ check_const_arith(n, r);
9991098 return r;
10001099 }
10011100 if (!strcmp(op, "<<") || !strcmp(op, ">>") || !strcmp(op, ">>>")) {
@@ -1114,6 +1213,10 @@ static JType *check_unary(Ctx *cx, Node *n) {
11141213 }
11151214 JType *t = check_expr(cx, n->a, NULL);
11161215 if (t->kind == TY_ERROR) return t;
1216+ if (op[0] == '&') { /* borrow: same type and value as the variable it borrows */
1217+ if (n->a->kind != N_NAME || n->a->rk != R_LOCAL) { cerr(n, "only a local variable can be borrowed with '%s'", op); return ty_error(); }
1218+ return t;
1219+ }
11171220 if (!strcmp(op, "!")) {
11181221 if (!is_bool(t)) { cerr(n, "operator '!' cannot be applied to %s", ty_str(t)); return TBOOL; }
11191222 return TBOOL;
@@ -1296,7 +1399,10 @@ JType *check_expr_ex(Ctx *cx, Node *n, JType *expected, int allow_type) {
12961399 if (a->kind == TY_ERROR) { t = a; break; }
12971400 deref_check(n, n->a, a);
12981401 if (a->kind != TY_ARRAY) { cerr(n, "indexing requires an array, found %s", ty_str(a)); t = ty_error(); }
1299- else t = a->elem;
1402+ else {
1403+ t = a->elem;
1404+ check_const_index(n);
1405+ }
13001406 break;
13011407 }
13021408 case N_UNARY: t = check_unary(cx, n); break;

hearth/front/src/jtypes/H/check.hmodified

+4−0
@@ -19,6 +19,10 @@ struct JLocal {
1919 const char *lname;
2020 Node *alias; /* instanceof binding: the scrutinee expression */
2121 int is_temp;
22+ int elems; /* jown: a candidate String[] / List<String> whose elements are all fresh Strings it alone owns */
23+ int *elem_of; /* jown: a for-each variable over such a container: the container's `elems` flag */
24+ int param_ok; /* jown: a parameter the method only reads, so a caller keeps ownership of what it passes */
25+ int drop; /* jown proved this local is the sole owner of a fresh String/array: free it at scope end */
2226 };
2327
2428 typedef enum { F_TRY, F_METHOD, F_LAMBDA, F_INIT } FrameKind;

hearth/front/src/jtypes/H/jtypes.hmodified

+7−0
@@ -51,6 +51,8 @@ struct JField {
5151 const char *lname; /* lowered name */
5252 const char *tmpl; /* @Lower template for native constants */
5353 int is_enum_const;
54+ int elems; /* jown: as JLocal.elems, for an owned String[] / List<String> field */
55+ int own; /* jown: the object exclusively owns this field's String / array (freed with the object) */
5456 };
5557
5658 struct JMethod {
@@ -70,6 +72,10 @@ struct JMethod {
7072 int has_override_anno;
7173 int is_record_accessor;
7274 int synthetic; /* generated (record members, enum values()) */
75+ int own_done; /* jown: sole-owner analysis of the body has run */
76+ JClass *fresh_cls; /* jown: the exact class of the fresh object every return hands over (NULL for String / array) */
77+ int fresh_ret; /* jown: 0 unknown, 1 every return is a fresh heap value, 2 not, 3 being analysed */
78+ int this_esc; /* jown: 0 unknown, 1 `this` does not escape, 2 it may, 3 being analysed */
7379 };
7480
7581 struct JClass {
@@ -101,6 +107,7 @@ struct JClass {
101107 int order; /* load order */
102108 JMethod *sam; /* cached single abstract method; sam_done marks the cache as valid */
103109 int sam_done;
110+ int mem_on; /* in a project with `memory-safety: on`: lowering may free what jown proved unshared */
104111 };
105112
106113 /* resolution kinds stored in Node.rk */

hearth/front/tests/cases/run/run_oop_j.ashmodified

+1−1
@@ -62,6 +62,6 @@ public class RunOop {
6262 Object o = a;
6363 System.out.println(o instanceof Greeter);
6464 System.out.println(o instanceof Polite);
65- System.out.println(b instanceof English e ? e.kind() : "no");
65+ System.out.println(g instanceof English e ? e.kind() : "no"); // b was moved into g
6666 }
6767 }

hearth/kiln/Makefilemodified

+12−5
@@ -2,7 +2,7 @@ CC = gcc
22 .DEFAULT_GOAL := all
33 KILN_RPATH_LIST = $(abspath ../../bin/extensions/pyre):/usr/local/lib/kiln
44 CFLAGS = -Wall -Wextra -std=c11 -O2 -Isrc -I../front/src -DKILN_RPATH='"$(KILN_RPATH_LIST)"'
5-LDFLAGS = ../../bin/libashfront.a
5+LDFLAGS = ../../bin/lib/front.a ../../bin/lib/mnemo.a -lpthread -ldl -lm
66
77 KILN_SRC = \
88 src/app/C/main.c \
@@ -18,6 +18,7 @@ KILN_SRC = \
1818 src/parser/C/statements/attempt_handle.c \
1919 src/codegen/C/builtins/builtins.c \
2020 src/codegen/C/builtins/exit_builtin.c \
21+ src/codegen/C/builtins/mem_builtin.c \
2122 src/codegen/C/builtins/proc_builtins.c \
2223 src/codegen/C/builtins/filter_builtin.c \
2324 src/codegen/C/builtins/map_builtin.c \
@@ -107,12 +108,16 @@ all: $(BIN)
107108
108109 # The shared front end (lexer, parser, type checker, lowering) is the only frontend kiln has;
109110 # front's own Makefile decides whether the library is up to date.
110-../../bin/libashfront.a: FORCE
111+../../bin/lib/front.a: FORCE
111112 $(MAKE) -C ../front
112113
114+# Mnemosyne (Rust): the ownership pass in front calls into it.
115+../../bin/lib/mnemo.a: FORCE
116+ $(MAKE) -C ../mnemo
117+
113118 FORCE:
114119
115-$(BIN): $(KILN_OBJ) ../../bin/libashfront.a
120+$(BIN): $(KILN_OBJ) ../../bin/lib/front.a ../../bin/lib/mnemo.a
116121 mkdir -p ../../bin
117122 $(CC) $(CFLAGS) -o $(BIN) $(KILN_OBJ) $(LDFLAGS)
118123
@@ -127,10 +132,11 @@ clean:
127132 # --link=shared kiln-compiled program's DT_RPATH points at (see
128133 # src/elf/elf_dynamic.h's KILN_RPATH) -- needs root to write there, so
129134 # this is a separate, explicit, opt-in step, never run by `make all`.
130-KILNRT = ../../bin/libkilnrt.so
135+KILNRT = ../../bin/lib/libkilnrt.so
131136 KILN_RPATH_DIR = /usr/local/lib/kiln
132137
133138 $(KILNRT): $(BIN)
139+ mkdir -p ../../bin/lib
134140 $(BIN) --emit-runtime -o $(KILNRT)
135141
136142 install-runtime: $(KILNRT)
@@ -141,9 +147,10 @@ install-runtime: $(KILNRT)
141147 # order checks the launching .exe's own directory first, so
142148 # libkilnrt.dll just needs to sit next to whatever --link=shared
143149 # --target=windows binary is using it (or on PATH).
144-KILNRT_DLL = ../../bin/libkilnrt.dll
150+KILNRT_DLL = ../../bin/lib/libkilnrt.dll
145151
146152 $(KILNRT_DLL): $(BIN)
153+ mkdir -p ../../bin/lib
147154 $(BIN) --emit-runtime -o $(KILNRT_DLL) --target=windows
148155
149156 runtime-dll: $(KILNRT_DLL)

hearth/kiln/src/app/C/main.cmodified

+7−0
@@ -10,6 +10,8 @@
1010 #include "pe/H/pe_dll_writer.h"
1111 #include "diagnostics/H/diagnostics.h"
1212 #include "jdriver/H/jdriver.h"
13+#include "jlower/H/jlower.h"
14+#include "codegen/H/runtime/heap.h"
1315 #include "app/H/target.h"
1416
1517 static void usage(const char *prog) {
@@ -82,8 +84,13 @@ int main(int argc, char *argv[]) {
8284 }
8385 }
8486
87+ // Only the static builds (Linux and Windows) can give memory back (heap.h); --link=shared IR carries no frees.
88+ if (kiln_get_link_mode() == KILN_LINK_STATIC) jl_drops = JL_DROP_STRINGS | JL_DROP_ARRAYS | JL_DROP_OBJECTS | JL_DROP_MAPS | JL_DROP_ELEMS | JL_NATIVE_NEWARR;
8589 char *ir = jdriver_compile_file(input_path, wrap_int);
8690 if (!ir) return 1;
91+ if (strstr(ir, "mem_free(") || strstr(ir, "mem_cat(") || strstr(ir, "mem_free_arr(") || strstr(ir, "mem_free_obj(") || strstr(ir, "mem_free_map(") || strstr(ir, "mem_free_elems(") || strstr(ir, "mem_newarr1(")) {
92+ heap_enable_free_lists(strstr(ir, "mem_free_arr(") != NULL || strstr(ir, "mem_free_obj(") != NULL || strstr(ir, "mem_free_map(") != NULL || strstr(ir, "mem_newarr1(") != NULL);
93+ }
8794 diagnostics_set_source(ir, "<internal ir>");
8895
8996 CodeBuf machine_code;

hearth/kiln/src/app/D/main.dmodified

+8−1
@@ -7,7 +7,9 @@ src/app/O/main.o: src/app/C/main.c src/parser/H/core/parser.h \
77 src/codegen/H/emit/ash_gpu_exports.def src/elf/H/elf_so_writer.h \
88 src/pe/H/pe_writer.h src/pe/H/pe_imports.def src/pe/H/pe_dll_writer.h \
99 src/diagnostics/H/diagnostics.h ../front/src/jdriver/H/jdriver.h \
10- src/app/H/target.h
10+ ../front/src/jlower/H/jlower.h ../front/src/jtypes/H/jtypes.h \
11+ ../front/src/jparse/H/jast.h ../front/src/jcommon/H/jcommon.h \
12+ src/codegen/H/runtime/heap.h src/app/H/target.h
1113 src/parser/H/core/parser.h:
1214 ../front/src/ir/H/ir_lex.h:
1315 src/codegen/H/emit/emit.h:
@@ -24,4 +26,9 @@ src/pe/H/pe_imports.def:
2426 src/pe/H/pe_dll_writer.h:
2527 src/diagnostics/H/diagnostics.h:
2628 ../front/src/jdriver/H/jdriver.h:
29+../front/src/jlower/H/jlower.h:
30+../front/src/jtypes/H/jtypes.h:
31+../front/src/jparse/H/jast.h:
32+../front/src/jcommon/H/jcommon.h:
33+src/codegen/H/runtime/heap.h:
2734 src/app/H/target.h:

hearth/kiln/src/codegen/C/builtins/builtins.cmodified

+10−1
@@ -14,6 +14,7 @@
1414 #include "codegen/H/io/argv_builtin.h"
1515 #include "codegen/H/builtins/type_builtin.h"
1616 #include "codegen/H/builtins/exit_builtin.h"
17+#include "codegen/H/builtins/mem_builtin.h"
1718 #include "codegen/H/builtins/proc_builtins.h"
1819 #include "parser/H/core/parser.h"
1920
@@ -119,6 +120,14 @@ int codegen_try_builtin_call(const char *name, int len) {
119120 if (len == 9 && strncmp(name, "ends_with", 9) == 0) { codegen_builtin_ends_with(); return 1; }
120121 if (len == 6 && strncmp(name, "repeat", 6) == 0) { codegen_builtin_repeat(); return 1; }
121122 if (len == 4 && strncmp(name, "exit", 4) == 0) { codegen_builtin_exit(); return 1; }
123+ if (len == 8 && strncmp(name, "mem_free", 8) == 0) { codegen_builtin_mem_free(); return 1; }
124+ if (len == 14 && strncmp(name, "mem_free_elems", 14) == 0) { codegen_builtin_mem_free_elems(); return 1; }
125+ if (len == 12 && strncmp(name, "mem_free_map", 12) == 0) { codegen_builtin_mem_free_map(); return 1; }
126+ if (len == 12 && strncmp(name, "mem_free_obj", 12) == 0) { codegen_builtin_mem_free_obj(); return 1; }
127+ if (len == 12 && strncmp(name, "mem_free_arr", 12) == 0) { codegen_builtin_mem_free_arr(); return 1; }
128+ if (len == 11 && strncmp(name, "mem_newarr1", 11) == 0) { codegen_builtin_mem_newarr1(); return 1; }
129+ if (len == 11 && strncmp(name, "mem_release", 11) == 0) { codegen_builtin_mem_release(); return 1; }
130+ if (len == 7 && strncmp(name, "mem_cat", 7) == 0) { codegen_builtin_mem_cat(); return 1; }
122131 if (len == 8 && strncmp(name, "sys_fork", 8) == 0) { codegen_builtin_sys_fork(); return 1; }
123132 if (len == 8 && strncmp(name, "sys_wait", 8) == 0) { codegen_builtin_sys_wait(); return 1; }
124133 if (len == 8 && strncmp(name, "sys_exec", 8) == 0) { codegen_builtin_sys_exec(); return 1; }
@@ -265,7 +274,7 @@ int codegen_builtin_arity(const char *name, int len) {
265274 {"type", 1}, {"chr", 1}, {"ord", 1}, {"pop", 1}, {"insert", 3}, {"slice", 3}, {"sort", 1},
266275 {"rename_file", 2}, {"delete_file", 1}, {"make_dir", 1}, {"list_dir", 1},
267276 {"contains", 2}, {"starts_with", 2}, {"ends_with", 2}, {"repeat", 2}, {"exit", 1}, {"argv", 0},
268- {"sys_fork", 0}, {"sys_wait", 1}, {"sys_exec", 2},
277+ {"sys_fork", 0}, {"sys_wait", 1}, {"sys_exec", 2}, {"mem_free", 1}, {"mem_cat", 3}, {"mem_release", 1}, {"mem_free_arr", 1}, {"mem_free_obj", 1}, {"mem_free_map", 1}, {"mem_free_elems", 1}, {"mem_newarr1", 2},
269278 };
270279 for (size_t i = 0; i < sizeof(table) / sizeof(table[0]); i++) {
271280 if ((int)strlen(table[i].name) == len && strncmp(table[i].name, name, (size_t)len) == 0) return table[i].arity;

hearth/kiln/src/codegen/C/builtins/mem_builtin.cadded

+311−0
@@ -0,0 +1,311 @@
1+#include "codegen/H/builtins/mem_builtin.h"
2+#include "codegen/H/expressions/expr.h"
3+#include "codegen/H/emit/emit.h"
4+#include "codegen/H/emit/emit_sse.h"
5+#include "codegen/H/emit/value.h"
6+#include "codegen/H/runtime/heap.h"
7+#include "codegen/H/strings/strings.h"
8+#include "codegen/H/collections/arrays.h"
9+#include "codegen/H/collections/maps.h"
10+#include "parser/H/core/parser.h"
11+
12+// mem_release(x): System.free(x) written by the program. kiln cannot tell what kind of block x is (only the
13+// Strings the compiler itself makes by + or str() are known to be recyclable), so it evaluates x and ignores it.
14+void codegen_builtin_mem_release(void) {
15+ codegen_expression();
16+ expect(TOKEN_RPAREN, "expected ')' after argument");
17+ emit_pop_reg(code, REG_RAX);
18+ emit_pop_reg(code, REG_RBX);
19+ emit_mov_reg_imm64(code, REG_RBX, TAG_NIL);
20+ emit_push_reg(code, REG_RBX);
21+ emit_push_reg(code, REG_RBX);
22+}
23+
24+static void push_nil(void) {
25+ emit_mov_reg_imm64(code, REG_RBX, TAG_NIL);
26+ emit_push_reg(code, REG_RBX);
27+ emit_push_reg(code, REG_RBX);
28+}
29+
30+void codegen_builtin_mem_free(void) {
31+ codegen_expression();
32+ expect(TOKEN_RPAREN, "expected ')' after argument");
33+ emit_pop_reg(code, REG_RAX); // payload
34+ emit_pop_reg(code, REG_RBX); // tag
35+ if (heap_free_lists_on()) {
36+ emit_cmp_reg_imm32(code, REG_RBX, TAG_STRING);
37+ int not_string = emit_jcc_rel32(code, COND_NE);
38+ heap_emit_free_string_rax(code);
39+ emit_patch_jump(code, not_string);
40+ }
41+ push_nil();
42+}
43+
44+// Frees the String whose payload is in RAX when bit `bit` of the flags in RDX is set.
45+// Keeps RDI and the stack as they were; clobbers RAX, RCX, RDX (when it frees), RSI.
46+static void free_if_flag(int bit) {
47+ emit_mov_reg_reg(code, REG_RCX, REG_RDX);
48+ emit_mov_reg_imm64(code, REG_RSI, (uint64_t)bit);
49+ emit_and_reg_reg(code, REG_RCX, REG_RSI);
50+ emit_cmp_reg_imm32(code, REG_RCX, 0);
51+ int skip = emit_jcc_rel32(code, COND_E);
52+ heap_emit_free_string_rax(code);
53+ emit_patch_jump(code, skip);
54+}
55+
56+void codegen_builtin_mem_cat(void) {
57+ codegen_expression();
58+ expect(TOKEN_COMMA, "expected ',' after argument");
59+ codegen_expression();
60+ expect(TOKEN_COMMA, "expected ',' after argument");
61+ codegen_expression();
62+ expect(TOKEN_RPAREN, "expected ')' after argument");
63+ emit_pop_reg(code, REG_RDX); // flags payload (a double)
64+ emit_pop_reg(code, REG_RBX); // tag
65+ emit_movq_xmm_from_reg(code, XMM0, REG_RDX);
66+ emit_cvttsd2si(code, REG_RDX, XMM0);
67+ emit_pop_reg(code, REG_RCX); // b payload
68+ emit_pop_reg(code, REG_RBX);
69+ emit_pop_reg(code, REG_RAX); // a payload
70+ emit_pop_reg(code, REG_RBX);
71+ if (!heap_free_lists_on()) {
72+ codegen_string_concat(); // operands stay where they are
73+ return;
74+ }
75+ emit_push_reg(code, REG_RAX); // a
76+ emit_push_reg(code, REG_RCX); // b
77+ emit_push_reg(code, REG_RDX); // flags
78+ codegen_string_concat(); // pushes the result (tag, payload)
79+ emit_pop_reg(code, REG_RDI); // result payload
80+ emit_pop_reg(code, REG_RBX); // result tag (a String)
81+ emit_pop_reg(code, REG_RDX); // flags
82+ emit_pop_reg(code, REG_RCX); // b
83+ emit_pop_reg(code, REG_RAX); // a
84+ emit_push_reg(code, REG_RDI); // result
85+ emit_push_reg(code, REG_RDX); // flags: the free code clobbers RDX
86+ emit_push_reg(code, REG_RCX); // b
87+ free_if_flag(1); // a is in RAX
88+ emit_pop_reg(code, REG_RAX); // b
89+ emit_pop_reg(code, REG_RDX); // flags
90+ free_if_flag(2);
91+ emit_pop_reg(code, REG_RDI); // result payload
92+ emit_mov_reg_imm64(code, REG_RBX, TAG_STRING);
93+ emit_push_reg(code, REG_RBX);
94+ emit_push_reg(code, REG_RDI);
95+}
96+
97+// mem_free_arr(x): gives an array (its object and element block) back to the allocator when the program was
98+// compiled with array free lists; otherwise evaluated and ignored.
99+void codegen_builtin_mem_free_arr(void) {
100+ codegen_expression();
101+ expect(TOKEN_RPAREN, "expected ')' after argument");
102+ emit_pop_reg(code, REG_RAX); // payload
103+ emit_pop_reg(code, REG_RBX); // tag
104+ if (heap_free_arrays_on()) {
105+ emit_cmp_reg_imm32(code, REG_RBX, TAG_ARRAY);
106+ int not_array = emit_jcc_rel32(code, COND_NE);
107+ heap_emit_free_array_rax(code);
108+ emit_patch_jump(code, not_array);
109+ }
110+ push_nil();
111+}
112+
113+// mem_newarr1(n, d): a new array of max(n, 0) copies of d with exactly that capacity, so freeing it gives
114+// back everything it allocated (the push loop it replaces leaves its abandoned growth blocks behind).
115+// Scratch layout, base in RSI: [0]=d tag [8]=d payload [16]=n [24]=object [32]=data block.
116+void codegen_builtin_mem_newarr1(void) {
117+ codegen_expression();
118+ expect(TOKEN_COMMA, "expected ',' after argument");
119+ codegen_expression();
120+ expect(TOKEN_RPAREN, "expected ')' after argument");
121+ emit_pop_reg(code, REG_RAX); // d payload
122+ emit_pop_reg(code, REG_RBX); // d tag
123+ emit_pop_reg(code, REG_RCX); // n payload (a double)
124+ emit_pop_reg(code, REG_RDX); // n tag
125+ emit_movq_xmm_from_reg(code, XMM0, REG_RCX);
126+ emit_cvttsd2si(code, REG_RCX, XMM0);
127+ emit_cmp_reg_imm32(code, REG_RCX, 0);
128+ int positive = emit_jcc_rel32(code, COND_GT);
129+ emit_mov_reg_imm64(code, REG_RCX, 0);
130+ emit_patch_jump(code, positive);
131+ emit_sub_reg_imm8(code, REG_RSP, 40);
132+ emit_mov_reg_reg(code, REG_RSI, REG_RSP);
133+ emit_store_mem_disp32(code, REG_RSI, 0, REG_RBX);
134+ emit_store_mem_disp32(code, REG_RSI, 8, REG_RAX);
135+ emit_store_mem_disp32(code, REG_RSI, 16, REG_RCX);
136+
137+ emit_mov_reg_reg(code, REG_RDI, REG_RCX);
138+ emit_mov_reg_imm64(code, REG_RAX, 16);
139+ emit_imul_reg_reg(code, REG_RDI, REG_RAX); // data size = n * 16
140+ emit_cmp_reg_imm32(code, REG_RDI, 0);
141+ int sized = emit_jcc_rel32(code, COND_NE);
142+ emit_mov_reg_imm64(code, REG_RDI, 1); // an empty array still owns a 1-byte block, like `[]`
143+ emit_patch_jump(code, sized);
144+ emit_push_reg(code, REG_RSI);
145+ heap_emit_alloc_recyclable(code); // RAX = data block
146+ emit_pop_reg(code, REG_RSI);
147+ emit_store_mem_disp32(code, REG_RSI, 32, REG_RAX);
148+
149+ emit_mov_reg_imm64(code, REG_RDI, ARRAY_OBJECT_SIZE);
150+ emit_push_reg(code, REG_RSI);
151+ heap_emit_alloc_recyclable(code); // RAX = array object
152+ emit_pop_reg(code, REG_RSI);
153+ emit_store_mem_disp32(code, REG_RSI, 24, REG_RAX);
154+
155+ emit_load_mem_disp32(code, REG_RBX, REG_RSI, 32); // fill: RBX = element cursor, RCX = remaining
156+ emit_load_mem_disp32(code, REG_RCX, REG_RSI, 16);
157+ int loop = code->count;
158+ emit_cmp_reg_imm32(code, REG_RCX, 0);
159+ int filled = emit_jcc_rel32(code, COND_E);
160+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 0);
161+ emit_store_mem_disp32(code, REG_RBX, 0, REG_RDX);
162+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 8);
163+ emit_store_mem_disp32(code, REG_RBX, 8, REG_RDX);
164+ emit_add_reg_imm8(code, REG_RBX, 16);
165+ emit_dec_reg(code, REG_RCX);
166+ emit_jmp_back(code, loop);
167+ emit_patch_jump(code, filled);
168+
169+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 24); // object
170+ emit_load_mem_disp32(code, REG_RCX, REG_RSI, 16);
171+ emit_store_mem_disp32(code, REG_RAX, 0, REG_RCX); // capacity = n
172+ emit_store_mem_disp32(code, REG_RAX, 8, REG_RCX); // count = n
173+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 32);
174+ emit_store_mem_disp32(code, REG_RAX, 16, REG_RDX); // data_ptr
175+ emit_add_reg_imm8(code, REG_RSP, 40);
176+ emit_mov_reg_imm64(code, REG_RBX, TAG_ARRAY);
177+ emit_push_reg(code, REG_RBX);
178+ emit_push_reg(code, REG_RAX);
179+}
180+
181+// mem_free_obj(x): gives an object (a map made by a map literal: its entries block and itself) back to the
182+// allocator when the program was compiled with block free lists; otherwise evaluated and ignored.
183+void codegen_builtin_mem_free_obj(void) {
184+ codegen_expression();
185+ expect(TOKEN_RPAREN, "expected ')' after argument");
186+ emit_pop_reg(code, REG_RAX); // payload
187+ emit_pop_reg(code, REG_RBX); // tag
188+ if (heap_free_arrays_on()) {
189+ emit_cmp_reg_imm32(code, REG_RBX, TAG_MAP);
190+ int not_map = emit_jcc_rel32(code, COND_NE);
191+ heap_emit_free_map_rax(code);
192+ emit_patch_jump(code, not_map);
193+ }
194+ push_nil();
195+}
196+
197+// mem_free_map(x): a HashMap / HashSet built by `{}`: the [key, value, next] entry arrays the prelude keeps as its
198+// values (each one an array literal, so object and element block go back), then the map's entries block and the
199+// map object. Keys and the program's own values are not touched. Without block free lists: evaluated and ignored.
200+// Scratch frame (RSP-relative through a copy, since freeing clobbers registers): [0]=cursor [8]=remaining.
201+void codegen_builtin_mem_free_map(void) {
202+ codegen_expression();
203+ expect(TOKEN_RPAREN, "expected ')' after argument");
204+ emit_pop_reg(code, REG_RAX); // payload
205+ emit_pop_reg(code, REG_RBX); // tag
206+ if (!heap_free_arrays_on()) { push_nil(); return; }
207+ emit_cmp_reg_imm32(code, REG_RBX, TAG_MAP);
208+ int not_map = emit_jcc_rel32(code, COND_NE);
209+
210+ emit_push_reg(code, REG_RAX); // the map object, kept for the final free
211+ emit_sub_reg_imm8(code, REG_RSP, 16);
212+ emit_mov_reg_reg(code, REG_RSI, REG_RSP);
213+ emit_load_mem_disp32(code, REG_RCX, REG_RAX, 16); // entries_ptr
214+ emit_store_mem_disp32(code, REG_RSI, 0, REG_RCX);
215+ emit_load_mem_disp32(code, REG_RCX, REG_RAX, 8); // count
216+ emit_store_mem_disp32(code, REG_RSI, 8, REG_RCX);
217+
218+ int entry_loop = code->count;
219+ emit_mov_reg_reg(code, REG_RSI, REG_RSP);
220+ emit_load_mem_disp32(code, REG_RCX, REG_RSI, 8);
221+ emit_cmp_reg_imm32(code, REG_RCX, 0);
222+ int all_done = emit_jcc_rel32(code, COND_E);
223+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 0); // cursor
224+ emit_load_mem_disp32(code, REG_RAX, REG_RDX, 16); // value tag
225+ emit_cmp_reg_imm32(code, REG_RAX, TAG_ARRAY);
226+ int not_chain = emit_jcc_rel32(code, COND_NE); // the element count is a number
227+ emit_load_mem_disp32(code, REG_RAX, REG_RDX, 24); // chain head: an entry array
228+
229+ int chain_loop = code->count;
230+ emit_cmp_reg_imm32(code, REG_RAX, 0);
231+ int chain_end = emit_jcc_rel32(code, COND_E);
232+ emit_load_mem_disp32(code, REG_RCX, REG_RAX, 8); // an entry array has exactly [key, value, next]
233+ emit_cmp_reg_imm32(code, REG_RCX, 3);
234+ int chain_odd = emit_jcc_rel32(code, COND_NE);
235+ emit_load_mem_disp32(code, REG_RCX, REG_RAX, 16); // its element block
236+ emit_load_mem_disp32(code, REG_RDX, REG_RCX, 40); // next payload
237+ emit_load_mem_disp32(code, REG_RCX, REG_RCX, 32); // next tag
238+ emit_push_reg(code, REG_RDX);
239+ emit_push_reg(code, REG_RCX);
240+ heap_emit_free_array_rax(code); // RAX = this entry array
241+ emit_pop_reg(code, REG_RCX); // next tag
242+ emit_pop_reg(code, REG_RAX); // next payload
243+ emit_cmp_reg_imm32(code, REG_RCX, TAG_ARRAY);
244+ emit_jcc_back(code, COND_E, chain_loop);
245+ emit_patch_jump(code, chain_end);
246+ emit_patch_jump(code, chain_odd);
247+ emit_patch_jump(code, not_chain);
248+
249+ emit_mov_reg_reg(code, REG_RSI, REG_RSP); // next entry
250+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 0);
251+ emit_add_reg_imm8(code, REG_RAX, MAP_ENTRY_SIZE);
252+ emit_store_mem_disp32(code, REG_RSI, 0, REG_RAX);
253+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8);
254+ emit_dec_reg(code, REG_RAX);
255+ emit_store_mem_disp32(code, REG_RSI, 8, REG_RAX);
256+ emit_jmp_back(code, entry_loop);
257+
258+ emit_patch_jump(code, all_done);
259+ emit_add_reg_imm8(code, REG_RSP, 16);
260+ emit_pop_reg(code, REG_RAX); // the map object
261+ heap_emit_free_map_rax(code);
262+ emit_patch_jump(code, not_map);
263+ push_nil();
264+}
265+
266+// mem_free_elems(x): the Strings of a String[] / List<String> whose elements are all fresh concatenations only
267+// it references (front/src/jown) go back to the allocator; the array itself is freed by mem_free_arr afterwards.
268+// Anything else, and programs without free lists, is evaluated and ignored.
269+// Scratch frame (RSP-relative through a copy, since freeing clobbers registers): [0]=cursor [8]=remaining.
270+void codegen_builtin_mem_free_elems(void) {
271+ codegen_expression();
272+ expect(TOKEN_RPAREN, "expected ')' after argument");
273+ emit_pop_reg(code, REG_RAX); // payload
274+ emit_pop_reg(code, REG_RBX); // tag
275+ if (!heap_free_lists_on()) { push_nil(); return; }
276+ emit_cmp_reg_imm32(code, REG_RBX, TAG_ARRAY);
277+ int not_array = emit_jcc_rel32(code, COND_NE);
278+
279+ emit_sub_reg_imm8(code, REG_RSP, 16);
280+ emit_mov_reg_reg(code, REG_RSI, REG_RSP);
281+ emit_load_mem_disp32(code, REG_RCX, REG_RAX, 16); // data_ptr
282+ emit_store_mem_disp32(code, REG_RSI, 0, REG_RCX);
283+ emit_load_mem_disp32(code, REG_RCX, REG_RAX, 8); // count
284+ emit_store_mem_disp32(code, REG_RSI, 8, REG_RCX);
285+
286+ int loop = code->count;
287+ emit_mov_reg_reg(code, REG_RSI, REG_RSP);
288+ emit_load_mem_disp32(code, REG_RCX, REG_RSI, 8);
289+ emit_cmp_reg_imm32(code, REG_RCX, 0);
290+ int done = emit_jcc_rel32(code, COND_E);
291+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 0); // cursor
292+ emit_load_mem_disp32(code, REG_RAX, REG_RDX, 0); // element tag
293+ emit_cmp_reg_imm32(code, REG_RAX, TAG_STRING);
294+ int not_string = emit_jcc_rel32(code, COND_NE); // none and the like stay
295+ emit_load_mem_disp32(code, REG_RAX, REG_RDX, 8); // the String's payload
296+ heap_emit_free_string_rax(code);
297+ emit_patch_jump(code, not_string);
298+ emit_mov_reg_reg(code, REG_RSI, REG_RSP);
299+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 0);
300+ emit_add_reg_imm8(code, REG_RAX, 16);
301+ emit_store_mem_disp32(code, REG_RSI, 0, REG_RAX);
302+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8);
303+ emit_dec_reg(code, REG_RAX);
304+ emit_store_mem_disp32(code, REG_RSI, 8, REG_RAX);
305+ emit_jmp_back(code, loop);
306+
307+ emit_patch_jump(code, done);
308+ emit_add_reg_imm8(code, REG_RSP, 16);
309+ emit_patch_jump(code, not_array);
310+ push_nil();
311+}

hearth/kiln/src/codegen/C/builtins/str_builtin.cmodified

+1−1
@@ -35,7 +35,7 @@ void codegen_builtin_str(void) {
3535 // ---- heap-allocate a length-prefixed block and copy the digits in ----
3636 emit_mov_reg_reg(code, REG_RDI, REG_RDX);
3737 emit_add_reg_imm8(code, REG_RDI, 8);
38- heap_emit_alloc(code); // RAX = new block, clobbers RSI
38+ heap_emit_alloc_str(code); // RAX = new block, clobbers RSI
3939
4040 emit_store_mem_disp32(code, REG_RAX, 0, REG_RDX); // length prefix
4141 emit_mov_reg_reg(code, REG_RSI, REG_RAX); // stash block addr (bytes_emit_copy preserves RSI)

hearth/kiln/src/codegen/C/collections/arrays.cmodified

+3−3
@@ -19,7 +19,7 @@ void codegen_array_literal(void) {
1919
2020 // elements block first (its address becomes the object's data_ptr)
2121 emit_mov_reg_imm64(code, REG_RDI, (uint64_t)(count > 0 ? count * 16 : 1));
22- heap_emit_alloc(code); // RAX = elements block
22+ heap_emit_alloc_recyclable(code); // RAX = elements block
2323 emit_mov_reg_reg(code, REG_RSI, REG_RAX); // stash: RSI survives the pop loop below
2424
2525 // Elements were pushed left to right, so the last one is on top --
@@ -34,7 +34,7 @@ void codegen_array_literal(void) {
3434 // now the stable array object
3535 emit_push_reg(code, REG_RSI); // save elements block address across this alloc
3636 emit_mov_reg_imm64(code, REG_RDI, ARRAY_OBJECT_SIZE);
37- heap_emit_alloc(code); // RAX = array object
37+ heap_emit_alloc_recyclable(code); // RAX = array object
3838 emit_pop_reg(code, REG_RSI);
3939
4040 emit_mov_reg_imm64(code, REG_RCX, (uint64_t)count);
@@ -143,7 +143,7 @@ void codegen_builtin_push(void) {
143143
144144 emit_push_reg(code, REG_RCX); // save scratch base across the call
145145 emit_push_reg(code, REG_RAX); // save new_capacity too (heap_emit_alloc doesn't touch the real stack)
146- heap_emit_alloc(code); // RAX = new data block
146+ heap_emit_alloc_recyclable(code); // RAX = new data block
147147 emit_pop_reg(code, REG_RDX); // RDX = new_capacity
148148 emit_pop_reg(code, REG_RCX); // scratch base
149149

hearth/kiln/src/codegen/C/collections/maps.cmodified

+2−2
@@ -25,7 +25,7 @@ void codegen_map_literal(void) {
2525 expect(TOKEN_RBRACE, "expected '}' after map literal");
2626
2727 emit_mov_reg_imm64(code, REG_RDI, (uint64_t)(count > 0 ? count * MAP_ENTRY_SIZE : 1));
28- heap_emit_alloc(code); // RAX = entries block
28+ heap_emit_alloc_recyclable(code); // RAX = entries block
2929 emit_mov_reg_reg(code, REG_RSI, REG_RAX); // stash across the pop loop
3030
3131 // Pairs were pushed key-then-value, left to right, so the LAST
@@ -44,7 +44,7 @@ void codegen_map_literal(void) {
4444
4545 emit_push_reg(code, REG_RSI); // save entries block across this alloc
4646 emit_mov_reg_imm64(code, REG_RDI, MAP_OBJECT_SIZE);
47- heap_emit_alloc(code); // RAX = map object
47+ heap_emit_alloc_recyclable(code); // RAX = map object
4848 emit_pop_reg(code, REG_RSI);
4949
5050 emit_mov_reg_imm64(code, REG_RCX, (uint64_t)count);

hearth/kiln/src/codegen/C/collections/maps_mutate.cmodified

+14−1
@@ -61,7 +61,7 @@ void codegen_map_index_store(void) {
6161 emit_mov_reg_reg(code, REG_RDI, REG_RAX);
6262 emit_mov_reg_imm64(code, REG_RBX, MAP_ENTRY_SIZE);
6363 emit_imul_reg_reg(code, REG_RDI, REG_RBX);
64- heap_emit_alloc(code); // RAX = new entries block
64+ heap_emit_alloc_recyclable(code); // RAX = new entries block
6565 emit_mov_reg_reg(code, REG_RCX, REG_RSP);
6666 emit_store_mem_disp32(code, REG_RCX, 40, REG_RAX);
6767
@@ -75,6 +75,19 @@ void codegen_map_index_store(void) {
7575 emit_mov_reg_reg(code, REG_RDX, REG_RAX); // byte count
7676 bytes_emit_copy(code);
7777
78+ if (heap_free_arrays_on()) {
79+ // The first insert into an empty map literal leaves its 16-byte block behind; nobody can be reading
80+ // an empty block, so it goes back (blocks of non-empty maps are kept: a loop may still hold them).
81+ emit_mov_reg_reg(code, REG_RCX, REG_RSP);
82+ emit_load_mem_disp32(code, REG_RSI, REG_RCX, 24);
83+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 0); // old capacity
84+ emit_cmp_reg_imm32(code, REG_RDX, 0);
85+ int keep_old = emit_jcc_rel32(code, COND_NE);
86+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 16); // old entries block
87+ heap_emit_free_empty_block_rax(code);
88+ emit_patch_jump(code, keep_old);
89+ }
90+
7891 emit_mov_reg_reg(code, REG_RCX, REG_RSP);
7992 emit_load_mem_disp32(code, REG_RSI, REG_RCX, 24);
8093 emit_load_mem_disp32(code, REG_RAX, REG_RCX, 32);

hearth/kiln/src/codegen/C/emit/layout.cmodified

+5−0
@@ -54,3 +54,8 @@ uint64_t kiln_try_handlers_addr(void) {
5454 if (is_linux_shared()) return KILN_DYN_GLOBALS_ADDR + 8 + KILN_HEAP_LIMIT_SIZE + KILN_TRY_DEPTH_SIZE;
5555 return KILN_TRY_HANDLERS_ADDR;
5656 }
57+
58+uint64_t kiln_freelist_addr(void) {
59+ if (kiln_get_target() == KILN_TARGET_WINDOWS) return KILN_PE_FREELIST_ADDR;
60+ return KILN_FREELIST_ADDR; // only meaningful for the static layouts; heap_enable_free_lists() guards every use
61+}

hearth/kiln/src/codegen/C/runtime/heap.cmodified

+145−1
@@ -3,6 +3,7 @@
33 #include "codegen/H/platform/win_call.h"
44 #include "elf/H/elf_dynamic_call.h"
55 #include "app/H/target.h"
6+#include "codegen/H/emit/value.h"
67
78 // Chunk-based growth, replacing the old single fixed 16MB grab: the arena
89 // starts with one chunk and the alloc routine below grows it by another
@@ -47,6 +48,123 @@ static int alloc_routine_offset = -1;
4748 // RBX/RCX/RDX (borrowed as scratch) around that one-off syscall/API call,
4849 // instead of changing the contract for the other ~26 sites that never hit
4950 // the slow path at all.
51+// ---- optional free lists (static Linux builds of programs that free; see heap.h) ----
52+#define FL_MAX_BLOCK 512 // blocks up to this size are recycled, larger ones are only bump-allocated
53+#define FL_ROUND_MASK (~(uint64_t)15)
54+
55+static int free_lists_on;
56+static int arrays_on;
57+
58+void heap_enable_free_lists(int blocks) {
59+ free_lists_on = kiln_get_link_mode() == KILN_LINK_STATIC && !kiln_is_compiling_so(); // Linux and Windows static builds
60+ arrays_on = free_lists_on && blocks;
61+}
62+
63+int heap_free_arrays_on(void) { return arrays_on; }
64+
65+int heap_free_lists_on(void) { return free_lists_on; }
66+
67+// Start of the string alloc routine: rounds the request up to 16 bytes (every string block is then a whole
68+// size class, so a freed one can serve any request of its class) and pops a recycled block of that class if
69+// there is one; otherwise falls through with the rounded size in RDI.
70+// In: RDI = size. Out: RAX = block (recycled path only). Clobbers RSI, RDI like the alloc routine.
71+static void emit_freelist_pop(CodeBuf *code) {
72+ emit_add_reg_imm8(code, REG_RDI, 15);
73+ emit_mov_reg_imm64(code, REG_RSI, FL_ROUND_MASK);
74+ emit_and_reg_reg(code, REG_RDI, REG_RSI);
75+ emit_cmp_reg_imm32(code, REG_RDI, FL_MAX_BLOCK);
76+ int too_big = emit_jcc_rel32(code, COND_A);
77+ emit_mov_reg_reg(code, REG_RSI, REG_RDI);
78+ emit_sar_reg_by1(code, REG_RSI); // size / 2 = class * 8
79+ emit_mov_reg_imm64(code, REG_RAX, kiln_freelist_addr());
80+ emit_add_reg_reg(code, REG_RSI, REG_RAX); // RSI = &head of this class
81+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 0); // RAX = head
82+ emit_cmp_reg_imm32(code, REG_RAX, 0);
83+ int empty = emit_jcc_rel32(code, COND_E);
84+ emit_load_mem_disp32(code, REG_RDI, REG_RAX, 0); // RDI = next free block
85+ emit_store_mem_disp32(code, REG_RSI, 0, REG_RDI); // head = next
86+ emit_ret(code);
87+ emit_patch_jump(code, empty);
88+ emit_patch_jump(code, too_big);
89+}
90+
91+// Puts the block starting at RAX, `size` bytes (RDX, a multiple of 16) on the free list of its class.
92+// Does nothing for blocks above FL_MAX_BLOCK. Clobbers RAX, RCX, RDX, RSI.
93+static void emit_free_block(CodeBuf *code) {
94+ emit_cmp_reg_imm32(code, REG_RDX, FL_MAX_BLOCK);
95+ int done = emit_jcc_rel32(code, COND_A);
96+ emit_cmp_reg_imm32(code, REG_RDX, 16);
97+ int tiny = emit_jcc_rel32(code, COND_B);
98+ emit_mov_reg_reg(code, REG_RSI, REG_RDX);
99+ emit_sar_reg_by1(code, REG_RSI);
100+ emit_mov_reg_imm64(code, REG_RCX, kiln_freelist_addr());
101+ emit_add_reg_reg(code, REG_RSI, REG_RCX); // RSI = &head
102+ emit_load_mem_disp32(code, REG_RCX, REG_RSI, 0);
103+ emit_store_mem_disp32(code, REG_RAX, 0, REG_RCX); // block.next = head
104+ emit_store_mem_disp32(code, REG_RSI, 0, REG_RAX); // head = block
105+ emit_patch_jump(code, tiny);
106+ emit_patch_jump(code, done);
107+}
108+
109+// A string payload in RAX (a heap block made by the concatenation or str() code, whose 8-byte length
110+// prefix sits right before it) goes back on the free list of its class. The class is computed from the
111+// length; the block is at least that big, so reusing it for that class is always in bounds.
112+// Clobbers RAX, RCX, RDX, RSI. Does nothing for blocks above FL_MAX_BLOCK.
113+void heap_emit_free_string_rax(CodeBuf *code) {
114+ emit_load_mem_disp32(code, REG_RDX, REG_RAX, -8); // length
115+ emit_add_reg_imm8(code, REG_RDX, 8 + 15);
116+ emit_mov_reg_imm64(code, REG_RSI, FL_ROUND_MASK);
117+ emit_and_reg_reg(code, REG_RDX, REG_RSI); // size class
118+ emit_sub_reg_imm8(code, REG_RAX, 8); // block start
119+ emit_free_block(code);
120+}
121+
122+// RAX = an array object made by the array literal / mem_newarr1 code while the array free lists were on
123+// (its 24 bytes were allocated as a 32-byte class block); its element block is capacity * 16 bytes (16 for
124+// capacity 0). Every array block is allocated through heap_emit_alloc_recyclable, so both are whole classes.
125+// Both go back on the free lists. Clobbers RAX, RCX, RDX, RSI.
126+void heap_emit_free_array_rax(CodeBuf *code) {
127+ emit_push_reg(code, REG_RAX);
128+ emit_load_mem_disp32(code, REG_RDX, REG_RAX, 0); // capacity
129+ emit_mov_reg_imm64(code, REG_RCX, 16);
130+ emit_imul_reg_reg(code, REG_RDX, REG_RCX); // element block size
131+ emit_cmp_reg_imm32(code, REG_RDX, 0);
132+ int sized = emit_jcc_rel32(code, COND_NE);
133+ emit_mov_reg_imm64(code, REG_RDX, 16); // an empty array's 1-byte block was rounded up to 16
134+ emit_patch_jump(code, sized);
135+ emit_load_mem_disp32(code, REG_RAX, REG_RAX, 16); // data_ptr
136+ emit_free_block(code);
137+ emit_pop_reg(code, REG_RAX); // the object itself
138+ emit_mov_reg_imm64(code, REG_RDX, 32);
139+ emit_free_block(code);
140+}
141+
142+// RAX = the 16-byte block an empty map literal / array literal was given (a 1-byte request rounded up);
143+// puts it back. Clobbers RAX, RCX, RDX, RSI.
144+void heap_emit_free_empty_block_rax(CodeBuf *code) {
145+ emit_mov_reg_imm64(code, REG_RDX, 16);
146+ emit_free_block(code);
147+}
148+
149+// RAX = a map object made by the map literal code while the free lists for blocks were on (an object of a
150+// plain class is one: its fields are all in the literal, so it never grows); entries block = capacity * 32
151+// bytes (16 for capacity 0). Both go back on the free lists. Clobbers RAX, RCX, RDX, RSI.
152+void heap_emit_free_map_rax(CodeBuf *code) {
153+ emit_push_reg(code, REG_RAX);
154+ emit_load_mem_disp32(code, REG_RDX, REG_RAX, 0); // capacity
155+ emit_mov_reg_imm64(code, REG_RCX, 32);
156+ emit_imul_reg_reg(code, REG_RDX, REG_RCX);
157+ emit_cmp_reg_imm32(code, REG_RDX, 0);
158+ int sized = emit_jcc_rel32(code, COND_NE);
159+ emit_mov_reg_imm64(code, REG_RDX, 16);
160+ emit_patch_jump(code, sized);
161+ emit_load_mem_disp32(code, REG_RAX, REG_RAX, 16); // entries_ptr
162+ emit_free_block(code);
163+ emit_pop_reg(code, REG_RAX);
164+ emit_mov_reg_imm64(code, REG_RDX, 32);
165+ emit_free_block(code);
166+}
167+
50168 static void emit_alloc_routine(CodeBuf *code) {
51169 int skip = emit_jmp_rel32(code);
52170 alloc_routine_offset = code->count;
@@ -112,6 +230,29 @@ static void emit_alloc_routine(CodeBuf *code) {
112230 emit_patch_jump(code, skip);
113231 }
114232
233+// Only strings are ever freed, so only their allocations (concatenation, str()) go through this routine;
234+// arrays, maps and everything else keep calling the plain bump routine, which is byte-for-byte the old one.
235+static int str_alloc_offset = -1;
236+
237+static void emit_str_alloc_routine(CodeBuf *code) {
238+ int skip = emit_jmp_rel32(code);
239+ str_alloc_offset = code->count;
240+ emit_freelist_pop(code);
241+ emit_call_back(code, alloc_routine_offset); // nothing to recycle: bump-allocate the rounded size
242+ emit_ret(code);
243+ emit_patch_jump(code, skip);
244+}
245+
246+void heap_emit_alloc_recyclable(CodeBuf *code) {
247+ if (arrays_on && str_alloc_offset >= 0) emit_call_back(code, str_alloc_offset);
248+ else heap_emit_alloc(code);
249+}
250+
251+void heap_emit_alloc_str(CodeBuf *code) {
252+ if (free_lists_on && str_alloc_offset >= 0) emit_call_back(code, str_alloc_offset);
253+ else heap_emit_alloc(code);
254+}
255+
115256 // Called by the dedicated "compile the runtime" driver (see
116257 // elf/C/elf_so_writer.c) to emit this routine into libkilnrt.so's OWN code
117258 // buffer, completely separate from any executable's compile_program.
@@ -194,7 +335,10 @@ void heap_emit_startup(CodeBuf *code) {
194335 emit_mov_reg_imm64(code, REG_RSI, kiln_heap_limit_addr());
195336 emit_store_mem_disp32(code, REG_RSI, 0, REG_RDI); // heap_limit = base + one chunk
196337
197- if (!is_shared()) emit_alloc_routine(code);
338+ if (!is_shared()) {
339+ emit_alloc_routine(code);
340+ if (free_lists_on) emit_str_alloc_routine(code);
341+ }
198342 }
199343
200344 void heap_emit_alloc(CodeBuf *code) {

hearth/kiln/src/codegen/C/strings/strings.cmodified

+1−1
@@ -79,7 +79,7 @@ void codegen_string_concat(void) {
7979 emit_add_reg_imm8(code, REG_RDI, 8); // +8 for the new string's own length prefix
8080
8181 emit_push_reg(code, REG_RSI); // heap_emit_alloc clobbers RSI -- save/restore around it
82- heap_emit_alloc(code); // RAX = new block
82+ heap_emit_alloc_str(code); // RAX = new block (a String: kiln may free it later)
8383 emit_pop_reg(code, REG_RSI);
8484 emit_store_mem_disp32(code, REG_RSI, 32, REG_RAX);
8585

hearth/kiln/src/codegen/D/builtins/builtins.dmodified

+2−0
@@ -10,6 +10,7 @@ src/codegen/O/builtins/builtins.o: src/codegen/C/builtins/builtins.c \
1010 src/codegen/H/io/input_builtin.h src/codegen/H/io/argv_builtin.h \
1111 src/codegen/H/builtins/type_builtin.h \
1212 src/codegen/H/builtins/exit_builtin.h \
13+ src/codegen/H/builtins/mem_builtin.h \
1314 src/codegen/H/builtins/proc_builtins.h src/parser/H/core/parser.h \
1415 src/codegen/H/emit/emit.h
1516 src/codegen/H/builtins/builtins.h:
@@ -28,6 +29,7 @@ src/codegen/H/io/input_builtin.h:
2829 src/codegen/H/io/argv_builtin.h:
2930 src/codegen/H/builtins/type_builtin.h:
3031 src/codegen/H/builtins/exit_builtin.h:
32+src/codegen/H/builtins/mem_builtin.h:
3133 src/codegen/H/builtins/proc_builtins.h:
3234 src/parser/H/core/parser.h:
3335 src/codegen/H/emit/emit.h:

hearth/kiln/src/codegen/D/builtins/mem_builtin.dadded

+19−0
@@ -0,0 +1,19 @@
1+src/codegen/O/builtins/mem_builtin.o: \
2+ src/codegen/C/builtins/mem_builtin.c \
3+ src/codegen/H/builtins/mem_builtin.h src/codegen/H/expressions/expr.h \
4+ ../front/src/ir/H/ir_lex.h src/codegen/H/emit/emit.h \
5+ src/codegen/H/emit/emit_sse.h src/codegen/H/emit/value.h \
6+ src/codegen/H/runtime/heap.h src/codegen/H/strings/strings.h \
7+ src/codegen/H/collections/arrays.h src/codegen/H/collections/maps.h \
8+ src/parser/H/core/parser.h
9+src/codegen/H/builtins/mem_builtin.h:
10+src/codegen/H/expressions/expr.h:
11+../front/src/ir/H/ir_lex.h:
12+src/codegen/H/emit/emit.h:
13+src/codegen/H/emit/emit_sse.h:
14+src/codegen/H/emit/value.h:
15+src/codegen/H/runtime/heap.h:
16+src/codegen/H/strings/strings.h:
17+src/codegen/H/collections/arrays.h:
18+src/codegen/H/collections/maps.h:
19+src/parser/H/core/parser.h:

hearth/kiln/src/codegen/D/runtime/heap.dmodified

+2−1
@@ -7,7 +7,7 @@ src/codegen/O/runtime/heap.o: src/codegen/C/runtime/heap.c \
77 src/elf/H/elf_dynamic_call.h src/elf/H/elf_dynamic.h \
88 src/codegen/H/emit/ash_gpu_import_enum.h \
99 src/codegen/H/emit/ash_gpu_exports.def src/elf/H/elf_writer.h \
10- src/app/H/target.h
10+ src/app/H/target.h src/codegen/H/emit/value.h
1111 src/codegen/H/runtime/heap.h:
1212 src/codegen/H/emit/emit.h:
1313 src/codegen/H/emit/layout.h:
@@ -23,3 +23,4 @@ src/codegen/H/emit/ash_gpu_import_enum.h:
2323 src/codegen/H/emit/ash_gpu_exports.def:
2424 src/elf/H/elf_writer.h:
2525 src/app/H/target.h:
26+src/codegen/H/emit/value.h:

hearth/kiln/src/codegen/H/builtins/mem_builtin.hadded

+34−0
@@ -0,0 +1,34 @@
1+#ifndef KILN_MEM_BUILTIN_H
2+#define KILN_MEM_BUILTIN_H
3+
4+// Memory builtins of the IR (front/src/jown + jlower emit them, System.free lowers to mem_free).
5+// Both self-parse their arguments, starting right after the already-consumed '('.
6+//
7+// mem_release(x): System.free(x) from the program: evaluated and ignored (see mem_builtin.c).
8+void codegen_builtin_mem_release(void);
9+
10+// mem_free(x): gives a String back to the allocator (heap.h free lists). Other values, and any program
11+// that did not enable the free lists (Windows, --link=shared), are evaluated and ignored: the bump heap
12+// keeps them until exit, like before. Result: none.
13+void codegen_builtin_mem_free(void);
14+
15+// mem_free_arr(x): the same for an array made by an array literal / mem_newarr1 (object and element block).
16+void codegen_builtin_mem_free_arr(void);
17+
18+// mem_free_obj(x): the same for an object (a plain class instance: map object and entries block).
19+void codegen_builtin_mem_free_obj(void);
20+
21+// mem_free_map(x): a HashMap / HashSet (map object, entries block and the prelude's entry arrays).
22+void codegen_builtin_mem_free_map(void);
23+
24+// mem_free_elems(x): the fresh Strings inside a String[] / List<String> (see jown); the array itself is not freed.
25+void codegen_builtin_mem_free_elems(void);
26+
27+// mem_newarr1(n, d): `new T[n]` for one dimension, filled with d, with exactly n elements of capacity.
28+void codegen_builtin_mem_newarr1(void);
29+
30+// mem_cat(a, b, flags): a + b for two Strings, then frees the operands that were temporaries
31+// (flags bit 0 = a, bit 1 = b), so `"x" + str(i)` does not leave the str(i) text behind. Result: the String.
32+void codegen_builtin_mem_cat(void);
33+
34+#endif

hearth/kiln/src/codegen/H/emit/layout.hmodified

+1−0
@@ -16,5 +16,6 @@ uint64_t kiln_heap_ptr_addr(void);
1616 uint64_t kiln_heap_limit_addr(void);
1717 uint64_t kiln_try_depth_addr(void);
1818 uint64_t kiln_try_handlers_addr(void);
19+uint64_t kiln_freelist_addr(void); // static Linux only: the table of free-list heads (33 slots, 8 bytes each)
1920
2021 #endif

hearth/kiln/src/codegen/H/emit/runtime_layout.hmodified

+4−0
@@ -18,6 +18,10 @@
1818 #define KILN_TRY_DEPTH_SIZE 8
1919
2020
21+// Heads of the free lists of the static builds' allocator (see codegen/C/runtime/heap.c): one 8-byte slot per
22+// 16-byte size class, 16..512 bytes, slot 0 unused. Zero in the file, so every list starts empty.
23+#define KILN_FREELIST_SIZE (33 * 8)
24+
2125 // Module-level state: top-level `local`s of an imported file whose value is
2226 // computed at import time live in a fixed table of (tag, payload) slots in the
2327 // globals region (see parser/C/declarations/module_state.c), because a called

hearth/kiln/src/codegen/H/runtime/heap.hmodified

+24−0
@@ -14,6 +14,27 @@
1414 // whole lifetime, so a pointer handed out by an earlier allocation is
1515 // never invalidated by a later growth step.
1616
17+// Free lists (static Linux builds only, and only for programs that free: the driver calls
18+// heap_enable_free_lists() after lowering when the program contains mem_free / mem_cat). Then the alloc
19+// string alloc routine (heap_emit_alloc_str) rounds every request to 16 bytes and recycles freed blocks of up
20+// to 512 bytes through one list per size class (heads in the globals region, kiln_freelist_addr()). The plain
21+// routine (heap_emit_alloc) is never touched, so arrays, maps and programs that never free cost exactly what
22+// they always did. Strings are the only thing kiln frees (heap_emit_free_string_rax).
23+void heap_enable_free_lists(int blocks); // blocks: the program also frees arrays or objects (mem_free_arr / mem_newarr1 / mem_free_obj)
24+int heap_free_lists_on(void);
25+int heap_free_arrays_on(void);
26+// RAX = payload of a heap string; puts its block on the free list. Clobbers RAX, RCX, RDX, RSI.
27+void heap_emit_free_string_rax(CodeBuf *code);
28+// RAX = an array object (see heap.c for what that requires); frees it and its element block.
29+void heap_emit_free_array_rax(CodeBuf *code);
30+// RAX = the rounded 1-byte block of an empty literal; puts it back. Clobbers RAX, RCX, RDX, RSI.
31+void heap_emit_free_empty_block_rax(CodeBuf *code);
32+// RAX = a map object (an object of a plain class); frees it and its entries block.
33+void heap_emit_free_map_rax(CodeBuf *code);
34+// Allocation of an array or map object or its block: rounded to whole size classes and served from the free
35+// lists when the program frees arrays, else the plain routine (exactly as before).
36+void heap_emit_alloc_recyclable(CodeBuf *code);
37+
1738 // Emitted once, at the very start of the generated program (before the
1839 // top-level statement loop): grabs the first chunk and records its start
1940 // as the heap bump pointer at KILN_GLOBALS_ADDR (see elf/H/elf_writer.h),
@@ -38,6 +59,9 @@ int heap_alloc_routine_offset(void);
3859 // heap_emit_startup) instead of re-emitting the routine's body inline at
3960 // every one of its ~27 call sites.
4061 void heap_emit_alloc(CodeBuf *code);
62+// Same contract, for the two places that make the Strings kiln can free (concatenation, str()); falls back to
63+// heap_emit_alloc when the free lists are off.
64+void heap_emit_alloc_str(CodeBuf *code);
4165
4266 // Arena-style checkpoint/reset for short-lived temporaries (built for
4367 // Pyre's own lowering, e.g. a tight inner loop's scratch buffers that

hearth/kiln/src/elf/H/elf_writer.hmodified

+2−1
@@ -34,13 +34,14 @@
3434 // inside it (see codegen/C/io/argv_builtin.c).
3535 #define KILN_ARGV_SIZE 8
3636
37-#define KILN_GLOBALS_SIZE (8 + KILN_HEAP_LIMIT_SIZE + KILN_TRY_DEPTH_SIZE + KILN_TRY_HANDLERS_SIZE + KILN_ARGV_SIZE + KILN_MODSTATE_SIZE) // heap_ptr, heap_limit, try_depth, handler stack, argv, module state
37+#define KILN_GLOBALS_SIZE (8 + KILN_HEAP_LIMIT_SIZE + KILN_TRY_DEPTH_SIZE + KILN_TRY_HANDLERS_SIZE + KILN_ARGV_SIZE + KILN_MODSTATE_SIZE + KILN_FREELIST_SIZE) // heap_ptr, heap_limit, try_depth, handler stack, argv, module state, free lists
3838 #define KILN_GLOBALS_ADDR (KILN_LOAD_BASE + KILN_EHDR_SIZE + KILN_PHDR_SIZE)
3939 #define KILN_HEAP_LIMIT_ADDR (KILN_GLOBALS_ADDR + 8)
4040 #define KILN_TRY_DEPTH_ADDR (KILN_HEAP_LIMIT_ADDR + KILN_HEAP_LIMIT_SIZE)
4141 #define KILN_TRY_HANDLERS_ADDR (KILN_TRY_DEPTH_ADDR + KILN_TRY_DEPTH_SIZE)
4242 #define KILN_ARGV_ADDR (KILN_TRY_HANDLERS_ADDR + KILN_TRY_HANDLERS_SIZE)
4343 #define KILN_MODSTATE_ADDR (KILN_ARGV_ADDR + KILN_ARGV_SIZE)
44+#define KILN_FREELIST_ADDR (KILN_MODSTATE_ADDR + KILN_MODSTATE_SIZE)
4445 #define KILN_CODE_START_OFFSET (KILN_EHDR_SIZE + KILN_PHDR_SIZE + KILN_GLOBALS_SIZE)
4546
4647 // Writes `machine_code` as a minimal, standalone, directly-executable

hearth/kiln/src/pe/H/pe_writer.hmodified

+4−1
@@ -36,7 +36,10 @@
3636 // Linux's argv() which has to snapshot the kernel's original RSP before
3737 // anything else touches it (see codegen/C/io/argv_builtin.c).
3838 #define KILN_PE_MODSTATE_ADDR (KILN_PE_TRY_HANDLERS_ADDR + KILN_TRY_HANDLERS_SIZE)
39-#define KILN_PE_GLOBALS_SIZE (8 + KILN_HEAP_LIMIT_SIZE + KILN_TRY_DEPTH_SIZE + KILN_TRY_HANDLERS_SIZE + KILN_MODSTATE_SIZE)
39+// Free-list heads of the allocator (codegen/C/runtime/heap.c), after the module state: 33 slots of 8 bytes, zero
40+// in the file. Only the static build uses them; the shared build leaves them empty.
41+#define KILN_PE_FREELIST_ADDR (KILN_PE_MODSTATE_ADDR + KILN_MODSTATE_SIZE)
42+#define KILN_PE_GLOBALS_SIZE (8 + KILN_HEAP_LIMIT_SIZE + KILN_TRY_DEPTH_SIZE + KILN_TRY_HANDLERS_SIZE + KILN_MODSTATE_SIZE + KILN_FREELIST_SIZE)
4043
4144 // Every kernel32.dll function kiln's Windows codegen calls -- single list
4245 // in pe_imports.def, shared by this enum and pe_writer.c's actual table

hearth/kiln/tests/cases/memoff/FreeIt.ashadded

+11−0
@@ -0,0 +1,11 @@
1+public class FreeIt {
2+ public static void main(String[] args) {
3+ long total = 0;
4+ for (int i = 0; i < 100000; i++) {
5+ String s = "x" + i;
6+ total += s.length();
7+ System.free(s);
8+ }
9+ System.out.println(total);
10+ }
11+}

hearth/kiln/tests/cases/memoff/FreeIt.outadded

+1−0
@@ -0,0 +1 @@
1+588890

hearth/kiln/tests/cases/memoff/ash.pkgadded

+3−0
@@ -0,0 +1,3 @@
1+name: o
2+version: 0.1.0
3+memory-safety: off

hearth/kiln/tests/cases/ownership/OwnBorrow.ashadded

+14−0
@@ -0,0 +1,14 @@
1+public class OwnBorrow {
2+ static int size(String s) { return s.length(); }
3+ static void append(int[] xs, int[] ys) { xs[0] = ys[0]; }
4+
5+ public static void main(String[] args) {
6+ String a = "abc";
7+ int n = size(&a) + size(&a); // shared borrows may overlap
8+ int[] xs = new int[2];
9+ int[] ys = new int[2];
10+ append(&mut xs, &ys); // different variables: fine
11+ append(&mut xs, &mut ys);
12+ System.out.println(n + a + xs.length);
13+ }
14+}

hearth/kiln/tests/cases/ownership/OwnBorrow.outadded

+1−0
@@ -0,0 +1 @@
1+6abc2

hearth/kiln/tests/cases/ownership/OwnBorrowInfer.ashadded

+31−0
@@ -0,0 +1,31 @@
1+public class OwnBorrowInfer {
2+ static class Box {
3+ String held;
4+ void put(String s) { held = s; } // keeps its argument: passing moves
5+ }
6+
7+ static int size(String s) { return s.length(); } // only reads: the caller keeps ownership
8+ static int sum(int[] xs) {
9+ int t = 0;
10+ for (int x : xs) t += x;
11+ return t;
12+ }
13+ static int twice(String s) { return size(s) + size(s); } // passes it on to a reader: still a reader
14+ static String same(String s) { return s; } // returns the caller's value: moves
15+
16+ public static void main(String[] args) {
17+ long total = 0;
18+ for (int i = 0; i < 20000; i++) {
19+ String a = "item" + i;
20+ int[] xs = {i, 1, 2};
21+ total += size(a) + size(a) + twice(a) + sum(xs) + sum(xs); // no move, and a / xs are still freed each iteration
22+ }
23+ String keep = "kept";
24+ Box b = new Box();
25+ b.put(keep);
26+ System.out.println(total + " " + b.held);
27+ String r = "r" + total;
28+ String back = same(r);
29+ System.out.println(back);
30+ }
31+}

hearth/kiln/tests/cases/ownership/OwnBorrowInfer.outadded

+2−0
@@ -0,0 +1,2 @@
1+400775560 kept
2+r400775560

hearth/kiln/tests/cases/ownership/OwnBorrowTricky.ashadded

+56−0
@@ -0,0 +1,56 @@
1+import ash.util.ArrayList;
2+import ash.util.List;
3+
4+public class OwnBorrowTricky {
5+ static class Animal {
6+ String name = "animal";
7+ String sound() { return "..."; }
8+ }
9+ static class Dog extends Animal {
10+ static String last;
11+ String sound() { last = name; return "woof"; } // an override that keeps a field: parameters of type Animal may be Dogs
12+ }
13+ static class Point {
14+ int x;
15+ int y;
16+ Point(int x, int y) { this.x = x; this.y = y; }
17+ }
18+
19+ static List<String> sink = new ArrayList<>();
20+
21+ static int reads(String s) { return s.length(); }
22+ static void keeps(String s) { sink.add(s); } // passes it to library code that keeps it
23+ static int lam(String s) {
24+ Runnable r = () -> sink.add(s); // captured by a lambda
25+ r.run();
26+ return 1;
27+ }
28+ static int chain(String s, int depth) {
29+ if (depth == 0) return s.length();
30+ return chain(s, depth - 1); // recursion: treated as escaping, never freed by the caller
31+ }
32+ static int pointSum(Point p) { return p.x + p.y; } // reads fields only: a plain class with no subclass is fine
33+ static String call(Animal a) { return a.sound(); } // may run Dog.sound: never borrowed
34+ static int overwrite(String s) {
35+ s = s + "!"; // the parameter is a copy: the caller's value is untouched
36+ return s.length();
37+ }
38+
39+ public static void main(String[] args) {
40+ long total = 0;
41+ for (int i = 0; i < 2000; i++) {
42+ String a = "a" + i;
43+ String b = "b" + i;
44+ String c = "c" + i;
45+ String d = "d" + i;
46+ String e = "e" + i;
47+ Point p = new Point(i, 1);
48+ total += reads(a) + reads(a) + overwrite(b) + chain(e, 3) + pointSum(p);
49+ keeps(c); // c now lives in sink
50+ total += lam(d); // d now lives in sink
51+ total += reads(b) + a.length();
52+ }
53+ Animal pet = new Dog();
54+ System.out.println(total + " " + sink.size() + " " + sink.get(1999) + " " + call(pet) + " " + Dog.last);
55+ }
56+}

hearth/kiln/tests/cases/ownership/OwnBorrowTricky.outadded

+1−0
@@ -0,0 +1 @@
1+2058340 4000 d999 woof animal

hearth/kiln/tests/cases/ownership/OwnFree.ashadded

+18−0
@@ -0,0 +1,18 @@
1+public class OwnFree {
2+ public static void main(String[] args) {
3+ long total = 0;
4+ for (int i = 0; i < 300000; i++) {
5+ String s = "item-" + i; // fresh String, sole owner: freed at the end of each iteration
6+ int[] xs = new int[16]; // fresh array, sole owner
7+ xs[3] = i;
8+ String t = s + "!";
9+ t += s; // reassigned with a fresh value: the old one is freed first
10+ if (i % 100000 == 0) {
11+ System.out.println(t);
12+ continue; // continue frees what the iteration owns
13+ }
14+ total += s.length() + xs.length + t.length() + xs[3] % 7;
15+ }
16+ System.out.println(total);
17+ }
18+}

hearth/kiln/tests/cases/ownership/OwnFree.outadded

+4−0
@@ -0,0 +1,4 @@
1+item-0!item-0
2+item-100000!item-100000
3+item-200000!item-200000
4+15566524

hearth/kiln/tests/cases/ownership/OwnFreeAlias.ashadded

+49−0
@@ -0,0 +1,49 @@
1+import ash.util.ArrayList;
2+import ash.util.List;
3+
4+public class OwnFreeAlias {
5+ static class A {
6+ String get(int i) { return "a" + i; } // fresh here...
7+ }
8+ static class B extends A {
9+ String shared = "shared";
10+ String get(int i) { return shared; } // ...but an override returns an alias: calls through A are never fresh
11+ }
12+
13+ static class Box {
14+ String held;
15+ void keep(String s) { held = s; } // keeps its argument
16+ }
17+
18+ static String grow(int n) {
19+ if (n == 0) return "";
20+ String rest = grow(n - 1); // recursive: never treated as fresh
21+ return rest + n;
22+ }
23+
24+ public static void main(String[] args) {
25+ long total = 0;
26+ List<String> kept = new ArrayList<>();
27+ Box box = new Box();
28+ for (int i = 0; i < 20000; i++) {
29+ A a = (i % 2 == 0) ? new A() : new B();
30+ String g = a.get(i); // virtual: must stay alive when it is B's field
31+ String t = "t" + i;
32+ String u = "u" + i;
33+ bool same = t.equals(u) || t.contains("1"); // reads only: t and u stay sole owners
34+ String v = (i % 3 == 0) ? t : u; // aliases: t and u must not be freed
35+ kept.add("k" + i); // fresh temp handed to a collection: never freed here
36+ if (i % 5000 == 0) box.keep("b" + i); // the argument outlives the iteration
37+ String w = grow(3);
38+ total += g.length() + v.length() + (same ? 1 : 0) + w.length();
39+ int[] row = new int[4];
40+ for (int x : row) total += x; // for-each over a sole-owner array
41+ switch (i % 3) {
42+ case 0 -> { String z = "z" + i; total += z.length(); }
43+ case 1 -> { int[] y = new int[2]; total += y.length; }
44+ default -> total += 1;
45+ }
46+ }
47+ System.out.println(total + " " + kept.size() + " " + kept.get(19999) + " " + box.held + " " + new B().shared);
48+ }
49+}

hearth/kiln/tests/cases/ownership/OwnFreeAlias.outadded

+1−0
@@ -0,0 +1 @@
1+353070 20000 k19999 b15000 shared

hearth/kiln/tests/cases/ownership/OwnFreeElems.ashadded

+60−0
@@ -0,0 +1,60 @@
1+import ash.util.ArrayList;
2+import ash.util.List;
3+
4+public class OwnFreeElems {
5+ static class Bag {
6+ List<String> names = new ArrayList<>(); // owned, only fresh adds, only harmless reads: its Strings die with the Bag
7+ void add(int i) { names.add("n" + i); }
8+ int total() {
9+ int t = 0;
10+ for (int i = 0; i < names.size(); i++) t += names.get(i).length();
11+ return t;
12+ }
13+ }
14+
15+ static String last; // keeps an element alive past its list
16+
17+ public static void main(String[] args) {
18+ long total = 0;
19+ for (int i = 0; i < 20000; i++) {
20+ List<String> a = new ArrayList<>(); // fresh adds + harmless reads: elements freed with the list
21+ a.add("a" + i);
22+ a.add("b" + i);
23+ total += a.get(0).length() + a.size();
24+ if (i % 10000 == 0) System.out.println(a.get(1));
25+ for (String s : a) total += s.length(); // the loop variable is only read
26+
27+ List<String> b = new ArrayList<>(); // a literal element: the list is freed, the literal never is
28+ b.add("lit");
29+ b.add("v" + i);
30+ total += b.get(0).length();
31+
32+ List<String> c = new ArrayList<>(); // an element escapes through a local alias
33+ c.add("c" + i);
34+ String kept = c.get(0);
35+ if (i == 7) last = kept;
36+
37+ String[] d = new String[2]; // fresh stores only
38+ d[0] = "d" + i;
39+ d[1] = "e" + i;
40+ total += d[0].length() + d[1].length();
41+
42+ String[] e = new String[2]; // a shared store
43+ String shared = "shared" + i;
44+ total += shared.length();
45+ e[0] = shared;
46+ e[1] = "f" + i;
47+ total += e[0].length();
48+
49+ List<String> f = new ArrayList<>(); // loop variable escapes
50+ f.add("g" + i);
51+ for (String s : f) { if (i == 9) last = s; }
52+
53+ Bag bag = new Bag();
54+ bag.add(i);
55+ bag.add(2);
56+ total += bag.total();
57+ }
58+ System.out.println(total + " " + last);
59+ }
60+}

hearth/kiln/tests/cases/ownership/OwnFreeElems.outadded

+3−0
@@ -0,0 +1,3 @@
1+b0
2+b10000
3+1211120 g9

hearth/kiln/tests/cases/ownership/OwnFreeFactory.ashadded

+47−0
@@ -0,0 +1,47 @@
1+public class OwnFreeFactory {
2+ static class Point {
3+ int x;
4+ int y;
5+ Point(int x, int y) { this.x = x; this.y = y; }
6+ int sum() { return x + y; }
7+ }
8+
9+ static class Shape {
10+ int w = 3;
11+ int area() { return w * w; }
12+ }
13+
14+ static class Square extends Shape {
15+ int area() { return w * w * 2; } // Shape has a subclass: its instances are never assumed exact
16+ }
17+
18+ static Point make(int i) {
19+ Point p = new Point(i, 1); // sole owner, handed to the caller
20+ p.x = p.x + 1;
21+ return p;
22+ }
23+
24+ static Point origin() { return new Point(0, 0); }
25+
26+ static Shape shape(int i) {
27+ if (i % 2 == 0) return new Shape();
28+ return new Square(); // two classes: never fresh
29+ }
30+
31+ static Point cached = new Point(5, 5);
32+ static Point shared() {
33+ return cached; // an alias of a static: never fresh
34+ }
35+
36+ public static void main(String[] args) {
37+ long total = 0;
38+ for (int i = 0; i < 100000; i++) {
39+ Point a = make(i); // freed each iteration
40+ Point b = origin(); // freed each iteration
41+ Shape s = shape(i);
42+ Point c = shared(); // alias: must survive
43+ total += a.sum() + b.sum() + s.area() + c.sum();
44+ }
45+ System.out.println(total + " " + shared().sum());
46+ }
47+}

hearth/kiln/tests/cases/ownership/OwnFreeFactory.outadded

+1−0
@@ -0,0 +1 @@
1+5002500000 10

hearth/kiln/tests/cases/ownership/OwnFreeFields.ashadded

+32−0
@@ -0,0 +1,32 @@
1+public class OwnFreeFields {
2+ static class Buffer {
3+ int[] cells = new int[4]; // fresh initializer, only indexed: owned
4+ String label; // only ever given fresh concatenations: owned
5+ String shared = "shared"; // a literal initializer is shared: never owned
6+ int[] borrowed; // assigned from a parameter: never owned
7+ String leaked; // returned by a getter: never owned
8+
9+ Buffer(int n, int[] given) {
10+ label = "buf" + n;
11+ borrowed = given;
12+ leaked = "leak" + n;
13+ cells[1] = n;
14+ }
15+ void relabel(int n) { label = label + n; cells = new int[2]; } // reassigning an owned field frees its old value
16+ int total() { return cells[0] + cells[1] + label.length() + borrowed.length + shared.length(); }
17+ String leak() { return leaked; } // lets the caller hold the field's value
18+ }
19+
20+ public static void main(String[] args) {
21+ String kept = "";
22+ long sum = 0;
23+ for (int i = 0; i < 20000; i++) {
24+ int[] given = {1, 2, 3}; // moved into the Buffer, which keeps it in a field
25+ Buffer b = new Buffer(i, given); // freed each iteration together with cells and label
26+ b.relabel(i);
27+ sum += b.total();
28+ if (i == 7) kept = b.leak(); // aliases leaked: it must survive b
29+ }
30+ System.out.println(sum + " " + kept);
31+ }
32+}

hearth/kiln/tests/cases/ownership/OwnFreeFields.outadded

+1−0
@@ -0,0 +1 @@
1+417780 leak7

hearth/kiln/tests/cases/ownership/OwnFreeFlow.ashadded

+44−0
@@ -0,0 +1,44 @@
1+public class OwnFreeFlow {
2+ static int keep(String s) { return s.length(); }
3+
4+ static String build(int n) {
5+ String acc = "";
6+ for (int i = 0; i < n; i++) {
7+ acc += i; // sole owner, reassigned with a fresh value each time
8+ if (acc.length() > 6) break; // break frees acc's loop-scoped siblings only
9+ }
10+ String out = acc + "|" + n; // fresh; escapes through return below, so it is never freed
11+ return out;
12+ }
13+
14+ static int early(int n) {
15+ String a = "x" + n;
16+ int[] xs = {n, n + 1, n + 2};
17+ if (n > 5) return xs[1] + a.length(); // return frees a and xs after computing the value
18+ String b = a + a;
19+ return b.length() + xs.length;
20+ }
21+
22+ public static void main(String[] args) {
23+ System.out.println(build(3) + " " + build(9));
24+ System.out.println(early(2) + " " + early(7));
25+ String shared = "s" + args.length;
26+ int k = keep(&shared) + keep(&shared); // lent to a call: not a sole-owner use, never freed
27+ String again = shared; // moved
28+ Runnable r = () -> System.out.println(again + k);
29+ r.run();
30+ for (int i = 0; i < 3; i++) {
31+ String line = "line " + i;
32+ String twice = line + line;
33+ int[] box = new int[2];
34+ box[1] = twice.length();
35+ while (true) {
36+ String inner = twice + box[1];
37+ System.out.println(inner);
38+ break; // break frees inner only, not line/twice/box
39+ }
40+ if (i == 1) continue;
41+ System.out.println(line + box.length);
42+ }
43+ }
44+}

hearth/kiln/tests/cases/ownership/OwnFreeFlow.outadded

+8−0
@@ -0,0 +1,8 @@
1+012|3 0123456|9
2+7 10
3+s04
4+line 0line 012
5+line 02
6+line 1line 112
7+line 2line 212
8+line 22

hearth/kiln/tests/cases/ownership/OwnFreeLists.ashadded

+48−0
@@ -0,0 +1,48 @@
1+import ash.util.ArrayList;
2+import ash.util.List;
3+
4+public class OwnFreeLists {
5+ static class Bag {
6+ List<Integer> items = new ArrayList<>(); // owned: only filled and read through list methods
7+ List<Integer> view; // assigned from elsewhere: shared
8+ void put(int v) { items.add(v); }
9+ int sum() {
10+ int t = 0;
11+ for (int i = 0; i < items.size(); i++) t += items.get(i);
12+ return t;
13+ }
14+ }
15+
16+ static List<Integer> build(int n) {
17+ List<Integer> out = new ArrayList<>();
18+ for (int i = 0; i < n; i++) out.add(i);
19+ return out; // handed to the caller
20+ }
21+
22+ static List<Integer> keep;
23+
24+ public static void main(String[] args) {
25+ long total = 0;
26+ for (int i = 0; i < 20000; i++) {
27+ List<Integer> xs = new ArrayList<>(); // sole owner: freed each iteration
28+ xs.add(i);
29+ xs.add(i + 1);
30+ xs.set(0, 5);
31+ total += xs.size() + xs.get(1);
32+ List<Integer> ys = build(3); // fresh from a factory
33+ for (int y : ys) total += y;
34+ List<Integer> zs = new ArrayList<>(); // passed on: not freed
35+ zs.add(7);
36+ if (i == 3) keep = zs;
37+ Bag b = new Bag();
38+ b.put(i);
39+ b.put(2);
40+ total += b.sum();
41+ List<Integer> ws = new ArrayList<>();
42+ ws.add(1);
43+ List<Integer> sub = ws.subList(0, 1); // a copy / view: ws is not a sole owner of what sub sees
44+ total += sub.size();
45+ }
46+ System.out.println(total + " " + keep.size() + " " + keep.get(0));
47+ }
48+}

hearth/kiln/tests/cases/ownership/OwnFreeLists.outadded

+1−0
@@ -0,0 +1 @@
1+400160000 1 7

hearth/kiln/tests/cases/ownership/OwnFreeLoops.ashadded

+85−0
@@ -0,0 +1,85 @@
1+public class OwnFreeLoops {
2+ static int classify(int i) {
3+ switch (i % 4) {
4+ case 0:
5+ String z = "zero" + i;
6+ return z.length();
7+ case 1: {
8+ int[] t = new int[i % 5 + 1];
9+ return t.length;
10+ }
11+ default:
12+ return -1;
13+ }
14+ }
15+
16+ static String nested(int n) {
17+ String out = "";
18+ for (int i = 0; i < n; i++) {
19+ String row = "r" + i;
20+ int[] cells = new int[3];
21+ for (int j = 0; j < 3; j++) {
22+ String cell = row + "c" + j;
23+ if (j == 1) continue;
24+ cells[j] = cell.length();
25+ switch (j) {
26+ case 2:
27+ if (i == 1) break;
28+ out += cell;
29+ break;
30+ default:
31+ out += "-";
32+ }
33+ }
34+ if (i == 2) break;
35+ out += cells[0] + ";";
36+ }
37+ return out;
38+ }
39+
40+ static int caught(int n) {
41+ String a = "a" + n;
42+ try {
43+ String b = a + "b";
44+ if (n == 1) throw new RuntimeException(b);
45+ return b.length();
46+ } catch (RuntimeException e) {
47+ String c = a + "c";
48+ return c.length() + 1;
49+ } finally {
50+ String d = a + "d";
51+ System.out.println("fin " + d);
52+ }
53+ }
54+
55+ static int whileLoop() {
56+ int total = 0;
57+ int i = 0;
58+ while (i < 5) {
59+ String s = "w" + i;
60+ i++;
61+ if (s.length() > 100) continue;
62+ do {
63+ int[] k = new int[2];
64+ total += k.length + s.length();
65+ } while (false);
66+ }
67+ return total;
68+ }
69+
70+ public static void main(String[] args) {
71+ long sum = 0;
72+ for (int i = 0; i < 8; i++) sum += classify(i);
73+ System.out.println(sum);
74+ System.out.println(nested(5));
75+ System.out.println(caught(0) + " " + caught(1) + " " + caught(2));
76+ System.out.println(whileLoop());
77+ String[] words = {"p" + 1, "q" + 2, "r" + 3};
78+ String joined = "";
79+ for (String w : words) {
80+ String up = w + "!";
81+ joined += up;
82+ }
83+ System.out.println(joined + words.length);
84+ }
85+}

hearth/kiln/tests/cases/ownership/OwnFreeLoops.outadded

+8−0
@@ -0,0 +1,8 @@
1+9
2+-r0c24;-4;-r2c2
3+fin a0d
4+fin a1d
5+fin a2d
6+3 4 3
7+20
8+p1!q2!r3!3

hearth/kiln/tests/cases/ownership/OwnFreeMaps.ashadded

+54−0
@@ -0,0 +1,54 @@
1+import ash.util.ArrayList;
2+import ash.util.HashMap;
3+import ash.util.HashSet;
4+import ash.util.List;
5+import ash.util.Map;
6+import ash.util.Set;
7+
8+public class OwnFreeMaps {
9+ static class Index {
10+ Map<String, Integer> counts = new HashMap<>(); // owned field
11+ void see(String w) {
12+ Integer? n = counts.get(w);
13+ counts.put(w, n == null ? 1 : n + 1);
14+ }
15+ int distinct() { return counts.size(); }
16+ }
17+
18+ static Map<String, Integer> shared = new HashMap<>();
19+
20+ static int lookup(Map<String, Integer> m, String k) { return m.containsKey(k) ? 1 : 0; } // reads only
21+
22+ public static void main(String[] args) {
23+ long total = 0;
24+ Map<String, Integer> kept = new HashMap<>();
25+ for (int i = 0; i < 5000; i++) {
26+ Map<String, Integer> m = new HashMap<>(); // sole owner: freed with its entry chains each iteration
27+ m.put("a" + i, i);
28+ m.put("b", 2);
29+ m.put("b", 3);
30+ total += m.size() + (m.containsKey("b") ? 1 : 0);
31+ if (m.containsKey("a" + i)) total += 1;
32+ m.remove("b");
33+ List<String> ks = m.keys();
34+ total += ks.size();
35+ Set<Integer> s = new HashSet<>();
36+ s.add(i % 7);
37+ s.add(3);
38+ s.add(3);
39+ total += s.size();
40+ if (s.contains(3)) total += 1;
41+ total += lookup(m, "a" + i); // a callee that only reads: still a sole owner
42+ Index ix = new Index();
43+ ix.see("x");
44+ ix.see("x");
45+ ix.see("y");
46+ total += ix.distinct();
47+ Map<String, Integer> k2 = new HashMap<>();
48+ k2.put("k", i);
49+ if (i == 4) kept = k2; // escapes: not freed
50+ if (i == 5) shared = m;
51+ }
52+ System.out.println(total + " " + kept.get("k") + " " + shared.size());
53+ }
54+}

hearth/kiln/tests/cases/ownership/OwnFreeMaps.outadded

+1−0
@@ -0,0 +1 @@
1+54286 4 1

hearth/kiln/tests/cases/ownership/OwnFreeObj.ashadded

+36−0
@@ -0,0 +1,36 @@
1+public class OwnFreeObj {
2+ static class Point {
3+ int x;
4+ int y;
5+ Point(int x, int y) { this.x = x; this.y = y; }
6+ int sum() { return x + y; }
7+ void bump() { x = x + 1; }
8+ }
9+
10+ static class Leaky {
11+ int v;
12+ Leaky(int v) { this.v = v; }
13+ Leaky self() { return this; } // `this` escapes: instances are never freed
14+ }
15+
16+ static Point kept;
17+
18+ public static void main(String[] args) {
19+ long total = 0;
20+ for (int i = 0; i < 200000; i++) {
21+ Point p = new Point(i, 2); // sole owner: freed at the end of each iteration
22+ p.bump();
23+ Point q = new Point(1, 1);
24+ q.x = p.sum();
25+ if (i % 100000 == 0) {
26+ System.out.println(q.sum());
27+ continue;
28+ }
29+ Leaky l = new Leaky(i);
30+ Leaky same = l.self(); // l is not a sole owner
31+ if (i == 5) kept = new Point(7, 8);
32+ total += q.sum() + same.v % 3;
33+ }
34+ System.out.println(total + " " + kept.sum());
35+ }
36+}

hearth/kiln/tests/cases/ownership/OwnFreeObj.outadded

+3−0
@@ -0,0 +1,3 @@
1+4
2+100004
3+20000799990 15

hearth/kiln/tests/cases/ownership/OwnFreeReturn.ashadded

+32−0
@@ -0,0 +1,32 @@
1+public class OwnFreeReturn {
2+ static String label(int i) {
3+ String s = "n" + i; // sole owner handed to the caller by `return s`
4+ return s;
5+ }
6+
7+ static String pick(int i) {
8+ if (i % 2 == 0) return "even" + i;
9+ return "odd" + i; // both returns are fresh
10+ }
11+
12+ static String name = "shared";
13+ static String alias(int i) { return name; } // not fresh: callers must never free it
14+
15+ static int[] table(int n) {
16+ int[] t = new int[n];
17+ t[0] = n;
18+ return t;
19+ }
20+
21+ public static void main(String[] args) {
22+ long total = 0;
23+ for (int i = 0; i < 200000; i++) {
24+ String a = label(i); // fresh result: sole owner, freed each iteration
25+ String b = pick(i);
26+ String c = alias(i); // aliases a static: never freed
27+ int[] t = table(8);
28+ total += a.length() + b.length() + c.length() + t[0];
29+ }
30+ System.out.println(total + " " + name);
31+ }
32+}

hearth/kiln/tests/cases/ownership/OwnFreeReturn.outadded

+1−0
@@ -0,0 +1 @@
1+5877780 shared

hearth/kiln/tests/cases/ownership/OwnFreeTry.ashadded

+61−0
@@ -0,0 +1,61 @@
1+public class OwnFreeTry {
2+ static int guarded(int n) {
3+ String s = "guard" + n;
4+ int[] box = new int[2];
5+ try {
6+ if (n > 2) return s.length(); // finally still reads s after the return value is computed
7+ box[0] = n;
8+ } finally {
9+ System.out.println("finally " + s + " " + box.length);
10+ }
11+ return -1;
12+ }
13+
14+ static void loopy() {
15+ for (int i = 0; i < 3; i++) {
16+ String s = "loop" + i;
17+ try {
18+ if (i == 1) continue; // finally runs after continue
19+ if (i == 2) break;
20+ System.out.println("body " + s);
21+ } finally {
22+ System.out.println("fin " + s);
23+ }
24+ }
25+ }
26+
27+ static int thrower(int n) {
28+ String s = "t" + n;
29+ try {
30+ if (n > 0) throw new RuntimeException("boom");
31+ return s.length();
32+ } catch (RuntimeException e) {
33+ System.out.println("caught " + s); // s must still be alive in the handler
34+ return -2;
35+ }
36+ }
37+
38+ static String pickCase(int k) {
39+ switch (k) {
40+ case 0: {
41+ String a = "zero" + k;
42+ return a + "!";
43+ }
44+ case 1: {
45+ int[] t = new int[3];
46+ t[0] = k;
47+ return "one" + t.length;
48+ }
49+ default:
50+ String d = "dflt" + k;
51+ return d;
52+ }
53+ }
54+
55+ public static void main(String[] args) {
56+ System.out.println(guarded(5) + " " + guarded(1));
57+ loopy();
58+ System.out.println(thrower(0) + " " + thrower(1));
59+ System.out.println(pickCase(0) + pickCase(1) + pickCase(9));
60+ }
61+}

hearth/kiln/tests/cases/ownership/OwnFreeTry.outadded

+8−0
@@ -0,0 +1,8 @@
1+finally guard5 2
2+finally guard1 2
3+6 -1
4+body loop0
5+fin loop0
6+caught t1
7+2 -2
8+zero0!one3dflt9

hearth/kiln/tests/cases/ownership/OwnOk.ashadded

+31−0
@@ -0,0 +1,31 @@
1+public class OwnOk {
2+ static int len(String s) { return s.length(); }
3+
4+ public static void main(String[] args) {
5+ String a = "hello";
6+ int n = len(&a) + len(&a); // &a lends it: a stays usable
7+ String b = a; // a moved into b
8+ a = "again"; // reinitialised: usable again
9+ System.out.println(n + b + a);
10+
11+ String c = "x";
12+ if (n > 100) {
13+ String d = c; // moved on a path that returns
14+ System.out.println(d);
15+ return;
16+ }
17+ System.out.println(c);
18+
19+ for (int i = 0; i < 3; i++) {
20+ String e = "iter" + i; // a fresh variable every iteration
21+ String f = e;
22+ System.out.println(f);
23+ }
24+ String g = "g";
25+ while (true) {
26+ String h = g; // moved, then the loop is left
27+ System.out.println(h);
28+ break;
29+ }
30+ }
31+}

hearth/kiln/tests/cases/ownership/OwnOk.outadded

+6−0
@@ -0,0 +1,6 @@
1+10helloagain
2+x
3+iter0
4+iter1
5+iter2
6+g

hearth/kiln/tests/cases/ownership/ash.pkgadded

+3−0
@@ -0,0 +1,3 @@
1+name: ownership-tests
2+version: 0.1.0
3+memory-safety: on

hearth/kiln/tests/scripts/memory_check.shadded

+52−0
@@ -0,0 +1,52 @@
1+#!/usr/bin/env bash
2+# Memory stays bounded: a loop that builds three million short-lived strings and then 300 thousand arrays must finish in a
3+# few MB with memory-safety on, and the same loop with it off keeps everything (hundreds of MB), which shows
4+# the check is measuring the freeing and not just a small program.
5+set -uo pipefail
6+# Hard cap on every program this script starts (virtual memory, 3 GB): the off-mode run keeps everything
7+# on purpose, and a bug must not be able to take the machine down with it.
8+ulimit -v 3000000
9+cd "$(dirname "$0")/../.." # -> project root
10+export ASH_PATH="${ASH_PATH:-$PWD/../../../ash-stdlib/src/main/ash}"
11+make -s || { echo "build failed"; exit 1; }
12+TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
13+mkdir "$TMP/on" "$TMP/off"
14+printf 'name: m\nversion: 0.1.0\nmemory-safety: on\n' > "$TMP/on/ash.pkg"
15+printf 'name: m\nversion: 0.1.0\nmemory-safety: off\n' > "$TMP/off/ash.pkg"
16+for d in on off; do
17+cat > "$TMP/$d/Churn.ash" <<'JAVA'
18+public class Churn {
19+ public static void main(String[] args) {
20+ long total = 0;
21+ for (int i = 0; i < 3000000; i++) {
22+ String s = "item-" + i;
23+ String t = s + "!";
24+ t += s;
25+ total += s.length() + t.length();
26+ }
27+ for (int i = 0; i < 300000; i++) {
28+ int[] xs = new int[16];
29+ int[] ys = {i, i + 1, i + 2};
30+ xs[3] = i;
31+ total += xs[3] + xs.length + ys[2];
32+ }
33+ System.out.println(total);
34+ }
35+}
36+JAVA
37+done
38+peak() { python3 - "$@" <<'PY'
39+import resource, subprocess, sys
40+r = subprocess.run(sys.argv[1:], capture_output=True, text=True)
41+print(resource.getrusage(resource.RUSAGE_CHILDREN).ru_maxrss // 1024, r.stdout.strip().splitlines()[-1] if r.stdout.strip() else r.stderr[:200])
42+PY
43+}
44+../../bin/kiln "$TMP/on/Churn.ash" -o "$TMP/on/churn" && ../../bin/kiln "$TMP/off/Churn.ash" -o "$TMP/off/churn" || { echo "compile failed"; exit 1; }
45+on=$(peak "$TMP/on/churn")
46+off=$(peak "$TMP/off/churn")
47+on_mb=${on%% *}; off_mb=${off%% *}
48+echo "memory-safety on: ${on} off: ${off} (peak MB, last line of output)"
49+[ "${on#* }" == "${off#* }" ] || { echo "FAIL: outputs differ"; exit 1; }
50+[ "$on_mb" -le 64 ] || { echo "FAIL: on-mode peak ${on_mb} MB is not bounded"; exit 1; }
51+[ "$off_mb" -ge $((on_mb * 4)) ] || { echo "FAIL: off-mode peak ${off_mb} MB is not clearly larger"; exit 1; }
52+echo "memory check ok"

hearth/kiln/tests/scripts/run_pe_emulated.pymodified

+1−1
@@ -31,7 +31,7 @@ def align(n, a=0x1000): return (n + a - 1) // a * a
3131
3232 STACK = 0x7fff00000000
3333 mu.mem_map(STACK, 0x400000)
34-HEAPS = [0x100000000]
34+HEAPS = [0x200000000] # above the image (0x140000000) and the 4 GB heap reservation kiln makes at startup stays clear of it
3535 mu.reg_write(UC_X86_REG_RSP, STACK + 0x400000 - 0x1000)
3636
3737 handles = {}

hearth/mnemo/H/flow/flow.hadded

+40−0
@@ -0,0 +1,40 @@
1+#ifndef MNEMO_FLOW_H
2+#define MNEMO_FLOW_H
3+
4+#include <stdint.h>
5+
6+/* Mnemosyne move-flow engine (Rust, hearth/mnemo/Rust/src/flow.rs), shared by ashvm and kiln through
7+ * hearth/front's ownership pass.
8+ *
9+ * The caller walks a function body and reports events; the engine keeps the data-flow state: the set of
10+ * variable ids moved on the current path, whether the path is dead, and saved copies ("snapshots") used
11+ * to fork at a branch and join afterwards. Ids are opaque 64-bit values chosen by the caller (the
12+ * ownership pass uses the address of the variable).
13+ *
14+ * Ownership: mnemo_walk_new allocates a MnemoWalk that the caller frees exactly once with
15+ * mnemo_walk_free. Snapshot handles belong to their walk; 0 is never valid; release them with
16+ * mnemo_release (unreleased ones are reclaimed by mnemo_walk_free).
17+ * Errors: operations on a NULL walk or an unknown handle do nothing and return -1 (or 0 for queries);
18+ * the library never panics across this boundary.
19+ * Thread safety: a walk must not be used from two threads at once; separate walks are independent.
20+ * No pointer passed in is retained. */
21+
22+typedef struct MnemoWalk MnemoWalk;
23+
24+MnemoWalk *mnemo_walk_new(void);
25+void mnemo_walk_free(MnemoWalk *w);
26+
27+void mnemo_move(MnemoWalk *w, uint64_t id); /* id is moved on the current path (ignored if dead) */
28+void mnemo_reinit(MnemoWalk *w, uint64_t id); /* id holds a new value again */
29+int mnemo_is_moved(MnemoWalk *w, uint64_t id); /* 1 when id is moved on the current, live path */
30+void mnemo_kill(MnemoWalk *w); /* the current path ended */
31+int mnemo_is_dead(MnemoWalk *w);
32+
33+uint32_t mnemo_save(MnemoWalk *w); /* snapshot of the current state */
34+uint32_t mnemo_save_dead(MnemoWalk *w); /* snapshot of "no path reaches here" */
35+int mnemo_restore(MnemoWalk *w, uint32_t h); /* current := snapshot */
36+int mnemo_join(MnemoWalk *w, uint32_t h); /* current := current joined with snapshot */
37+int mnemo_join_into(MnemoWalk *w, uint32_t h); /* snapshot := snapshot joined with current */
38+void mnemo_release(MnemoWalk *w, uint32_t h);
39+
40+#endif

hearth/mnemo/Makefileadded

+23−0
@@ -0,0 +1,23 @@
1+# Mnemosyne: memory safety for Ash. A Rust static library with a C ABI (H/flow/flow.h) that both
2+# ashvm and kiln link, built to ../../bin/lib/mnemo.a. The crate is Rust/; cargo is the only build step.
3+# A missing Rust toolchain is a hard error (run tools/install.sh), never a silent skip.
4+CARGO ?= $(shell command -v cargo 2>/dev/null || echo $(HOME)/.cargo/bin/cargo)
5+LIB = ../../bin/lib/mnemo.a
6+CRATE = Rust
7+
8+.DEFAULT_GOAL := all
9+all: $(LIB)
10+
11+$(LIB): FORCE
12+ @test -x "$(CARGO)" || { echo "mnemo: cargo not found; run tools/install.sh" >&2; exit 1; }
13+ cd $(CRATE) && "$(CARGO)" build --release --quiet
14+ mkdir -p ../../bin/lib
15+ cp $(CRATE)/target/release/libmnemo.a $(LIB)
16+
17+FORCE:
18+
19+clean:
20+ rm -f $(LIB)
21+ rm -rf $(CRATE)/target
22+
23+.PHONY: all clean FORCE

hearth/mnemo/O/.gitkeepadded

+0−0

No content changes. View the file on GitHub

hearth/mnemo/README.mdadded

+51−0
@@ -0,0 +1,51 @@
1+<p align="center">
2+ <img src="https://static.wikia.nocookie.net/godofwar/images/f/ff/Mnemosyne-gabriel-dante-rosetti-1881.jpg/revision/latest?cb=20241211010522" alt="Mnemosyne by Dante Gabriel Rossetti, 1881" width="400">
3+</p>
4+
5+<h1 align="center">Mnemosyne</h1>
6+
7+<p align="center">
8+ <em>Memory safety for the Ash language.</em>
9+</p>
10+
11+---
12+
13+## Who is Mnemosyne?
14+
15+In Greek mythology, Mnemosyne is the Titaness of memory. She is the mother of the nine Muses, because the Greeks believed nothing could be created without first remembering. She keeps track of everything that was and everything that is.
16+
17+That is exactly the job of this library: keep track of every piece of memory, and make sure nobody touches what they shouldn't.
18+
19+## Layout
20+
21+```
22+bin/
23+├── kiln
24+├── ashvm
25+└── lib/
26+ ├── front.a
27+ └── mnemo.a
28+```
29+
30+## What is in it
31+
32+`mnemo.a` is a Rust static library with a C ABI (`H/flow/flow.h`), linked by both `ashvm` and `kiln`. It is the move-flow engine of the ownership pass in `hearth/front/src/jown`: the pass walks the typed AST and reports moves, uses, branches and loops; Mnemosyne keeps the data-flow state (which variables are moved on the current path, joined at every branch and loop back edge).
33+
34+```
35+hearth/mnemo/
36+├── Rust/src/flow.rs the engine (cargo crate, built to bin/lib/mnemo.a by the Makefile)
37+├── H/flow/flow.h the C ABI contract: ownership, errors, thread safety
38+└── tests/ C programs that call the public ABI: tests/scripts/run_regression.sh
39+```
40+
41+The pass decides what is moved, borrowed and freed; see "Memory safety" in the top-level README.
42+
43+## Why "Mnemosyne"?
44+
45+Because memory deserves a goddess. And because almost nobody has heard of her, which makes the name more fun.
46+
47+---
48+
49+<p align="center">
50+ Part of the <a href="https://ash-project.org">Ash</a> project.
51+</p>
\ No newline at end of file

hearth/mnemo/Rust/.gitignoreadded

+1−0
@@ -0,0 +1 @@
1+target/

hearth/mnemo/Rust/Cargo.lockadded

+7−0
@@ -0,0 +1,7 @@
1+# This file is automatically @generated by Cargo.
2+# It is not intended for manual editing.
3+version = 4
4+
5+[[package]]
6+name = "mnemo"
7+version = "0.1.0"

hearth/mnemo/Rust/Cargo.tomladded

+16−0
@@ -0,0 +1,16 @@
1+[package]
2+name = "mnemo"
3+version = "0.1.0"
4+edition = "2021"
5+description = "Mnemosyne: memory safety for Ash. C-ABI static library shared by ashvm and kiln."
6+publish = false
7+
8+[lib]
9+name = "mnemo"
10+crate-type = ["staticlib"]
11+
12+[profile.release]
13+opt-level = 3
14+panic = "abort"
15+lto = true
16+codegen-units = 1

hearth/mnemo/Rust/src/flow.rsadded

+179−0
@@ -0,0 +1,179 @@
1+//! Move-flow engine for the ownership pass (front/src/jown). The C side walks the typed AST and calls
2+//! in here for every event; this side owns the dataflow state, so the "moved" sets and their joins at
3+//! branches and loops live in memory-safe code. Contract and ownership rules: `H/flow/flow.h`.
4+
5+use std::collections::BTreeSet;
6+
7+#[derive(Clone, Default)]
8+struct State {
9+ /// the path ended (return / throw / break / continue); it contributes nothing to a join
10+ dead: bool,
11+ moved: BTreeSet<u64>,
12+}
13+
14+impl State {
15+ fn dead() -> State {
16+ State { dead: true, moved: BTreeSet::new() }
17+ }
18+
19+ /// Joins `other` into `self`: a dead path contributes nothing.
20+ fn join(&mut self, other: &State) {
21+ if other.dead {
22+ return;
23+ }
24+ if self.dead {
25+ *self = other.clone();
26+ return;
27+ }
28+ self.moved.extend(other.moved.iter().copied());
29+ }
30+}
31+
32+pub struct Walk {
33+ cur: State,
34+ /// saved states; a handle is slot index + 1 so that 0 is never valid
35+ snaps: Vec<Option<State>>,
36+ free: Vec<usize>,
37+}
38+
39+impl Walk {
40+ fn slot(&mut self, st: State) -> u32 {
41+ let i = match self.free.pop() {
42+ Some(i) => {
43+ self.snaps[i] = Some(st);
44+ i
45+ }
46+ None => {
47+ self.snaps.push(Some(st));
48+ self.snaps.len() - 1
49+ }
50+ };
51+ (i + 1) as u32
52+ }
53+
54+ fn get(&self, h: u32) -> Option<&State> {
55+ (h as usize).checked_sub(1).and_then(|i| self.snaps.get(i)).and_then(|s| s.as_ref())
56+ }
57+}
58+
59+fn walk<'a>(w: *mut Walk) -> Option<&'a mut Walk> {
60+ // SAFETY: the caller passes a pointer from mnemo_walk_new that has not been freed (flow.h).
61+ unsafe { w.as_mut() }
62+}
63+
64+#[no_mangle]
65+pub extern "C" fn mnemo_walk_new() -> *mut Walk {
66+ Box::into_raw(Box::new(Walk { cur: State::default(), snaps: Vec::new(), free: Vec::new() }))
67+}
68+
69+#[no_mangle]
70+pub extern "C" fn mnemo_walk_free(w: *mut Walk) {
71+ if !w.is_null() {
72+ // SAFETY: allocated by mnemo_walk_new, freed exactly once by contract.
73+ drop(unsafe { Box::from_raw(w) });
74+ }
75+}
76+
77+#[no_mangle]
78+pub extern "C" fn mnemo_move(w: *mut Walk, id: u64) {
79+ if let Some(w) = walk(w) {
80+ if !w.cur.dead {
81+ w.cur.moved.insert(id);
82+ }
83+ }
84+}
85+
86+#[no_mangle]
87+pub extern "C" fn mnemo_reinit(w: *mut Walk, id: u64) {
88+ if let Some(w) = walk(w) {
89+ w.cur.moved.remove(&id);
90+ }
91+}
92+
93+#[no_mangle]
94+pub extern "C" fn mnemo_is_moved(w: *mut Walk, id: u64) -> i32 {
95+ match walk(w) {
96+ Some(w) => (!w.cur.dead && w.cur.moved.contains(&id)) as i32,
97+ None => 0,
98+ }
99+}
100+
101+#[no_mangle]
102+pub extern "C" fn mnemo_kill(w: *mut Walk) {
103+ if let Some(w) = walk(w) {
104+ w.cur.dead = true;
105+ }
106+}
107+
108+#[no_mangle]
109+pub extern "C" fn mnemo_is_dead(w: *mut Walk) -> i32 {
110+ walk(w).map_or(0, |w| w.cur.dead as i32)
111+}
112+
113+#[no_mangle]
114+pub extern "C" fn mnemo_save(w: *mut Walk) -> u32 {
115+ match walk(w) {
116+ Some(w) => {
117+ let st = w.cur.clone();
118+ w.slot(st)
119+ }
120+ None => 0,
121+ }
122+}
123+
124+/// A snapshot that is already dead: the empty accumulator for `break`/`continue` states.
125+#[no_mangle]
126+pub extern "C" fn mnemo_save_dead(w: *mut Walk) -> u32 {
127+ walk(w).map_or(0, |w| w.slot(State::dead()))
128+}
129+
130+#[no_mangle]
131+pub extern "C" fn mnemo_restore(w: *mut Walk, h: u32) -> i32 {
132+ let Some(w) = walk(w) else { return -1 };
133+ match w.get(h).cloned() {
134+ Some(st) => {
135+ w.cur = st;
136+ 0
137+ }
138+ None => -1,
139+ }
140+}
141+
142+#[no_mangle]
143+pub extern "C" fn mnemo_join(w: *mut Walk, h: u32) -> i32 {
144+ let Some(w) = walk(w) else { return -1 };
145+ match w.get(h).cloned() {
146+ Some(st) => {
147+ w.cur.join(&st);
148+ 0
149+ }
150+ None => -1,
151+ }
152+}
153+
154+/// Joins the current state into snapshot `h` (accumulates a `break` / `continue` exit).
155+#[no_mangle]
156+pub extern "C" fn mnemo_join_into(w: *mut Walk, h: u32) -> i32 {
157+ let Some(w) = walk(w) else { return -1 };
158+ let cur = w.cur.clone();
159+ match (h as usize).checked_sub(1).and_then(|i| w.snaps.get_mut(i)).and_then(|s| s.as_mut()) {
160+ Some(st) => {
161+ st.join(&cur);
162+ 0
163+ }
164+ None => -1,
165+ }
166+}
167+
168+/// Releases snapshot `h`; its slot is reused. Unknown or already released handles are ignored.
169+#[no_mangle]
170+pub extern "C" fn mnemo_release(w: *mut Walk, h: u32) {
171+ if let Some(w) = walk(w) {
172+ if let Some(i) = (h as usize).checked_sub(1) {
173+ if matches!(w.snaps.get(i), Some(Some(_))) {
174+ w.snaps[i] = None;
175+ w.free.push(i);
176+ }
177+ }
178+ }
179+}

hearth/mnemo/Rust/src/lib.rsadded

+4−0
@@ -0,0 +1,4 @@
1+//! Mnemosyne: C-ABI static library shared by ashvm and kiln (through hearth/front).
2+//! The public contract is `H/flow/flow.h`; nothing Rust-specific crosses it.
3+
4+mod flow;

hearth/mnemo/tests/cases/flow/flow_abi.cadded

+58−0
@@ -0,0 +1,58 @@
1+/* Calls the public C ABI of mnemo.a (H/flow/flow.h) the way front's ownership pass does. */
2+#include <stdio.h>
3+#include "mnemo/H/flow/flow.h"
4+
5+static int failures;
6+#define CHECK(c) do { if (!(c)) { printf("FAIL line %d: %s\n", __LINE__, #c); failures++; } } while (0)
7+
8+int main(void) {
9+ MnemoWalk *w = mnemo_walk_new();
10+ CHECK(w != NULL);
11+
12+ /* move / reinit */
13+ mnemo_move(w, 1);
14+ CHECK(mnemo_is_moved(w, 1) == 1 && mnemo_is_moved(w, 2) == 0);
15+ mnemo_reinit(w, 1);
16+ CHECK(mnemo_is_moved(w, 1) == 0);
17+
18+ /* branches: moved on one path stays moved after the join */
19+ uint32_t entry = mnemo_save(w);
20+ mnemo_move(w, 7);
21+ uint32_t then_s = mnemo_save(w);
22+ CHECK(mnemo_restore(w, entry) == 0 && mnemo_is_moved(w, 7) == 0);
23+ CHECK(mnemo_join(w, then_s) == 0 && mnemo_is_moved(w, 7) == 1);
24+
25+ /* a dead path contributes nothing; a move on a dead path is ignored */
26+ uint32_t keep = mnemo_save(w);
27+ mnemo_restore(w, entry);
28+ mnemo_move(w, 9);
29+ mnemo_kill(w);
30+ CHECK(mnemo_is_dead(w) == 1 && mnemo_is_moved(w, 9) == 0);
31+ mnemo_move(w, 11);
32+ uint32_t dead_s = mnemo_save(w);
33+ mnemo_restore(w, keep);
34+ CHECK(mnemo_join(w, dead_s) == 0 && mnemo_is_moved(w, 7) == 1 && mnemo_is_moved(w, 9) == 0 && mnemo_is_moved(w, 11) == 0);
35+
36+ /* accumulators: break states joined into a dead snapshot */
37+ uint32_t acc = mnemo_save_dead(w);
38+ CHECK(mnemo_join_into(w, acc) == 0);
39+ mnemo_restore(w, entry);
40+ mnemo_join(w, acc);
41+ CHECK(mnemo_is_moved(w, 7) == 1);
42+
43+ /* handles: released and unknown ones are rejected without crashing */
44+ mnemo_release(w, acc);
45+ CHECK(mnemo_restore(w, acc) == -1 && mnemo_join(w, 0) == -1 && mnemo_join_into(w, 9999) == -1);
46+ mnemo_release(w, acc);
47+ mnemo_release(w, 9999);
48+
49+ /* NULL walk is inert */
50+ mnemo_move(NULL, 1);
51+ CHECK(mnemo_is_moved(NULL, 1) == 0 && mnemo_save(NULL) == 0 && mnemo_restore(NULL, 1) == -1);
52+ mnemo_walk_free(NULL);
53+
54+ mnemo_walk_free(w);
55+ if (failures) return 1;
56+ puts("flow ABI ok");
57+ return 0;
58+}

hearth/mnemo/tests/scripts/run_regression.shadded

+13−0
@@ -0,0 +1,13 @@
1+#!/usr/bin/env bash
2+# mnemo/tests/scripts/run_regression.sh
3+# Builds mnemo.a and runs the C programs in tests/cases/*/ against its public C ABI.
4+set -uo pipefail
5+cd "$(dirname "$0")/../.." # -> mnemo/
6+make -s || { echo "build failed"; exit 1; }
7+TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
8+fail=0
9+for f in tests/cases/*/*.c; do
10+ gcc -Wall -Wextra -std=c11 -I.. "$f" ../../bin/lib/mnemo.a -lpthread -ldl -lm -o "$TMP/t" || { echo "DIFF: $f (compile)"; fail=1; continue; }
11+ if "$TMP/t" > "$TMP/out" 2>&1; then echo "OK: $f"; else echo "DIFF: $f"; cat "$TMP/out"; fail=1; fi
12+done
13+exit $fail