-
Notifications
You must be signed in to change notification settings - Fork 0
Memory Ownership
A whole-program escape/ownership analysis decides what each class frees, erring toward a leak (safe) over a double-free:
- destructors free what a class
newed (and the typed pointer handed to a base'svoid*field); - short-lived heap locals are freed at scope close, and before every early
return; - owned pointer fields are NULL-initialized, freed before reassignment, and freed in the destructor;
- owned containers are walked and freed element-by-element.
Borrowed dependencies, value containers, objects owned by a receiver, and fields handed back out of
the object are left alone. A field reference is recognized whether written this.field or bare
field (Haxe lets you omit this.), so ownership does not hinge on the qualifier.
For an injected pointer the class stores but did not new — where own-vs-borrow is not statically
decidable — mark the field @owned to have the destructor free it (a scalar with delete, a
container element-by-element); the local-scope counterpart @delete var x = … frees a marked
local at scope close. Unmarked injected pointers stay borrowed.
These overrides are obeyed but advisory-checked — the analysis warns when a tag looks unsound
(e.g. an @owned field that is also handed out); auto-inferring an injected pointer's ownership would
need interprocedural call-site analysis and is a future improvement.
A new passed to a constructor parameter the class owns is emitted inline — the constructed
object frees it — rather than hoisted into a scope-owned local that would double-free it.
@sink marks a value as handed off — the current scope no longer frees it. It can sit on a consuming
parameter, on a call argument, or on a local declaration; all three say the same thing (ownership
leaves here) and share the same conservative trust model.
A method/function parameter can be marked @sink — function setKey(key, @sink val:JValue) —
a consuming parameter that takes ownership across the call (distinct from @owned, which says a
field frees its member when the object dies; @sink says ownership transfers in at this call): a
new handed to a @sink position is emitted inline, and an owned local handed there has its
scope-close free dropped, so the value is freed once (by the receiver) rather than dangled by the
caller. This is the explicit answer to a retaining method (store(x) that keeps x), which an
intraprocedural analysis cannot otherwise see; @sink on a by-value parameter (where there is nothing
to consume) is flagged as a no-op.
@sink may also be written at the call site, on the argument itself — new Quad(@sink new Vertex(x, y))
or foo(@sink v). (Haxe permits metadata on any expression; Hatchet parses it, and any expression-position
metadata other than @sink is inert, as it is under hxcpp.) A call-site @sink transfers ownership of that
argument to the callee exactly as the parameter form does — a @sink new X(...) is emitted inline and a
@sink local has its scope-close free dropped:
var quad = new Quad(
@sink new Vertex(x, y),
@sink new Vertex(x + cell, y));
// → Quad* quad = new Quad(new Vertex(x, y), new Vertex(x + cell, y));
// delete quad; // the vertices are transferred; the caller frees only `quad`A local declaration can be marked @sink var x = new X(...), which suppresses the scope-close free
of x entirely — the exact inverse of @delete var (which adds one). Use it when ownership is handed off
with no single call to attach the marker to (x is stored through a native API, registered in a global, or
otherwise leaves the scope in a way the analysis cannot follow):
@sink var s:Vertex = new Vertex(engine, x, y);
// → Vertex* s = new Vertex(engine, x, y); // and NO `delete s;` at scope closeThe marker is in every case an assertion that ownership leaves the current scope, so it applies even when
the destination is opaque to the analysis (an @:native or otherwise unresolved class) — it overrides the
inferred ownership. If nothing actually takes ownership the result is a leak, never a double-free. @sink on
a value local or argument (a struct/array/map/primitive — nothing to hand off) is flagged as a no-op.
A new pushed into a container that escapes the scope — a class-owned field container, or a local
container that is returned/stored — likewise comes to rest there and is not freed locally.
On exception unwind (a throw inside a try), the scope-close frees do not run, so owned objects
created in the try before the throw leak — a deliberate extension of the conservative bias
(never a double-free/use-after-free); free them in the catch if it matters. (Exceptions must be
enabled on the target — VC6 /GX; g++ enables them by default.)
The @owned / @sink / @delete tags are summarized alongside the rest in Metadata.
Property setters interact with ownership — see Members & Access.
Hatchet is licensed under the MIT License — see LICENSE. (c) 2026 Andrew Grant Lind
Getting Started
Language Support
- Declarations
- Value Types & Abstracts
- Members & Access
- Statements & Expressions
- Types & Nullability
- Conditional Compilation
- Memory Ownership
Semantics & Interop
Internals