← Commit history
Add ownership tests and memory safety features
lennoxrose committed on Oct 18, 2026 at 23:10 UTC
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 notes105105
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 GitHub168182
into a project-local @ash-modules/ folder; point ASH_PATH at an installed source root169183
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 kiln171186
```172187
173188
Every subsystem keeps sources in `C/`, headers in `H/`, build output in a mirrored `O/` tree, andexamples/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 below6969
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_keys1515
{ "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: MIT3333
// main: demo.Main main entry class (qualified) for `apm run start`-style tooling3434
// keywords: ash, cli keywords comma-separated35
+// memory-safety: on memory-safety on (default) | off: ownership/borrow checks and freeing, read by ashvm and kiln3536
//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_GC2626
endif2727
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.c5859
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: FORCE93
+../../bin/lib/front.a: FORCE9394
$(MAKE) -C ../front9495
96
+# Mnemosyne (Rust): the ownership pass in front calls into it.97
+../../bin/lib/mnemo.a: FORCE98
+ $(MAKE) -C ../mnemo99
+95100
FORCE:96101
97
-$(BIN): $(ASHVM_OBJ) ../../bin/libashfront.a ../../bin/extensions/pyre/libpyre.a102
+$(BIN): $(ASHVM_OBJ) ../../bin/lib/front.a ../../bin/lib/mnemo.a ../../bin/extensions/pyre/libpyre.a98103
mkdir -p ../../bin99104
$(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 owners2830
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.h4
+ ../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.h57
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 value1920
// (`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, 231
+ 0, 1, 2,32
+ 1, 3, 1, 1, 1, 1, 13133
};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, sort5355
if (id == 43) return vm_call_builtin_matrix(id, args, argc); // matrix_mul5456
if (id >= 44 && id <= 46) return vm_call_builtin_proc(id, args, argc); // sys_fork, sys_wait, sys_exec57
+ if (id >= 47 && id <= 53) return vm_call_builtin_mem(id, args, argc); // mem_free5558
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, which78
+ // 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 status8084
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 value8
+// (front/src/jown, `memory-safety: on` in ash.pkg). Only the value's own buffer is freed; elements of an9
+// array may be shared and are left alone. Anything else is ignored. Under the experimental collector10
+// (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_GC16
+ int fl = (int)args[2].number;17
+ if (fl & 1) free(args[0].str);18
+ if (fl & 2) free(args[1].str);19
+#endif20
+ return r;21
+ }22
+ if (argc != 1) { vm_runtime_error("mem_free(x) expects 1 argument\n"); }23
+#ifndef ASH_GC24
+ VMValue v = args[0];25
+ if (id == 53) { // mem_free_elems(x): the Strings of a String[] / List<String> that only it references26
+ 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 itself37
+ 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 number41
+ 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 alone53
+ 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
+#endif60
+ 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.h3
+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
#endifhearth/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
+588890hearth/ashvm/tests/cases/memoff/ash.pkgadded
+3−0@@ -0,0 +1,3 @@1
+name: o2
+version: 0.1.03
+memory-safety: offhearth/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 overlap8
+ int[] xs = new int[2];9
+ int[] ys = new int[2];10
+ append(&mut xs, &ys); // different variables: fine11
+ 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
+6abc2hearth/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 call7
+ both(&mut xs, xs); // error: exclusive borrow plus another use8
+ int[] ys = &xs; // error: a borrow is only a call argument9
+ }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 call2
+ --> tests/cases/ownership/OwnBorrowConflict.ash:6:233
+ |4
+ 6| both(&mut xs, &xs); // error: exclusive and shared borrow of xs in one call5
+ | ^6
+error: cannot use 'xs' here: it is already borrowed mutably in this call7
+ --> tests/cases/ownership/OwnBorrowConflict.ash:7:238
+ |9
+ 7| both(&mut xs, xs); // error: exclusive borrow plus another use10
+ | ^11
+error: a borrow ('&') can only be passed as a call argument12
+ --> tests/cases/ownership/OwnBorrowConflict.ash:8:2013
+ |14
+ 8| int[] ys = &xs; // error: a borrow is only a call argument15
+ | ^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 moves5
+ }6
+7
+ static int size(String s) { return s.length(); } // only reads: the caller keeps ownership8
+ 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 reader14
+ static String same(String s) { return s; } // returns the caller's value: moves15
+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 iteration22
+ }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 kept2
+r400775560hearth/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 Dogs12
+ }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 it23
+ static int lam(String s) {24
+ Runnable r = () -> sink.add(s); // captured by a lambda25
+ 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 caller31
+ }32
+ static int pointSum(Point p) { return p.x + p.y; } // reads fields only: a plain class with no subclass is fine33
+ static String call(Animal a) { return a.sound(); } // may run Dog.sound: never borrowed34
+ static int overwrite(String s) {35
+ s = s + "!"; // the parameter is a copy: the caller's value is untouched36
+ 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 sink50
+ total += lam(d); // d now lives in sink51
+ 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 animalhearth/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 only6
+ System.out.println(a); // error: maybe moved7
+ 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 first2
+ --> tests/cases/ownership/OwnBranch.ash:6:283
+ |4
+ 6| System.out.println(a); // error: maybe moved5
+ | ^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 iteration6
+ int[] xs = new int[16]; // fresh array, sole owner7
+ xs[3] = i;8
+ String t = s + "!";9
+ t += s; // reassigned with a fresh value: the old one is freed first10
+ if (i % 100000 == 0) {11
+ System.out.println(t);12
+ continue; // continue frees what the iteration owns13
+ }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-02
+item-100000!item-1000003
+item-200000!item-2000004
+15566524hearth/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 fresh11
+ }12
+13
+ static class Box {14
+ String held;15
+ void keep(String s) { held = s; } // keeps its argument16
+ }17
+18
+ static String grow(int n) {19
+ if (n == 0) return "";20
+ String rest = grow(n - 1); // recursive: never treated as fresh21
+ 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 field31
+ 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 owners34
+ String v = (i % 3 == 0) ? t : u; // aliases: t and u must not be freed35
+ kept.add("k" + i); // fresh temp handed to a collection: never freed here36
+ if (i % 5000 == 0) box.keep("b" + i); // the argument outlives the iteration37
+ 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 array41
+ 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 sharedhearth/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 you2
+ --> tests/cases/ownership/OwnFreeDenied.ash:7:133
+ |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 you7
+ --> tests/cases/ownership/OwnFreeDenied.ash:7:138
+ |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 Bag7
+ 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 list16
+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 list21
+ 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 read26
+27
+ List<String> b = new ArrayList<>(); // a literal element: the list is freed, the literal never is28
+ 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 alias33
+ 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 only38
+ 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 store43
+ 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 escapes50
+ 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
+b02
+b100003
+1211120 g9hearth/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 exact16
+ }17
+18
+ static Point make(int i) {19
+ Point p = new Point(i, 1); // sole owner, handed to the caller20
+ 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 fresh29
+ }30
+31
+ static Point cached = new Point(5, 5);32
+ static Point shared() {33
+ return cached; // an alias of a static: never fresh34
+ }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 iteration40
+ Point b = origin(); // freed each iteration41
+ Shape s = shape(i);42
+ Point c = shared(); // alias: must survive43
+ 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 10hearth/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: owned4
+ String label; // only ever given fresh concatenations: owned5
+ String shared = "shared"; // a literal initializer is shared: never owned6
+ int[] borrowed; // assigned from a parameter: never owned7
+ String leaked; // returned by a getter: never owned8
+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 value16
+ 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 value18
+ }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 field25
+ Buffer b = new Buffer(i, given); // freed each iteration together with cells and label26
+ b.relabel(i);27
+ sum += b.total();28
+ if (i == 7) kept = b.leak(); // aliases leaked: it must survive b29
+ }30
+ System.out.println(sum + " " + kept);31
+ }32
+}hearth/ashvm/tests/cases/ownership/OwnFreeFields.outadded
+1−0@@ -0,0 +1 @@1
+417780 leak7hearth/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 time8
+ if (acc.length() > 6) break; // break frees acc's loop-scoped siblings only9
+ }10
+ String out = acc + "|" + n; // fresh; escapes through return below, so it is never freed11
+ 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 value18
+ 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 freed27
+ String again = shared; // moved28
+ 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/box39
+ }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|92
+7 103
+s044
+line 0line 0125
+line 026
+line 1line 1127
+line 2line 2128
+line 22hearth/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 methods7
+ List<Integer> view; // assigned from elsewhere: shared8
+ 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 caller20
+ }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 iteration28
+ 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 factory33
+ for (int y : ys) total += y;34
+ List<Integer> zs = new ArrayList<>(); // passed on: not freed35
+ 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 sees44
+ 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 7hearth/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
+92
+-r0c24;-4;-r2c23
+fin a0d4
+fin a1d5
+fin a2d6
+3 4 37
+208
+p1!q2!r3!3hearth/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 field11
+ 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 only21
+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 iteration27
+ 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 owner42
+ 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 freed50
+ 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 1hearth/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 freed14
+ }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 iteration22
+ 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 owner31
+ 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
+42
+1000043
+20000799990 15hearth/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 fresh10
+ }11
+12
+ static String name = "shared";13
+ static String alias(int i) { return name; } // not fresh: callers must never free it14
+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 iteration25
+ String b = pick(i);26
+ String c = alias(i); // aliases a static: never freed27
+ 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 sharedhearth/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 computed7
+ 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 continue19
+ 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 handler34
+ 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 22
+finally guard1 23
+6 -14
+body loop05
+fin loop06
+caught t17
+2 -28
+zero0!one3dflt9hearth/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 parameter5
+ }6
+ static String same(String s) { return s; } // returns its parameter7
+ 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 parameter15
+ String e = "ghi";16
+ int n = size(&e); // fine: size only reads17
+ 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 it2
+ --> tests/cases/ownership/OwnLendKept.ash:12:153
+ |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 it7
+ --> tests/cases/ownership/OwnLendKept.ash:14:258
+ |9
+ 14| String d = same(&c); // error: same returns its parameter10
+ | ^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 iteration7
+ }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 first2
+ --> tests/cases/ownership/OwnLoop.ash:6:203
+ |4
+ 6| keep = a; // error: moved in the previous iteration5
+ | ^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 moved6
+ 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 first2
+ --> tests/cases/ownership/OwnMoved.ash:5:283
+ |4
+ 5| System.out.println(a); // error: a was moved5
+ | ^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 in3
+4
+ public static void main(String[] args) {5
+ String a = "abc";6
+ String n = same(a); // moves a into the callee7
+ System.out.println(a + n); // error: a was moved by the call8
+ }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 first2
+ --> tests/cases/ownership/OwnMovedCall.ash:7:283
+ |4
+ 7| System.out.println(a + n); // error: a was moved by the call5
+ | ^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 moved11
+ System.out.println(a + back); // error12
+ Box b = new Box();13
+ String c = "def";14
+ b.put(c); // stores its parameter: c is moved15
+ System.out.println(c); // error16
+ }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 first2
+ --> tests/cases/ownership/OwnMovedKeep.ash:11:283
+ |4
+ 11| System.out.println(a + back); // error5
+ | ^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 first7
+ --> tests/cases/ownership/OwnMovedKeep.ash:15:288
+ |9
+ 15| System.out.println(c); // error10
+ | ^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 usable7
+ String b = a; // a moved into b8
+ a = "again"; // reinitialised: usable again9
+ System.out.println(n + b + a);10
+11
+ String c = "x";12
+ if (n > 100) {13
+ String d = c; // moved on a path that returns14
+ 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 iteration21
+ 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 left27
+ System.out.println(h);28
+ break;29
+ }30
+ }31
+}hearth/ashvm/tests/cases/ownership/OwnOk.outadded
+6−0@@ -0,0 +1,6 @@1
+10helloagain2
+x3
+iter04
+iter15
+iter26
+ghearth/ashvm/tests/cases/ownership/ash.pkgadded
+3−0@@ -0,0 +1,3 @@1
+name: ownership-tests2
+version: 0.1.03
+memory-safety: onhearth/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 array6
+ int b = xs[-1]; // negative7
+ 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 32
+ --> tests/cases/safety/ConstIndex.ash:5:173
+ |4
+ 5| int a = xs[3]; // past the end of a final array5
+ | ^6
+error: array index -1 is negative7
+ --> tests/cases/safety/ConstIndex.ash:6:178
+ |9
+ 6| int b = xs[-1]; // negative10
+ | ^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 long2
+ --> tests/cases/safety/ConstOverflow.ash:4:173
+ |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 long7
+ --> tests/cases/safety/ConstOverflow.ash:6:178
+ |9
+ 6| int b = n * n;10
+ | ^11
+error: arithmetic overflow: constant expression is outside the range of int; use an 'L' suffix for long12
+ --> tests/cases/safety/ConstOverflow.ash:7:1713
+ |14
+ 7| int c = BIG + BIG;15
+ | ^16
+error: arithmetic overflow: constant expression is outside the range of int; use an 'L' suffix for long17
+ --> tests/cases/safety/ConstOverflow.ash:8:1718
+ |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 bash2
+# Memory stays bounded: a loop that builds three million short-lived strings and then 300 thousand arrays must finish in a3
+# few MB with memory-safety on, and the same loop with it off keeps everything (hundreds of MB), which shows4
+# the check is measuring the freeing and not just a small program.5
+set -uo pipefail6
+# Hard cap on every program this script starts (virtual memory, 3 GB): the off-mode run keeps everything7
+# on purpose, and a bug must not be able to take the machine down with it.8
+ulimit -v 30000009
+cd "$(dirname "$0")/../.." # -> project root10
+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"' EXIT13
+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; do17
+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
+JAVA37
+done38
+peak() { python3 - "$@" <<'PY'39
+import resource, subprocess, sys40
+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
+PY43
+}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 bash2
+# 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 is4
+# 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 with7
+# 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 pipefail9
+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"' EXIT15
+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 400000019
+ok=0; skipped=0; bad=020
+for seed in $(seq "$FIRST" "$LAST"); do21
+ 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; fi25
+ 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"; fi29
+ if [ -n "${KILN:-}" ]; then30
+ if "$KILN_BIN" "$TMP/on/F$seed.ash" -o "$TMP/k$seed" > /dev/null 2>&1; then31
+ 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"; fi34
+ fi35
+ 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)); fi36
+done37
+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 python32
+# 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, sys5
+6
+HELPERS = '''7
+ static String SK; // statics that keep values alive8
+ 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 = 051
+ self.lines = []52
+ self.out_i = 053
+54
+ def fresh(self):55
+ self.n += 156
+ return self.n57
+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 vars66
+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 None70
+71
+ def use(self, ind, v, k):72
+ # a read of v that would show corruption73
+ 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.r78
+ op = r.randrange(30)79
+ if op <= 3: # declare something fresh80
+ 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] = k94
+ elif op == 4: # from a factory95
+ 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] = k99
+ elif op <= 7: # read something100
+ v = self.pick(vars)101
+ if v: self.use(ind, v, vars[v])102
+ elif op == 8: # call a reader helper103
+ 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 result114
+ 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] = k120
+ elif op == 11: # alias121
+ 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] = k127
+ elif op == 12 and depth < 2: # loop128
+ 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 moved133
+ for v in list(vars):134
+ if v not in inner: del vars[v]135
+ elif op == 13 and depth < 2: # if / else136
+ 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 work144
+ 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 Box150
+ 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 container158
+ 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 list162
+ 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 strings166
+ 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 / break174
+ 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 locals182
+ 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 local195
+ v = self.pick(vars, "S")196
+ if v: self.emit(ind, f'{v} = {v} + "+" + total;')197
+ elif op == 23: # conditional helper returning one of two198
+ 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 ways204
+ 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 reads215
+ 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 later227
+ 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 element234
+ 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 element237
+ 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 loop243
+ 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 reads248
+ 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 here257
+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 survived264
+ 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 = gcc22
.DEFAULT_GOAL := all3
-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 IR6
-# 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
endef4445
$(foreach s,$(FRONT_SRC),$(eval $(call compile_rule,$(s))))4546
46
-LIB = ../../bin/libashfront.a47
+LIB = ../../bin/lib/front.a4748
4849
all: $(LIB)4950
5051
$(LIB): $(FRONT_OBJ)51
- mkdir -p ../../bin52
+ mkdir -p ../../bin/lib5253
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", NULL69
+ "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", NULL3270
};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 ch412450
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 via89
* 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 the584
+ * 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 the19
+ * 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 122
+#define JL_DROP_ARRAYS 223
+#define JL_DROP_OBJECTS 424
+#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_H33
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−0Show 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 is381
+ * declared directly in a block with a fresh String (a non-constant `+`) or array and is only ever382
+ * 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 use384
+ * (passing it on, returning it, storing it, capturing it in a lambda, `T b = a`) leaves it to the385
+ * runtime, which never frees it: leaking is safe, freeing a shared value is not. Reassignments must386
+ * 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 to410
+ * code we cannot see. Field reads and writes through `this`, and calls to other non-escaping methods411
+ * 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 in506
+ * 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 becomes521
+ * 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 value738
+ * 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 the780
+ * 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 passes802
+ * (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; for814
+ * 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 lose896
+ * 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−0No content changes. View the file on GitHub
hearth/front/src/jown/H/own.hadded
+42−0@@ -0,0 +1,42 @@1
+#ifndef JOWN_H2
+#define JOWN_H3
+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 the7
+ * 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 MOVED10
+ * 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 can14
+ * 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.pkg17
+ * 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 124
+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 can32
+ * 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
+#endifhearth/front/src/jown/O/.gitkeepadded
+0−0No 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 that973
+ * 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 constant984
+ * (`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` local1041
+ * initialised with one (other locals can still be reassigned, and programs rely on the run-time1042
+ * 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 g6666
}6767
}hearth/kiln/Makefilemodified
+12−5@@ -2,7 +2,7 @@ CC = gcc22
.DEFAULT_GOAL := all33
KILN_RPATH_LIST = $(abspath ../../bin/extensions/pyre):/usr/local/lib/kiln44
CFLAGS = -Wall -Wextra -std=c11 -O2 -Isrc -I../front/src -DKILN_RPATH='"$(KILN_RPATH_LIST)"'5
-LDFLAGS = ../../bin/libashfront.a5
+LDFLAGS = ../../bin/lib/front.a ../../bin/lib/mnemo.a -lpthread -ldl -lm66
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: FORCE111
+../../bin/lib/front.a: FORCE111112
$(MAKE) -C ../front112113
114
+# Mnemosyne (Rust): the ownership pass in front calls into it.115
+../../bin/lib/mnemo.a: FORCE116
+ $(MAKE) -C ../mnemo117
+113118
FORCE:114119
115
-$(BIN): $(KILN_OBJ) ../../bin/libashfront.a120
+$(BIN): $(KILN_OBJ) ../../bin/lib/front.a ../../bin/lib/mnemo.a116121
mkdir -p ../../bin117122
$(CC) $(CFLAGS) -o $(BIN) $(KILN_OBJ) $(LDFLAGS)118123
@@ -127,10 +132,11 @@ clean:127132
# --link=shared kiln-compiled program's DT_RPATH points at (see128133
# src/elf/elf_dynamic.h's KILN_RPATH) -- needs root to write there, so129134
# this is a separate, explicit, opt-in step, never run by `make all`.130
-KILNRT = ../../bin/libkilnrt.so135
+KILNRT = ../../bin/lib/libkilnrt.so131136
KILN_RPATH_DIR = /usr/local/lib/kiln132137
133138
$(KILNRT): $(BIN)139
+ mkdir -p ../../bin/lib134140
$(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, so142148
# libkilnrt.dll just needs to sit next to whatever --link=shared143149
# --target=windows binary is using it (or on PATH).144
-KILNRT_DLL = ../../bin/libkilnrt.dll150
+KILNRT_DLL = ../../bin/lib/libkilnrt.dll145151
146152
$(KILNRT_DLL): $(BIN)153
+ mkdir -p ../../bin/lib147154
$(BIN) --emit-runtime -o $(KILNRT_DLL) --target=windows148155
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.h10
+ ../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.h1113
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 the13
+// 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); // payload34
+ emit_pop_reg(code, REG_RBX); // tag35
+ 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); // tag65
+ emit_movq_xmm_from_reg(code, XMM0, REG_RDX);66
+ emit_cvttsd2si(code, REG_RDX, XMM0);67
+ emit_pop_reg(code, REG_RCX); // b payload68
+ emit_pop_reg(code, REG_RBX);69
+ emit_pop_reg(code, REG_RAX); // a payload70
+ emit_pop_reg(code, REG_RBX);71
+ if (!heap_free_lists_on()) {72
+ codegen_string_concat(); // operands stay where they are73
+ return;74
+ }75
+ emit_push_reg(code, REG_RAX); // a76
+ emit_push_reg(code, REG_RCX); // b77
+ emit_push_reg(code, REG_RDX); // flags78
+ codegen_string_concat(); // pushes the result (tag, payload)79
+ emit_pop_reg(code, REG_RDI); // result payload80
+ emit_pop_reg(code, REG_RBX); // result tag (a String)81
+ emit_pop_reg(code, REG_RDX); // flags82
+ emit_pop_reg(code, REG_RCX); // b83
+ emit_pop_reg(code, REG_RAX); // a84
+ emit_push_reg(code, REG_RDI); // result85
+ emit_push_reg(code, REG_RDX); // flags: the free code clobbers RDX86
+ emit_push_reg(code, REG_RCX); // b87
+ free_if_flag(1); // a is in RAX88
+ emit_pop_reg(code, REG_RAX); // b89
+ emit_pop_reg(code, REG_RDX); // flags90
+ free_if_flag(2);91
+ emit_pop_reg(code, REG_RDI); // result payload92
+ 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 was98
+// 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); // payload103
+ emit_pop_reg(code, REG_RBX); // tag104
+ 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 gives114
+// 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 payload122
+ emit_pop_reg(code, REG_RBX); // d tag123
+ emit_pop_reg(code, REG_RCX); // n payload (a double)124
+ emit_pop_reg(code, REG_RDX); // n tag125
+ 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 * 16140
+ 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 block146
+ 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 object152
+ 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 = remaining156
+ 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); // object170
+ emit_load_mem_disp32(code, REG_RCX, REG_RSI, 16);171
+ emit_store_mem_disp32(code, REG_RAX, 0, REG_RCX); // capacity = n172
+ emit_store_mem_disp32(code, REG_RAX, 8, REG_RCX); // count = n173
+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 32);174
+ emit_store_mem_disp32(code, REG_RAX, 16, REG_RDX); // data_ptr175
+ 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 the182
+// 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); // payload187
+ emit_pop_reg(code, REG_RBX); // tag188
+ 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 its198
+// values (each one an array literal, so object and element block go back), then the map's entries block and the199
+// 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); // payload205
+ emit_pop_reg(code, REG_RBX); // tag206
+ 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 free211
+ 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_ptr214
+ emit_store_mem_disp32(code, REG_RSI, 0, REG_RCX);215
+ emit_load_mem_disp32(code, REG_RCX, REG_RAX, 8); // count216
+ 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); // cursor224
+ emit_load_mem_disp32(code, REG_RAX, REG_RDX, 16); // value tag225
+ emit_cmp_reg_imm32(code, REG_RAX, TAG_ARRAY);226
+ int not_chain = emit_jcc_rel32(code, COND_NE); // the element count is a number227
+ emit_load_mem_disp32(code, REG_RAX, REG_RDX, 24); // chain head: an entry array228
+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 block236
+ emit_load_mem_disp32(code, REG_RDX, REG_RCX, 40); // next payload237
+ emit_load_mem_disp32(code, REG_RCX, REG_RCX, 32); // next tag238
+ emit_push_reg(code, REG_RDX);239
+ emit_push_reg(code, REG_RCX);240
+ heap_emit_free_array_rax(code); // RAX = this entry array241
+ emit_pop_reg(code, REG_RCX); // next tag242
+ emit_pop_reg(code, REG_RAX); // next payload243
+ 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 entry250
+ 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 object261
+ 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 only267
+// 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); // payload274
+ emit_pop_reg(code, REG_RBX); // tag275
+ 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_ptr282
+ emit_store_mem_disp32(code, REG_RSI, 0, REG_RCX);283
+ emit_load_mem_disp32(code, REG_RCX, REG_RAX, 8); // count284
+ 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); // cursor292
+ emit_load_mem_disp32(code, REG_RAX, REG_RDX, 0); // element tag293
+ emit_cmp_reg_imm32(code, REG_RAX, TAG_STRING);294
+ int not_string = emit_jcc_rel32(code, COND_NE); // none and the like stay295
+ emit_load_mem_disp32(code, REG_RAX, REG_RDX, 8); // the String's payload296
+ 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 RSI38
+ heap_emit_alloc_str(code); // RAX = new block, clobbers RSI3939
4040
emit_store_mem_disp32(code, REG_RAX, 0, REG_RDX); // length prefix4141
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 block22
+ heap_emit_alloc_recyclable(code); // RAX = elements block2323
emit_mov_reg_reg(code, REG_RSI, REG_RAX); // stash: RSI survives the pop loop below2424
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 object3535
emit_push_reg(code, REG_RSI); // save elements block address across this alloc3636
emit_mov_reg_imm64(code, REG_RDI, ARRAY_OBJECT_SIZE);37
- heap_emit_alloc(code); // RAX = array object37
+ heap_emit_alloc_recyclable(code); // RAX = array object3838
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 call145145
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 block146
+ heap_emit_alloc_recyclable(code); // RAX = new data block147147
emit_pop_reg(code, REG_RDX); // RDX = new_capacity148148
emit_pop_reg(code, REG_RCX); // scratch base149149
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 block28
+ heap_emit_alloc_recyclable(code); // RAX = entries block2929
emit_mov_reg_reg(code, REG_RSI, REG_RAX); // stash across the pop loop3030
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 alloc4646
emit_mov_reg_imm64(code, REG_RDI, MAP_OBJECT_SIZE);47
- heap_emit_alloc(code); // RAX = map object47
+ heap_emit_alloc_recyclable(code); // RAX = map object4848
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 block64
+ heap_emit_alloc_recyclable(code); // RAX = new entries block6565
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 count7676
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 reading80
+ // 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 capacity84
+ 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 block87
+ 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 use61
+}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 arena89
// 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 hit4950
// 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-allocated53
+#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 builds60
+ 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 whole68
+// size class, so a freed one can serve any request of its class) and pops a recycled block of that class if69
+// 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 * 879
+ emit_mov_reg_imm64(code, REG_RAX, kiln_freelist_addr());80
+ emit_add_reg_reg(code, REG_RSI, REG_RAX); // RSI = &head of this class81
+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 0); // RAX = head82
+ 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 block85
+ emit_store_mem_disp32(code, REG_RSI, 0, REG_RDI); // head = next86
+ 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 = &head102
+ emit_load_mem_disp32(code, REG_RCX, REG_RSI, 0);103
+ emit_store_mem_disp32(code, REG_RAX, 0, REG_RCX); // block.next = head104
+ emit_store_mem_disp32(code, REG_RSI, 0, REG_RAX); // head = block105
+ 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 length110
+// prefix sits right before it) goes back on the free list of its class. The class is computed from the111
+// 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); // length115
+ 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 class118
+ emit_sub_reg_imm8(code, REG_RAX, 8); // block start119
+ 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 on123
+// (its 24 bytes were allocated as a 32-byte class block); its element block is capacity * 16 bytes (16 for124
+// 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); // capacity129
+ emit_mov_reg_imm64(code, REG_RCX, 16);130
+ emit_imul_reg_reg(code, REG_RDX, REG_RCX); // element block size131
+ 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 16134
+ emit_patch_jump(code, sized);135
+ emit_load_mem_disp32(code, REG_RAX, REG_RAX, 16); // data_ptr136
+ emit_free_block(code);137
+ emit_pop_reg(code, REG_RAX); // the object itself138
+ 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 a150
+// plain class is one: its fields are all in the literal, so it never grows); entries block = capacity * 32151
+// 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); // capacity155
+ 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_ptr162
+ 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 size242
+ 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 (see116257
// elf/C/elf_so_writer.c) to emit this routine into libkilnrt.so's OWN code117258
// 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 chunk196337
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 prefix8080
8181
emit_push_reg(code, REG_RSI); // heap_emit_alloc clobbers RSI -- save/restore around it82
- heap_emit_alloc(code); // RAX = new block82
+ 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.h1516
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.h9
+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.h10
+ src/app/H/target.h src/codegen/H/emit/value.h1111
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_H2
+#define KILN_MEM_BUILTIN_H3
+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 program11
+// that did not enable the free lists (Windows, --link=shared), are evaluated and ignored: the bump heap12
+// 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 temporaries31
+// (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
+#endifhearth/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
#endifhearth/kiln/src/codegen/H/emit/runtime_layout.hmodified
+4−0@@ -18,6 +18,10 @@1818
#define KILN_TRY_DEPTH_SIZE 81919
2020
21
+// Heads of the free lists of the static builds' allocator (see codegen/C/runtime/heap.c): one 8-byte slot per22
+// 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 is2226
// computed at import time live in a fixed table of (tag, payload) slots in the2327
// globals region (see parser/C/declarations/module_state.c), because a calledhearth/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 is1515
// never invalidated by a later growth step.1616
17
+// Free lists (static Linux builds only, and only for programs that free: the driver calls18
+// heap_enable_free_lists() after lowering when the program contains mem_free / mem_cat). Then the alloc19
+// string alloc routine (heap_emit_alloc_str) rounds every request to 16 bytes and recycles freed blocks of up20
+// to 512 bytes through one list per size class (heads in the globals region, kiln_freelist_addr()). The plain21
+// routine (heap_emit_alloc) is never touched, so arrays, maps and programs that never free cost exactly what22
+// 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 free35
+// 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 the1839
// top-level statement loop): grabs the first chunk and records its start1940
// 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 at3960
// 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 to63
+// 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 for4367
// Pyre's own lowering, e.g. a tight inner loop's scratch buffers thathearth/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 83636
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 state37
+#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 lists3838
#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-executablehearth/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 before3737
// 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, zero40
+// 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 list4245
// in pe_imports.def, shared by this enum and pe_writer.c's actual tablehearth/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
+588890hearth/kiln/tests/cases/memoff/ash.pkgadded
+3−0@@ -0,0 +1,3 @@1
+name: o2
+version: 0.1.03
+memory-safety: offhearth/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 overlap8
+ int[] xs = new int[2];9
+ int[] ys = new int[2];10
+ append(&mut xs, &ys); // different variables: fine11
+ 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
+6abc2hearth/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 moves5
+ }6
+7
+ static int size(String s) { return s.length(); } // only reads: the caller keeps ownership8
+ 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 reader14
+ static String same(String s) { return s; } // returns the caller's value: moves15
+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 iteration22
+ }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 kept2
+r400775560hearth/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 Dogs12
+ }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 it23
+ static int lam(String s) {24
+ Runnable r = () -> sink.add(s); // captured by a lambda25
+ 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 caller31
+ }32
+ static int pointSum(Point p) { return p.x + p.y; } // reads fields only: a plain class with no subclass is fine33
+ static String call(Animal a) { return a.sound(); } // may run Dog.sound: never borrowed34
+ static int overwrite(String s) {35
+ s = s + "!"; // the parameter is a copy: the caller's value is untouched36
+ 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 sink50
+ total += lam(d); // d now lives in sink51
+ 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 animalhearth/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 iteration6
+ int[] xs = new int[16]; // fresh array, sole owner7
+ xs[3] = i;8
+ String t = s + "!";9
+ t += s; // reassigned with a fresh value: the old one is freed first10
+ if (i % 100000 == 0) {11
+ System.out.println(t);12
+ continue; // continue frees what the iteration owns13
+ }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-02
+item-100000!item-1000003
+item-200000!item-2000004
+15566524hearth/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 fresh11
+ }12
+13
+ static class Box {14
+ String held;15
+ void keep(String s) { held = s; } // keeps its argument16
+ }17
+18
+ static String grow(int n) {19
+ if (n == 0) return "";20
+ String rest = grow(n - 1); // recursive: never treated as fresh21
+ 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 field31
+ 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 owners34
+ String v = (i % 3 == 0) ? t : u; // aliases: t and u must not be freed35
+ kept.add("k" + i); // fresh temp handed to a collection: never freed here36
+ if (i % 5000 == 0) box.keep("b" + i); // the argument outlives the iteration37
+ 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 array41
+ 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 sharedhearth/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 Bag7
+ 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 list16
+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 list21
+ 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 read26
+27
+ List<String> b = new ArrayList<>(); // a literal element: the list is freed, the literal never is28
+ 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 alias33
+ 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 only38
+ 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 store43
+ 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 escapes50
+ 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
+b02
+b100003
+1211120 g9hearth/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 exact16
+ }17
+18
+ static Point make(int i) {19
+ Point p = new Point(i, 1); // sole owner, handed to the caller20
+ 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 fresh29
+ }30
+31
+ static Point cached = new Point(5, 5);32
+ static Point shared() {33
+ return cached; // an alias of a static: never fresh34
+ }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 iteration40
+ Point b = origin(); // freed each iteration41
+ Shape s = shape(i);42
+ Point c = shared(); // alias: must survive43
+ 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 10hearth/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: owned4
+ String label; // only ever given fresh concatenations: owned5
+ String shared = "shared"; // a literal initializer is shared: never owned6
+ int[] borrowed; // assigned from a parameter: never owned7
+ String leaked; // returned by a getter: never owned8
+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 value16
+ 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 value18
+ }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 field25
+ Buffer b = new Buffer(i, given); // freed each iteration together with cells and label26
+ b.relabel(i);27
+ sum += b.total();28
+ if (i == 7) kept = b.leak(); // aliases leaked: it must survive b29
+ }30
+ System.out.println(sum + " " + kept);31
+ }32
+}hearth/kiln/tests/cases/ownership/OwnFreeFields.outadded
+1−0@@ -0,0 +1 @@1
+417780 leak7hearth/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 time8
+ if (acc.length() > 6) break; // break frees acc's loop-scoped siblings only9
+ }10
+ String out = acc + "|" + n; // fresh; escapes through return below, so it is never freed11
+ 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 value18
+ 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 freed27
+ String again = shared; // moved28
+ 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/box39
+ }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|92
+7 103
+s044
+line 0line 0125
+line 026
+line 1line 1127
+line 2line 2128
+line 22hearth/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 methods7
+ List<Integer> view; // assigned from elsewhere: shared8
+ 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 caller20
+ }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 iteration28
+ 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 factory33
+ for (int y : ys) total += y;34
+ List<Integer> zs = new ArrayList<>(); // passed on: not freed35
+ 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 sees44
+ 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 7hearth/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
+92
+-r0c24;-4;-r2c23
+fin a0d4
+fin a1d5
+fin a2d6
+3 4 37
+208
+p1!q2!r3!3hearth/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 field11
+ 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 only21
+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 iteration27
+ 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 owner42
+ 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 freed50
+ 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 1hearth/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 freed14
+ }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 iteration22
+ 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 owner31
+ 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
+42
+1000043
+20000799990 15hearth/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 fresh10
+ }11
+12
+ static String name = "shared";13
+ static String alias(int i) { return name; } // not fresh: callers must never free it14
+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 iteration25
+ String b = pick(i);26
+ String c = alias(i); // aliases a static: never freed27
+ 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 sharedhearth/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 computed7
+ 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 continue19
+ 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 handler34
+ 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 22
+finally guard1 23
+6 -14
+body loop05
+fin loop06
+caught t17
+2 -28
+zero0!one3dflt9hearth/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 usable7
+ String b = a; // a moved into b8
+ a = "again"; // reinitialised: usable again9
+ System.out.println(n + b + a);10
+11
+ String c = "x";12
+ if (n > 100) {13
+ String d = c; // moved on a path that returns14
+ 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 iteration21
+ 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 left27
+ System.out.println(h);28
+ break;29
+ }30
+ }31
+}hearth/kiln/tests/cases/ownership/OwnOk.outadded
+6−0@@ -0,0 +1,6 @@1
+10helloagain2
+x3
+iter04
+iter15
+iter26
+ghearth/kiln/tests/cases/ownership/ash.pkgadded
+3−0@@ -0,0 +1,3 @@1
+name: ownership-tests2
+version: 0.1.03
+memory-safety: onhearth/kiln/tests/scripts/memory_check.shadded
+52−0@@ -0,0 +1,52 @@1
+#!/usr/bin/env bash2
+# Memory stays bounded: a loop that builds three million short-lived strings and then 300 thousand arrays must finish in a3
+# few MB with memory-safety on, and the same loop with it off keeps everything (hundreds of MB), which shows4
+# the check is measuring the freeing and not just a small program.5
+set -uo pipefail6
+# Hard cap on every program this script starts (virtual memory, 3 GB): the off-mode run keeps everything7
+# on purpose, and a bug must not be able to take the machine down with it.8
+ulimit -v 30000009
+cd "$(dirname "$0")/../.." # -> project root10
+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"' EXIT13
+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; do17
+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
+JAVA37
+done38
+peak() { python3 - "$@" <<'PY'39
+import resource, subprocess, sys40
+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
+PY43
+}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 * a3131
3232
STACK = 0x7fff000000003333
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 it3535
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_H2
+#define MNEMO_FLOW_H3
+4
+#include <stdint.h>5
+6
+/* Mnemosyne move-flow engine (Rust, hearth/mnemo/Rust/src/flow.rs), shared by ashvm and kiln through7
+ * 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 of10
+ * variable ids moved on the current path, whether the path is dead, and saved copies ("snapshots") used11
+ * to fork at a branch and join afterwards. Ids are opaque 64-bit values chosen by the caller (the12
+ * ownership pass uses the address of the variable).13
+ *14
+ * Ownership: mnemo_walk_new allocates a MnemoWalk that the caller frees exactly once with15
+ * mnemo_walk_free. Snapshot handles belong to their walk; 0 is never valid; release them with16
+ * 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
+#endifhearth/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 both2
+# 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.a6
+CRATE = Rust7
+8
+.DEFAULT_GOAL := all9
+all: $(LIB)10
+11
+$(LIB): FORCE12
+ @test -x "$(CARGO)" || { echo "mnemo: cargo not found; run tools/install.sh" >&2; exit 1; }13
+ cd $(CRATE) && "$(CARGO)" build --release --quiet14
+ mkdir -p ../../bin/lib15
+ cp $(CRATE)/target/release/libmnemo.a $(LIB)16
+17
+FORCE:18
+19
+clean:20
+ rm -f $(LIB)21
+ rm -rf $(CRATE)/target22
+23
+.PHONY: all clean FORCEhearth/mnemo/O/.gitkeepadded
+0−0No 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
+## Layout20
+21
+```22
+bin/23
+├── kiln24
+├── ashvm25
+└── lib/26
+ ├── front.a27
+ └── mnemo.a28
+```29
+30
+## What is in it31
+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 safety38
+└── tests/ C programs that call the public ABI: tests/scripts/run_regression.sh39
+```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 filehearth/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 = 44
+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 = false7
+8
+[lib]9
+name = "mnemo"10
+crate-type = ["staticlib"]11
+12
+[profile.release]13
+opt-level = 314
+panic = "abort"15
+lto = true16
+codegen-units = 1hearth/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 calls2
+//! in here for every event; this side owns the dataflow state, so the "moved" sets and their joins at3
+//! 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 join10
+ 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 valid35
+ 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
+ i45
+ }46
+ None => {47
+ self.snaps.push(Some(st));48
+ self.snaps.len() - 149
+ }50
+ };51
+ (i + 1) as u3252
+ }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
+ 0137
+ }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
+ 0149
+ }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
+ 0163
+ }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 bash2
+# mnemo/tests/scripts/run_regression.sh3
+# Builds mnemo.a and runs the C programs in tests/cases/*/ against its public C ABI.4
+set -uo pipefail5
+cd "$(dirname "$0")/../.." # -> mnemo/6
+make -s || { echo "build failed"; exit 1; }7
+TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT8
+fail=09
+for f in tests/cases/*/*.c; do10
+ 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; fi12
+done13
+exit $fail