Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,21 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
${CMAKE_CURRENT_SOURCE_DIR}/src/vm
${CMAKE_CURRENT_SOURCE_DIR}/src/serialization
)

# ARC reclamation engine test (moon fork, Phase 1)
add_executable(test_arc test_arc.cpp)
target_link_libraries(test_arc PRIVATE libmoon_static)
target_compile_options(test_arc PRIVATE -Wall ${WARNING_FLAGS} ${OPTIMIZE_FLAGS})
target_include_directories(test_arc PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src/testing
${CMAKE_CURRENT_SOURCE_DIR}/src/memory
${CMAKE_CURRENT_SOURCE_DIR}/src/memory/gc
${CMAKE_CURRENT_SOURCE_DIR}/src/core
${CMAKE_CURRENT_SOURCE_DIR}/src/objects
${CMAKE_CURRENT_SOURCE_DIR}/src/compiler
${CMAKE_CURRENT_SOURCE_DIR}/src/vm
${CMAKE_CURRENT_SOURCE_DIR}/src/serialization
)
endif()

# Install targets
Expand Down
129 changes: 129 additions & 0 deletions src/memory/mgc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "mprefix.h"

#include <cstring>
#include <vector>


#include "moon.h"
Expand Down Expand Up @@ -437,6 +438,134 @@ void freeobj (moon_State& L, GCObject *o) {
}


/*
** {======================================================
** ARC (automatic reference counting) reclamation engine — moon fork, Phase 1
**
** moonC_release decrements an object's refcount; when it reaches zero the object
** is reclaimed deterministically: its GC children are released first (which may
** cascade), it is unlinked from the global 'allgc' list, and its memory is
** freed via freeobj(). A reference cycle never reaches zero from an external
** release, so cycles leak by design (broken later with weak refs).
**
** This is the reclamation CORE. Wiring retain/release into every TValue slot
** write across the VM/stack is a later increment; for now retain is wired into
** container stores (see Table) so the engine can be exercised end to end.
** =======================================================
*/

static unsigned long long arc_deinit_count = 0; // objects reclaimed (debug)

// Deferred-free queue. moonC_decref drops a reference WITHOUT needing a
// moon_State (so it can be called from the L-free table write primitives); when
// a count hits zero the object is queued here, and moonC_drain() reclaims the
// queue at a safe point where L is available. This decouples the cheap decrement
// from the actual free, avoids deep recursion, and avoids freeing mid-write.
// (A single global queue assumes objects from at most one live state are pending
// at a time — sufficient for the fork's single-state usage; revisit for
// multi-state embedding.)
static std::vector<GCObject*> arc_pending;

unsigned long long moonC_deinitcount() noexcept { return arc_deinit_count; }
void moonC_resetdeinitcount() noexcept { arc_deinit_count = 0; }

void moonC_decref(GCObject* o) noexcept {
if (o != nullptr && o->release() == 0) // last reference dropped?
arc_pending.push_back(o); // queue for reclamation at next drain
}

static void arc_decrefvalue(const TValue* v) noexcept {
if (iscollectable(v)) moonC_decref(gcvalue(v));
}

// Queue every GC child of 'o' for decref (a decref-instead-of-mark mirror of the
// marking traversal). Leaf types (strings, numbers) have no children. Threads
// and open upvalues are intentionally skipped for now (handled in a later
// increment); skipping only leaks, it is never unsafe.
static void arc_decrefchildren(GCObject* o) {
switch (static_cast<int>(o->getType())) {
case static_cast<int>(ctb(MoonT::TABLE)): {
Table* h = gco2t(o);
if (h->getMetatable() != nullptr)
moonC_decref(obj2gco(h->getMetatable()));
for (unsigned i = 0; i < h->arraySize(); i++)
moonC_decref(gcvalarr(h, i));
Node* limit = gnodelast(h);
for (Node* n = gnode(h, 0); n < limit; n++) {
if (!isempty(gval(n))) {
if (n->isKeyCollectable())
moonC_decref(n->getKeyGC());
arc_decrefvalue(gval(n));
}
}
break;
}
case static_cast<int>(ctb(MoonT::LCL)): {
LClosure* cl = gco2lcl(o);
if (cl->getProto() != nullptr) moonC_decref(obj2gco(cl->getProto()));
for (int i = 0; i < cl->getNumUpvalues(); i++)
if (cl->getUpval(i) != nullptr) moonC_decref(obj2gco(cl->getUpval(i)));
break;
}
case static_cast<int>(ctb(MoonT::CCL)): {
CClosure* cl = gco2ccl(o);
for (int i = 0; i < cl->getNumUpvalues(); i++)
arc_decrefvalue(cl->getUpvalue(i));
break;
}
case static_cast<int>(ctb(MoonT::PROTO)): {
Proto* p = gco2p(o);
if (p->getSource() != nullptr) moonC_decref(obj2gco(p->getSource()));
for (auto& constant : p->getConstantsSpan())
arc_decrefvalue(&constant);
for (Proto* nested : p->getProtosSpan())
if (nested != nullptr) moonC_decref(obj2gco(nested));
break;
}
case static_cast<int>(ctb(MoonT::USERDATA)): {
Udata* u = gco2u(o);
if (u->getMetatable() != nullptr) moonC_decref(obj2gco(u->getMetatable()));
for (int i = 0; i < u->getNumUserValues(); i++)
arc_decrefvalue(&u->getUserValue(i)->value);
break;
}
default:
break; // leaves (strings/numbers) and not-yet-handled types (thread/upval)
}
}

// Unlink 'o' from the global 'allgc' list (O(n); a list-free design or a
// doubly-linked list is a later performance increment).
static void arc_unlink(moon_State& L, GCObject* o) {
GCObject** p = G(L)->getAllGCPtr();
while (*p != nullptr) {
if (*p == o) { *p = o->getNext(); return; }
p = (*p)->getNextPtr();
}
}

// Reclaim all queued (zero-refcount) objects. Processing is iterative: releasing
// an object's children may queue more, which this loop drains in turn. Cycles
// never enter the queue (their counts never reach zero), so they are not freed.
void moonC_drain(moon_State& L) {
while (!arc_pending.empty()) {
GCObject* o = arc_pending.back();
arc_pending.pop_back();
arc_decrefchildren(o); // queue children (cascade)
arc_unlink(L, o); // remove from the global object list
++arc_deinit_count;
freeobj(L, o); // reclaim memory
}
}

void moonC_release(moon_State& L, GCObject* o) {
moonC_decref(o);
moonC_drain(L);
}

// }======================================================


/*
** sweep at most 'countin' elements from a list of GCObjects erasing dead
** objects, where a dead object is one marked with the old (non current)
Expand Down
14 changes: 14 additions & 0 deletions src/memory/mgc.h
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,20 @@ inline void moonC_barrierback(moon_State* L, GCObject* p, const TValue* v) noexc

// Use GCObject::fix() method instead of moonC_fix
MOONI_FUNC void moonC_freeallobjects (moon_State& L);

// ARC (automatic reference counting) reclamation engine — moon fork, Phase 1.
// Deferred model: moonC_incref/moonC_decref adjust an object's refcount without
// needing a moon_State (decref queues zero-count objects); moonC_drain(L)
// reclaims the queue at a safe point, recursively releasing children. Cycles
// never reach zero and so leak by design. moonC_release(L,o) is decref+drain for
// convenience. The deinit counter is a debug aid for tests.
inline void moonC_incref (GCObject *o) noexcept { o->retain(); }
MOONI_FUNC void moonC_decref (GCObject *o) noexcept;
MOONI_FUNC void moonC_drain (moon_State& L);
MOONI_FUNC void moonC_release (moon_State& L, GCObject *o);
inline void moonC_retain (GCObject *o) noexcept { o->retain(); } // alias of incref
MOONI_FUNC unsigned long long moonC_deinitcount() noexcept;
MOONI_FUNC void moonC_resetdeinitcount() noexcept;
// moonC_step and moonC_fullgc declared earlier for template functions
MOONI_FUNC void moonC_runtilstate (moon_State& L, GCState state, int fast);
MOONI_FUNC void propagateall (GlobalState& g); // used by GCCollector
Expand Down
13 changes: 13 additions & 0 deletions src/objects/mtable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1324,12 +1324,24 @@ int Table::psetStr(TString* key, TValue* val) {
}

void Table::set(moon_State* L, const TValue* key, TValue* value) {
// ARC accounting (moon fork): a table slot owns its value, and a NEW entry
// also owns its (collectable) key. Capture the old value, retain the new
// value/key before the write, release the old value after. 'resize' moves
// entries via insertkey and bypasses this path, so moves are not recounted.
TValue oldv; oldv.setNil(); (void)get(key, &oldv);
const bool newentry = ttisnil(&oldv);
if (iscollectable(value)) moonC_incref(gcvalue(value));
if (newentry && iscollectable(key)) moonC_incref(gcvalue(key));
int hres = pset(key, value);
if (hres != HOK)
finishSet(L, key, value, hres);
if (iscollectable(&oldv)) moonC_decref(gcvalue(&oldv));
}

void Table::setInt(moon_State* L, moon_Integer key, TValue* value) {
// ARC accounting (see Table::set). Integer keys are not collectable.
TValue oldv; oldv.setNil(); (void)getInt(key, &oldv);
if (iscollectable(value)) moonC_incref(gcvalue(value));
unsigned ik = ikeyinarray(this, key);
if (ik > 0)
obj2arr(this, ik - 1, value);
Expand All @@ -1341,6 +1353,7 @@ void Table::setInt(moon_State* L, moon_Integer key, TValue* value) {
moonH_newkey(L, *this, &k, value);
}
}
if (iscollectable(&oldv)) moonC_decref(gcvalue(&oldv));
}

void Table::finishSet(moon_State* L, const TValue* key, TValue* value, int hres) {
Expand Down
88 changes: 88 additions & 0 deletions test_arc.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// ARC reclamation engine test — moon fork, Phase 1.
//
// Exercises moonC_retain / moonC_release directly on real object graphs, with
// explicit ownership modelling (retain on store, release on drop), since the
// VM/stack is not yet ARC-instrumented. Verifies:
// * an acyclic graph is reclaimed deterministically (whole subtree frees), and
// * a reference cycle leaks (no free) without crashing.

#include <cassert>
#include <cstdio>

#include "moon.h"
#include "mauxlib.h"

#include "mstate.h"
#include "mobject.h"
#include "mtable.h"
#include "mstring.h"
#include "mgc.h"

static int failures = 0;
static void expect(bool cond, const char* what) {
if (cond) { std::printf(" ok: %s\n", what); }
else { std::printf(" FAIL: %s\n", what); ++failures; }
}

// Store value 'v' into table 't' at integer key 'k'. Table::setInt now performs
// ARC accounting itself (retains the stored value), so no manual retain here.
static void storeInt(moon_State* L, Table* t, moon_Integer k, const TValue* v) {
TValue tmp; tmp = *v;
t->setInt(L, k, &tmp);
}

int main() {
moon_State* L = moonL_newstate();
if (L == nullptr) { std::printf("could not create state\n"); return 1; }

// ---- Acyclic graph: root -> {childTable, string}; releasing root frees all 3.
{
unsigned long long before = moonC_deinitcount();

Table* root = Table::create(L); // refcount 1 (birth)
Table* child = Table::create(L); // refcount 1 (birth)
TString* str = TString::create(L, "arc-leaf-unique-xyz"); // refcount 1 (birth)

TValue v;
sethvalue(L, &v, child); storeInt(L, root, 1, &v); // setInt retains child -> rc 2
setsvalue(L, &v, str); storeInt(L, root, 2, &v); // setInt retains str -> rc 2

// Drop the birth references of the children — root is now their sole owner.
moonC_release(*L, obj2gco(child)); // child rc 1
moonC_release(*L, obj2gco(str)); // str rc 1

// Drop root's last reference: frees root, then cascades to child and str.
moonC_release(*L, obj2gco(root));

unsigned long long freed = moonC_deinitcount() - before;
std::printf("acyclic: reclaimed %llu objects (expected 3)\n", freed);
expect(freed == 3, "acyclic graph fully reclaimed (root + child + leaf)");
}

// ---- Cycle: x <-> y. Dropping both birth refs leaves a self-sustaining
// cycle (each still has refcount 1 from the other) -> leaks, no crash.
{
unsigned long long before = moonC_deinitcount();

Table* x = Table::create(L); // refcount 1
Table* y = Table::create(L); // refcount 1

TValue v;
sethvalue(L, &v, y); storeInt(L, x, 1, &v); // setInt retains y -> rc 2
sethvalue(L, &v, x); storeInt(L, y, 1, &v); // setInt retains x -> rc 2

// Drop both birth references. Now x and y reference only each other.
moonC_release(*L, obj2gco(x)); // x rc 1 (held by y)
moonC_release(*L, obj2gco(y)); // y rc 1 (held by x)

unsigned long long freed = moonC_deinitcount() - before;
std::printf("cyclic: reclaimed %llu objects (expected 0 — cycle leaks)\n", freed);
expect(freed == 0, "reference cycle leaks (not reclaimed) and does not crash");
}

moon_close(L);

if (failures == 0) std::printf("ARC engine test: ALL OK\n");
else std::printf("ARC engine test: %d FAILURE(S)\n", failures);
return failures == 0 ? 0 : 1;
}
Loading