Skip to content

moon Phase 1 (start): ARC reclamation engine#86

Merged
NiceAndPeter merged 2 commits into
mainfrom
phase1/arc-core
Jun 27, 2026
Merged

moon Phase 1 (start): ARC reclamation engine#86
NiceAndPeter merged 2 commits into
mainfrom
phase1/arc-core

Conversation

@NiceAndPeter

Copy link
Copy Markdown
Owner

First increment of Phase 1 — ARC core: the reclamation engine that replaces tracing as moon's memory model.

What's implemented

  • moonC_retain(o) — increment refcount.
  • moonC_release(L, o) — decrement; at zero, recursively release the object's GC children, unlink from allgc, and reclaim via the existing freeobj(). The child traversal is a release-instead-of-mark mirror of the marking code (tables incl. metatable/array/hash keys+values, Lua/C closures, protos, userdata; strings/numbers are leaves). Reference cycles never reach zero from an external release → they leak by design.
  • Debug deinit counter (moonC_deinitcount) for verification.
  • test_arc.cpp (GCC-only) drives the engine on real object graphs.

Verification

  • Acyclic graph (root → child table + leaf string): releasing the root deterministically reclaims all 3 objects (recursive cascade).
  • Cycle (x ↔ y): dropping both external refs reclaims 0 (leaks by design) and does not crash.
  • Clean under ASan + UBSan (leak detection on — moon_close reclaims the leaked cycle at shutdown).

Scope / what's next (honest status)

This is the reclamation engine — the hard algorithmic core — verified in isolation. It is not yet wired into the VM, so ordinary moon programs don't auto-free yet. The next increment (the larger, more invasive part) wires retain/release into every TValue slot write across the stack and tables, which requires the init-vs-assign / nil-on-free stack discipline to stay use-after-free-free. Tracked as tasks #8/#9.

Builds green; Phase 0 smoke still passes (engine is additive, not yet on the hot path).

🤖 Generated with Claude Code

Peter Neiss and others added 2 commits June 27, 2026 12:51
… free)

Implements the core of automatic reference counting — the algorithmic heart of
the post-GC memory model:
- moonC_retain(o): increment refcount.
- moonC_release(L, o): decrement; at zero, recursively release the object's GC
  children (a release-instead-of-mark mirror of the marking traversal — tables,
  closures, protos, userdata; strings/numbers are leaves), unlink from the
  global 'allgc' list, and reclaim via the existing freeobj(). Reference cycles
  never reach zero from an external release, so they leak by design.
- A debug deinit counter (moonC_deinitcount / reset) for verification.

Adds test_arc.cpp (GCC-only, like test_moonallocator) driving the engine on real
object graphs with explicit ownership modelling (the VM/stack is not yet
ARC-instrumented). Verified: an acyclic graph (root -> child table + leaf string)
is reclaimed deterministically (3/3 objects), and an x<->y cycle leaks (0 freed)
without crashing. Clean under ASan + UBSan with leak detection on (moon_close
reclaims the leaked cycle at shutdown).

NOT yet done (next increment): wiring retain/release into every TValue slot write
across the VM stack and tables so ordinary moon programs free automatically —
that needs the init-vs-assign / nil-on-free stack discipline and is the larger
Phase 1 follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Refactor the ARC engine to a deferred-free model and wire it into table stores.

Engine (mgc):
- moonC_incref/moonC_decref adjust refcounts WITHOUT needing a moon_State —
  decref queues zero-count objects on a pending list. moonC_drain(L) reclaims
  the queue at a safe point, iteratively releasing children (no deep recursion,
  no freeing mid-write). moonC_release(L,o) = decref + drain. This solves the
  problem that the low-level table write primitives have no L to free with.

Tables (mtable):
- Table::set / Table::setInt now do ARC accounting: capture the old value, retain
  the new value (and, for a new entry, a collectable key), then release the old
  value after the write. resize() moves entries via insertkey and bypasses this
  path, so rehash moves are not double-counted. (Fixed an uninitialized
  capture-slot that caused a spurious decref.)

Safety: the interpreter never calls moonC_drain yet, so no object is freed during
normal execution — stores/overwrites/removes just adjust counts (which leak,
exactly as in Phase 0). Real frees arrive once the stack is ARC-instrumented and
drain is called at safe points. test_arc now builds graphs through the real
Table::setInt API (no manual retain) and verifies: acyclic graph fully reclaimed
(3/3), cycle leaks (0), clean under ASan+UBSan. Full interpreter smoke + a
table build/overwrite/remove/iterate script pass.

Not yet wired: table array-part fast paths and VM fast-set bypass Table::set/
setInt (safe while drain is off); the stack; calling drain at runtime.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@NiceAndPeter
NiceAndPeter merged commit 9ba8f1c into main Jun 27, 2026
6 checks passed
@NiceAndPeter
NiceAndPeter deleted the phase1/arc-core branch June 27, 2026 11:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant