# Plan: Complete Copying GC Implementation ## Overview Remove reference counting (DupValue/FreeValue) entirely and complete the Cheney copying garbage collector. Each JSContext will use bump allocation from a heap block, and when out of memory, request a new heap from JSRuntime's buddy allocator and copy live objects to the new heap. ## Target Architecture (from docs/memory.md) ### Object Types (simplified from current): **Type 0 - Array**: `{ header, length, elements[] }` **Type 1 - Blob**: `{ header, length, bits[] }` **Type 2 - Text**: `{ header, length_or_hash, packed_chars[] }` **Type 3 - Record**: `{ header, prototype, length, key_value_pairs[] }` **Type 4 - Function**: `{ header, code_ptr, outer_frame_ptr }` - 3 words only, always stone **Type 5 - Frame**: `{ header, function_ptr, caller_ptr, ret_addr, args[], closure_vars[], local_vars[], temps[] }` **Type 6 - Code**: Lives in immutable memory only, never copied **Type 7 - Forward**: Object has moved; cap56 contains new address ### Key Design Points: - **JSFunction** is just a pointer to code and a pointer to the frame that created it (3 words) - **Closure variables live in frames** - when a function returns, its frame is "reduced" to just the closure variables - **Code objects are immutable** - stored in stone memory, never copied during GC - **Frame reduction**: When a function returns, `caller` is set to zero, signaling the frame can be shrunk ## Current State (needs refactoring) 1. **Partial Cheney GC exists** at `source/quickjs.c:1844-2030`: `ctx_gc`, `gc_copy_value`, `gc_scan_object` 2. **744 calls to JS_DupValue/JS_FreeValue** scattered throughout (currently undefined, causing compilation errors) 3. **Current JSFunction** is bloated (has kind, name, union of cfunc/bytecode/bound) - needs simplification 4. **Current JSVarRef** is a separate object - should be eliminated, closures live in frames 5. **Bump allocator** in `js_malloc` (line 1495) with `heap_base`/`heap_free`/`heap_end` 6. **Buddy allocator** for memory blocks (lines 1727-1837) 7. **Header offset inconsistency** - some structs have header at offset 0, some at offset 8 ## Implementation Steps ### Phase 1: Define No-Op DupValue/FreeValue (To Enable Compilation) Add these near line 100 in `source/quickjs.c`: ```c /* Copying GC - no reference counting needed */ #define JS_DupValue(ctx, v) (v) #define JS_FreeValue(ctx, v) ((void)0) #define JS_DupValueRT(rt, v) (v) #define JS_FreeValueRT(rt, v) ((void)0) ``` This makes the code compile while keeping existing call sites (they become no-ops). ### Phase 2: Standardize Object Headers (offset 0) Remove `JSGCObjectHeader` (ref counting remnant) and put `objhdr_t` at offset 0: ```c typedef struct JSArray { objhdr_t hdr; // offset 0 word_t length; JSValue values[]; } JSArray; typedef struct JSRecord { objhdr_t hdr; // offset 0 JSRecord *proto; word_t length; slot slots[]; } JSRecord; typedef struct JSText { objhdr_t hdr; // offset 0 word_t length; // pretext: length, text: hash word_t packed[]; } JSText; typedef struct JSBlob { objhdr_t hdr; // offset 0 word_t length; uint8_t bits[]; } JSBlob; /* Simplified JSFunction per memory.md - 3 words */ typedef struct JSFunction { objhdr_t hdr; // offset 0, always stone JSCode *code; // pointer to immutable code object struct JSFrame *outer; // frame that created this function } JSFunction; /* JSFrame per memory.md */ typedef struct JSFrame { objhdr_t hdr; // offset 0 JSFunction *function; // function being executed struct JSFrame *caller; // calling frame (NULL = reduced/returned) word_t ret_addr; // return instruction address JSValue slots[]; // args, closure vars, locals, temps } JSFrame; /* JSCode - always in immutable (stone) memory */ typedef struct JSCode { objhdr_t hdr; // offset 0, always stone word_t arity; // max number of inputs word_t frame_size; // capacity of activation frame word_t closure_size; // reduced capacity for returned frames word_t entry_point; // address to begin execution word_t disruption_point;// address of disruption clause uint8_t bytecode[]; // actual bytecode } JSCode; ``` ### Phase 3: Complete gc_object_size for All Types Update `gc_object_size` (line 1850) to read header at offset 0: ```c static size_t gc_object_size(void *ptr) { objhdr_t hdr = *(objhdr_t*)ptr; // Header at offset 0 uint8_t type = objhdr_type(hdr); uint64_t cap = objhdr_cap56(hdr); switch (type) { case OBJ_ARRAY: return sizeof(JSArray) + cap * sizeof(JSValue); case OBJ_BLOB: return sizeof(JSBlob) + (cap + 7) / 8; // cap is bits case OBJ_TEXT: return sizeof(JSText) + ((cap + 1) / 2) * sizeof(uint64_t); case OBJ_RECORD: return sizeof(JSRecord) + (cap + 1) * sizeof(slot); // cap is mask case OBJ_FUNCTION: return sizeof(JSFunction); // 3 words case OBJ_FRAME: return sizeof(JSFrame) + cap * sizeof(JSValue); // cap is slot count case OBJ_CODE: return 0; // Code is never copied (immutable) default: return 64; // Conservative fallback } } ``` ### Phase 4: Complete gc_scan_object for All Types Update `gc_scan_object` (line 1924): ```c static void gc_scan_object(JSContext *ctx, void *ptr, uint8_t **to_free, uint8_t *to_end) { objhdr_t hdr = *(objhdr_t*)ptr; uint8_t type = objhdr_type(hdr); switch (type) { case OBJ_ARRAY: { JSArray *arr = (JSArray*)ptr; for (uint32_t i = 0; i < arr->length; i++) { arr->values[i] = gc_copy_value(ctx, arr->values[i], to_free, to_end); } break; } case OBJ_RECORD: { JSRecord *rec = (JSRecord*)ptr; // Copy prototype if (rec->proto) { JSValue proto_val = JS_MKPTR(rec->proto); proto_val = gc_copy_value(ctx, proto_val, to_free, to_end); rec->proto = (JSRecord*)JS_VALUE_GET_PTR(proto_val); } // Copy table entries uint32_t mask = objhdr_cap56(rec->hdr); for (uint32_t i = 1; i <= mask; i++) { // Skip slot 0 JSValue k = rec->slots[i].key; if (!rec_key_is_empty(k) && !rec_key_is_tomb(k)) { rec->slots[i].key = gc_copy_value(ctx, k, to_free, to_end); rec->slots[i].value = gc_copy_value(ctx, rec->slots[i].value, to_free, to_end); } } break; } case OBJ_FUNCTION: { JSFunction *func = (JSFunction*)ptr; // Code is immutable, don't copy - but outer frame needs copying if (func->outer) { JSValue outer_val = JS_MKPTR(func->outer); outer_val = gc_copy_value(ctx, outer_val, to_free, to_end); func->outer = (JSFrame*)JS_VALUE_GET_PTR(outer_val); } break; } case OBJ_FRAME: { JSFrame *frame = (JSFrame*)ptr; // Copy function pointer if (frame->function) { JSValue func_val = JS_MKPTR(frame->function); func_val = gc_copy_value(ctx, func_val, to_free, to_end); frame->function = (JSFunction*)JS_VALUE_GET_PTR(func_val); } // Copy caller (unless NULL = reduced frame) if (frame->caller) { JSValue caller_val = JS_MKPTR(frame->caller); caller_val = gc_copy_value(ctx, caller_val, to_free, to_end); frame->caller = (JSFrame*)JS_VALUE_GET_PTR(caller_val); } // Copy all slots (args, closure vars, locals, temps) uint32_t slot_count = objhdr_cap56(frame->hdr); for (uint32_t i = 0; i < slot_count; i++) { frame->slots[i] = gc_copy_value(ctx, frame->slots[i], to_free, to_end); } break; } case OBJ_TEXT: case OBJ_BLOB: case OBJ_CODE: // No internal references to scan break; } } ``` ### Phase 5: Fix gc_copy_value Forwarding Update `gc_copy_value` (line 1883) for offset 0 headers: ```c static JSValue gc_copy_value(JSContext *ctx, JSValue v, uint8_t **to_free, uint8_t *to_end) { if (!JS_IsPtr(v)) return v; // Immediate value void *ptr = JS_VALUE_GET_PTR(v); // Stone memory - don't copy (includes Code objects) objhdr_t hdr = *(objhdr_t*)ptr; if (objhdr_s(hdr)) return v; // Check if in current heap if ((uint8_t*)ptr < ctx->heap_base || (uint8_t*)ptr >= ctx->heap_end) return v; // External allocation // Already forwarded? if (objhdr_type(hdr) == OBJ_FORWARD) { void *new_ptr = (void*)(uintptr_t)objhdr_cap56(hdr); return JS_MKPTR(new_ptr); } // Copy object to new space size_t size = gc_object_size(ptr); void *new_ptr = *to_free; *to_free += size; memcpy(new_ptr, ptr, size); // Leave forwarding pointer in old location *(objhdr_t*)ptr = objhdr_make((uint64_t)(uintptr_t)new_ptr, OBJ_FORWARD, 0, 0, 0, 0); return JS_MKPTR(new_ptr); } ``` ### Phase 6: Complete GC Root Tracing Update `ctx_gc` (line 1966) to trace all roots including JSGCRef: ```c static int ctx_gc(JSContext *ctx) { // ... existing setup code ... // Copy roots: global object, class prototypes, etc. (existing) ctx->global_obj = gc_copy_value(ctx, ctx->global_obj, &to_free, to_end); ctx->global_var_obj = gc_copy_value(ctx, ctx->global_var_obj, &to_free, to_end); // ... other existing root copying ... // Copy GC root stack (JS_PUSH_VALUE/JS_POP_VALUE) for (JSGCRef *ref = ctx->top_gc_ref; ref; ref = ref->prev) { ref->val = gc_copy_value(ctx, ref->val, &to_free, to_end); } // Copy GC root list (JS_AddGCRef/JS_DeleteGCRef) for (JSGCRef *ref = ctx->last_gc_ref; ref; ref = ref->prev) { ref->val = gc_copy_value(ctx, ref->val, &to_free, to_end); } // Copy current exception ctx->current_exception = gc_copy_value(ctx, ctx->current_exception, &to_free, to_end); // Cheney scan (existing) // ... } ``` ### Phase 7: Trigger GC on Allocation Failure Update `js_malloc` (line 1495): ```c void *js_malloc(JSContext *ctx, size_t size) { size = (size + 7) & ~7; // Align to 8 bytes if ((uint8_t*)ctx->heap_free + size > (uint8_t*)ctx->heap_end) { if (ctx_gc(ctx) < 0) { JS_ThrowOutOfMemory(ctx); return NULL; } // Retry after GC if ((uint8_t*)ctx->heap_free + size > (uint8_t*)ctx->heap_end) { JS_ThrowOutOfMemory(ctx); return NULL; } } void *ptr = ctx->heap_free; ctx->heap_free = (uint8_t*)ctx->heap_free + size; return ptr; } ``` ### Phase 8: Frame Reduction (for closures) When a function returns, "reduce" its frame to just closure variables: ```c static void reduce_frame(JSContext *ctx, JSFrame *frame) { if (frame->caller == NULL) return; // Already reduced JSCode *code = frame->function->code; uint32_t closure_size = code->closure_size; // Shrink capacity to just closure variables frame->hdr = objhdr_make(closure_size, OBJ_FRAME, 0, 0, 0, 0); frame->caller = NULL; // Signal: frame is reduced } ``` ### Phase 9: Remove Unused Reference Counting Code Delete: - `gc_decref`, `gc_decref_child` functions - `gc_scan_incref_child`, `gc_scan_incref_child2` functions - `JS_GCPhaseEnum`, `gc_phase` fields - `JSGCObjectHeader` struct (merge into objhdr_t) - `ref_count` fields from any remaining structs - `mark_function_children_decref` function - All `free_*` functions that rely on ref counting ## Files to Modify 1. **source/quickjs.c** - Main implementation: - Add DupValue/FreeValue no-op macros (~line 100) - Restructure JSArray, JSBlob, JSText, JSRecord (lines 468-499) - Simplify JSFunction to 3-word struct (line 1205) - Add JSFrame as heap object (new) - Restructure JSCode/JSFunctionBytecode (line 1293) - Fix gc_object_size (line 1850) - Fix gc_copy_value (line 1883) - Complete gc_scan_object (line 1924) - Update ctx_gc for all roots (line 1966) - Update js_malloc to trigger GC (line 1495) - Delete ref counting code throughout 2. **source/quickjs.h** - Public API: - Remove JSGCObjectHeader - Update JSValue type checks if needed - Ensure JS_IsStone works with offset 0 headers ## Execution Order 1. **First**: Add DupValue/FreeValue macros (enables compilation) 2. **Second**: Standardize struct layouts (header at offset 0) 3. **Third**: Fix gc_object_size and gc_copy_value 4. **Fourth**: Complete gc_scan_object for all types 5. **Fifth**: Update ctx_gc with complete root tracing 6. **Sixth**: Wire js_malloc to trigger GC 7. **Seventh**: Add frame reduction for closures 8. **Finally**: Remove ref counting dead code ## Verification 1. **Compile test**: `make` should succeed without errors 2. **Basic test**: Run simple scripts: ```js var a = [1, 2, 3] log.console(a[1]) ``` 3. **Stress test**: Allocate many objects to trigger GC: ```js for (var i = 0; i < 100000; i++) { var x = { value: i } } log.console("done") ``` 4. **Closure test**: Test functions with closures survive GC: ```js fn make_counter() { var count = 0 fn inc() { count = count + 1; return count } return inc } var c = make_counter() log.console(c()) // 1 log.console(c()) // 2 ``` 5. **GC stress with closures**: Create many closures, trigger GC, verify they still work ## Key Design Decisions (Resolved) 1. **JSCode storage**: Lives in stone (immutable) memory, never copied during GC ✓ 2. **Header offset**: Standardized to offset 0 for all heap objects ✓ 3. **Closure variables**: Live in JSFrame objects; frames are "reduced" when functions return ✓ 4. **JSVarRef**: Eliminated - closures reference their outer frame directly ✓