Compare commits
48 Commits
e53f55fb23
...
misty
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb36832f7e | ||
|
|
19576533d9 | ||
|
|
c08249b6f1 | ||
|
|
dc348d023f | ||
|
|
fd5e4d155e | ||
|
|
e734353722 | ||
|
|
94f1645be1 | ||
|
|
41e3a6d91a | ||
|
|
04c569eab1 | ||
|
|
dc3c474b3a | ||
|
|
f1117bbd41 | ||
|
|
43faad95e0 | ||
|
|
acc9878b36 | ||
|
|
03c45ee8b0 | ||
|
|
a171a0d2af | ||
|
|
bb8d3930b3 | ||
|
|
522ae6128a | ||
|
|
16c26e4bf2 | ||
|
|
a9804785e0 | ||
|
|
11ae703693 | ||
|
|
f203278c3e | ||
|
|
a80557283a | ||
|
|
3e40885e07 | ||
|
|
e4b7de46f6 | ||
|
|
e680439a9b | ||
|
|
ae11504e00 | ||
|
|
893deaec23 | ||
|
|
69b032d3dc | ||
|
|
256a00c501 | ||
|
|
8e166b8f98 | ||
|
|
ddbdd00496 | ||
|
|
bdf0461e1f | ||
|
|
0b86af1d4c | ||
|
|
beac9608ea | ||
|
|
a04bebd0d7 | ||
|
|
ce74f726dd | ||
|
|
4d1ab60852 | ||
|
|
22ab6c8098 | ||
|
|
9a9775690f | ||
|
|
be71ae3bba | ||
|
|
f2a76cbb55 | ||
|
|
2d834c37b3 | ||
|
|
ba1b92aa78 | ||
|
|
c356fe462d | ||
|
|
b23b918f97 | ||
|
|
e720152bcd | ||
|
|
f093e6f5a3 | ||
|
|
4fc904de63 |
@@ -10,6 +10,10 @@ AllowShortFunctionsOnASingleLine: true
|
||||
AllowShortBlocksOnASingleLine: true
|
||||
AllowShortIfStatementsOnASingleLine: true
|
||||
BreakBeforeBraces: Attach
|
||||
ColumnLimit: 0
|
||||
BreakFunctionDefinitionParameters: false
|
||||
BinPackParameters: false
|
||||
BinPackArguments: false
|
||||
|
||||
# --- Fix the "static T\nname(...)" style ---
|
||||
AlwaysBreakAfterDefinitionReturnType: None
|
||||
|
||||
2
Makefile
2
Makefile
@@ -58,7 +58,7 @@ static:
|
||||
# Bootstrap: build cell from scratch using meson (only needed once)
|
||||
# Also installs core scripts to ~/.cell/core
|
||||
bootstrap:
|
||||
meson setup build_bootstrap -Dbuildtype=debugoptimized -Db_sanitize=address
|
||||
meson setup build_bootstrap -Dbuildtype=debug -Db_sanitize=address
|
||||
meson compile -C build_bootstrap
|
||||
cp build_bootstrap/cell .
|
||||
cp build_bootstrap/libcell_runtime.dylib .
|
||||
|
||||
@@ -8,14 +8,14 @@ static JSClassID js_writer_class_id;
|
||||
static void js_reader_finalizer(JSRuntime *rt, JSValue val) {
|
||||
mz_zip_archive *zip = JS_GetOpaque(val, js_reader_class_id);
|
||||
mz_zip_reader_end(zip);
|
||||
js_free_rt(rt,zip);
|
||||
js_free_rt(zip);
|
||||
}
|
||||
|
||||
static void js_writer_finalizer(JSRuntime *rt, JSValue val) {
|
||||
mz_zip_archive *zip = JS_GetOpaque(val, js_writer_class_id);
|
||||
mz_zip_writer_finalize_archive(zip);
|
||||
mz_zip_writer_end(zip);
|
||||
js_free_rt(rt,zip);
|
||||
js_free_rt(zip);
|
||||
}
|
||||
|
||||
static JSClassDef js_reader_class = {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#include "cell.h"
|
||||
|
||||
JSC_CCALL(os_gc, JS_RunGC(JS_GetRuntime(js)) )
|
||||
JSC_CCALL(os_mem_limit, JS_SetMemoryLimit(JS_GetRuntime(js), js2number(js,argv[0])))
|
||||
JSC_CCALL(os_gc_threshold, JS_SetGCThreshold(JS_GetRuntime(js), js2number(js,argv[0])))
|
||||
JSC_CCALL(os_max_stacksize, JS_SetMaxStackSize(JS_GetRuntime(js), js2number(js,argv[0])))
|
||||
|
||||
// Compute the approximate size of a single JS value in memory.
|
||||
@@ -91,9 +89,7 @@ JSC_CCALL(js_fn_info,
|
||||
static const JSCFunctionListEntry js_js_funcs[] = {
|
||||
MIST_FUNC_DEF(os, calc_mem, 0),
|
||||
MIST_FUNC_DEF(os, mem_limit, 1),
|
||||
MIST_FUNC_DEF(os, gc_threshold, 1),
|
||||
MIST_FUNC_DEF(os, max_stacksize, 1),
|
||||
MIST_FUNC_DEF(os, gc, 0),
|
||||
MIST_FUNC_DEF(os, eval, 2),
|
||||
MIST_FUNC_DEF(js, compile, 2),
|
||||
MIST_FUNC_DEF(js, eval_compile, 1),
|
||||
|
||||
869
gc_plan.md
869
gc_plan.md
@@ -1,602 +1,403 @@
|
||||
# GC Refactoring Plan: Cheney Copying Collector
|
||||
# Plan: Complete Copying GC Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
Replace the current reference-counting GC with a simple two-space Cheney copying collector.
|
||||
This fundamentally simplifies the system: no more `JSGCObjectHeader`, no ref counts,
|
||||
no cycle detection - just bump allocation and copying live objects when memory fills up.
|
||||
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.
|
||||
|
||||
## Architecture
|
||||
## Target Architecture (from docs/memory.md)
|
||||
|
||||
```
|
||||
JSRuntime (256 MB pool)
|
||||
├── Buddy allocator for block management
|
||||
├── Class definitions (shared across contexts)
|
||||
└── JSContext #1 (actor)
|
||||
├── Current block (64KB initially)
|
||||
├── heap_base: start of block
|
||||
├── heap_free: bump pointer (total used = heap_free - heap_base)
|
||||
├── Text interning table (per-context, rebuilt on GC)
|
||||
└── On memory pressure: request new block, copy live objects, return old block
|
||||
```
|
||||
### Object Types (simplified from current):
|
||||
|
||||
**Key principle**: Each JSContext (actor) owns its own memory. Nothing is stored in JSRuntime
|
||||
except the buddy allocator and class definitions. There is no runtime-level string arena.
|
||||
**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
|
||||
|
||||
## Memory Model (from docs/memory.md)
|
||||
### 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
|
||||
|
||||
### Object Header (objhdr_t - 64 bits)
|
||||
```
|
||||
[56 bits: capacity] [1 bit: R flag] [3 bits: reserved] [1 bit: stone] [3 bits: type]
|
||||
```
|
||||
## Current State (needs refactoring)
|
||||
|
||||
All heap objects start with just `objhdr_t`. No `JSGCObjectHeader`, no ref counts.
|
||||
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
|
||||
|
||||
### Object Types
|
||||
- 0: OBJ_ARRAY - Header, Length, Elements[]
|
||||
- 1: OBJ_BLOB - Header, Length (bits), BitWords[]
|
||||
- 2: OBJ_TEXT - Header, Length/Hash, PackedChars[] (see Text section below)
|
||||
- 3: OBJ_RECORD - Header, Prototype, Length, Key/Value pairs
|
||||
- 4: OBJ_FUNCTION - Header, Code, Outer (always stone, 3 words)
|
||||
- 5: OBJ_CODE - Header, Arity, Size, ClosureSize, Entry, Disruption (in context memory)
|
||||
- 6: OBJ_FRAME - Header, Function, Caller, ReturnAddr, Slots[]
|
||||
- 7: OBJ_FORWARD - Forwarding pointer (used during GC)
|
||||
## Implementation Steps
|
||||
|
||||
### Text (Type 2) - Two Modes
|
||||
### Phase 1: Define No-Op DupValue/FreeValue (To Enable Compilation)
|
||||
|
||||
Text has two forms depending on the stone bit:
|
||||
|
||||
**Pretext (stone=0)**: Mutable intermediate representation
|
||||
- `capacity` = max chars it can hold
|
||||
- `length` word = actual number of characters
|
||||
- Used during string building/concatenation
|
||||
|
||||
**Text (stone=1)**: Immutable user-facing string
|
||||
- `capacity` = length (they're equal for stoned text)
|
||||
- `length` word = hash (for record key lookup)
|
||||
- All text keys in records must be stoned
|
||||
- Text literals are stoned and interned
|
||||
Add these near line 100 in `source/quickjs.c`:
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
objhdr_t hdr; /* type=OBJ_TEXT, cap=capacity, s=stone bit */
|
||||
uint64_t len_or_hash; /* length if pretext (s=0), hash if text (s=1) */
|
||||
uint64_t packed[]; /* 2 UTF32 chars per word */
|
||||
/* 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;
|
||||
```
|
||||
|
||||
**DELETE JSString** - just use JSText (currently named mist_text).
|
||||
### Phase 3: Complete gc_object_size for All Types
|
||||
|
||||
### Text Interning (Per-Context)
|
||||
|
||||
Each context maintains its own text interning table:
|
||||
- Texts used as record keys are stoned and interned
|
||||
- Text literals are stoned and interned
|
||||
- During GC, a new interning table is built as live objects are copied
|
||||
- This prevents the table from becoming a graveyard
|
||||
Update `gc_object_size` (line 1850) to read header at offset 0:
|
||||
|
||||
```c
|
||||
/* In JSContext */
|
||||
JSText **text_intern_array; /* indexed by ID */
|
||||
uint32_t *text_intern_hash; /* hash table mapping to IDs */
|
||||
uint32_t text_intern_count; /* number of interned texts */
|
||||
uint32_t text_intern_size; /* hash table size */
|
||||
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
|
||||
|
||||
## Phase 1: Add Buddy Allocator to JSRuntime [DONE]
|
||||
Update `gc_scan_object` (line 1924):
|
||||
|
||||
### File: source/quickjs.c
|
||||
|
||||
**1.1 Add buddy allocator structures**
|
||||
```c
|
||||
#define BUDDY_MIN_ORDER 16 /* 64KB minimum block */
|
||||
#define BUDDY_MAX_ORDER 28 /* 256MB maximum */
|
||||
#define BUDDY_LEVELS (BUDDY_MAX_ORDER - BUDDY_MIN_ORDER + 1)
|
||||
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);
|
||||
|
||||
typedef struct BuddyBlock {
|
||||
struct BuddyBlock *next;
|
||||
struct BuddyBlock *prev;
|
||||
uint8_t order; /* log2 of size */
|
||||
uint8_t is_free;
|
||||
} BuddyBlock;
|
||||
|
||||
typedef struct BuddyAllocator {
|
||||
uint8_t *base; /* 256MB base address */
|
||||
size_t total_size; /* 256MB */
|
||||
BuddyBlock *free_lists[BUDDY_LEVELS];
|
||||
} BuddyAllocator;
|
||||
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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**1.2 Update JSRuntime**
|
||||
### Phase 5: Fix gc_copy_value Forwarding
|
||||
|
||||
Update `gc_copy_value` (line 1883) for offset 0 headers:
|
||||
|
||||
```c
|
||||
struct JSRuntime {
|
||||
BuddyAllocator buddy;
|
||||
int class_count;
|
||||
JSClass *class_array;
|
||||
struct list_head context_list;
|
||||
/* REMOVE: gc_obj_list, gc_zero_ref_count_list, gc_phase, malloc_gc_threshold */
|
||||
/* REMOVE: malloc functions, malloc_state - contexts use bump allocation */
|
||||
};
|
||||
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);
|
||||
}
|
||||
```
|
||||
|
||||
**1.3 Implement buddy functions** [DONE]
|
||||
- `buddy_init(BuddyAllocator *b)` - allocate 256MB, initialize free lists
|
||||
- `buddy_alloc(BuddyAllocator *b, size_t size)` - allocate block of given size
|
||||
- `buddy_free(BuddyAllocator *b, void *ptr, size_t size)` - return block
|
||||
- `buddy_destroy(BuddyAllocator *b)` - free the 256MB
|
||||
### Phase 6: Complete GC Root Tracing
|
||||
|
||||
---
|
||||
Update `ctx_gc` (line 1966) to trace all roots including JSGCRef:
|
||||
|
||||
## Phase 2: Restructure JSContext for Bump Allocation [DONE]
|
||||
|
||||
### File: source/quickjs.c
|
||||
|
||||
**2.1 Update JSContext**
|
||||
```c
|
||||
struct JSContext {
|
||||
JSRuntime *rt;
|
||||
struct list_head link;
|
||||
static int ctx_gc(JSContext *ctx) {
|
||||
// ... existing setup code ...
|
||||
|
||||
/* Actor memory block - bump allocation */
|
||||
uint8_t *heap_base; /* start of current block */
|
||||
uint8_t *heap_free; /* bump pointer */
|
||||
uint8_t *heap_end; /* end of block */
|
||||
size_t current_block_size; /* 64KB initially */
|
||||
size_t next_block_size; /* doubles if <10% recovered */
|
||||
// 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 ...
|
||||
|
||||
/* Stack (VM execution) */
|
||||
JSValue *value_stack;
|
||||
int value_stack_top;
|
||||
int value_stack_capacity;
|
||||
struct VMFrame *frame_stack;
|
||||
int frame_stack_top;
|
||||
int frame_stack_capacity;
|
||||
// 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);
|
||||
}
|
||||
|
||||
/* Roots */
|
||||
JSValue global_obj;
|
||||
JSValue *class_proto;
|
||||
JSValue current_exception;
|
||||
// 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);
|
||||
}
|
||||
|
||||
/* Text interning (per-context, rebuilt on GC) */
|
||||
JSText **text_intern_array;
|
||||
uint32_t *text_intern_hash;
|
||||
uint32_t text_intern_count;
|
||||
uint32_t text_intern_size;
|
||||
// Copy current exception
|
||||
ctx->current_exception = gc_copy_value(ctx, ctx->current_exception, &to_free, to_end);
|
||||
|
||||
/* Other context state */
|
||||
uint16_t class_count;
|
||||
int interrupt_counter;
|
||||
void *user_opaque;
|
||||
|
||||
/* REMOVE: JSGCObjectHeader header */
|
||||
};
|
||||
// Cheney scan (existing)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**2.2 Implement bump allocator** [DONE]
|
||||
### Phase 7: Trigger GC on Allocation Failure
|
||||
|
||||
Update `js_malloc` (line 1495):
|
||||
|
||||
```c
|
||||
static void *ctx_alloc(JSContext *ctx, size_t size) {
|
||||
size = (size + 7) & ~7; /* 8-byte align */
|
||||
if (ctx->heap_free + size > ctx->heap_end) {
|
||||
if (ctx_gc(ctx) < 0) return NULL; /* triggers GC */
|
||||
if (ctx->heap_free + size > ctx->heap_end) {
|
||||
return NULL; /* still OOM after GC */
|
||||
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 += size;
|
||||
ctx->heap_free = (uint8_t*)ctx->heap_free + size;
|
||||
return ptr;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
### Phase 8: Frame Reduction (for closures)
|
||||
|
||||
## Phase 3: Unify Object Headers (Remove JSGCObjectHeader)
|
||||
|
||||
### 3.1 New unified object layout
|
||||
|
||||
All heap objects start with just `objhdr_t`:
|
||||
When a function returns, "reduce" its frame to just closure variables:
|
||||
|
||||
```c
|
||||
/* Array */
|
||||
typedef struct {
|
||||
objhdr_t hdr; /* type=OBJ_ARRAY, cap=element_capacity */
|
||||
uint64_t len;
|
||||
JSValue elem[];
|
||||
} JSArray;
|
||||
static void reduce_frame(JSContext *ctx, JSFrame *frame) {
|
||||
if (frame->caller == NULL) return; // Already reduced
|
||||
|
||||
/* Text (replaces both mist_text and JSString) */
|
||||
typedef struct {
|
||||
objhdr_t hdr; /* type=OBJ_TEXT, cap=capacity, s=stone bit */
|
||||
uint64_t len_or_hash; /* len if pretext (s=0), hash if text (s=1) */
|
||||
uint64_t packed[]; /* 2 UTF32 chars per word */
|
||||
} JSText;
|
||||
JSCode *code = frame->function->code;
|
||||
uint32_t closure_size = code->closure_size;
|
||||
|
||||
/* Record (object) */
|
||||
typedef struct JSRecord {
|
||||
objhdr_t hdr; /* type=OBJ_RECORD, cap=mask, s=stone bit */
|
||||
struct JSRecord *proto;
|
||||
uint64_t len;
|
||||
uint64_t tombs;
|
||||
uint16_t class_id;
|
||||
uint16_t _pad;
|
||||
uint32_t rec_id; /* for record-as-key hashing */
|
||||
JSValue slots[]; /* key[0], val[0], key[1], val[1], ... */
|
||||
} JSRecord;
|
||||
|
||||
/* Function */
|
||||
typedef struct {
|
||||
objhdr_t hdr; /* type=OBJ_FUNCTION, always stone */
|
||||
JSValue code; /* pointer to code object */
|
||||
JSValue outer; /* pointer to enclosing frame */
|
||||
} JSFunction;
|
||||
|
||||
/* Frame */
|
||||
typedef struct {
|
||||
objhdr_t hdr; /* type=OBJ_FRAME, cap=slot_count */
|
||||
JSValue function; /* JSFunction */
|
||||
JSValue caller; /* JSFrame or null */
|
||||
uint64_t return_addr;
|
||||
JSValue slots[]; /* args, locals, temporaries */
|
||||
} JSFrame;
|
||||
|
||||
/* Code (in context memory, always stone) */
|
||||
typedef struct {
|
||||
objhdr_t hdr; /* type=OBJ_CODE, always stone */
|
||||
uint32_t arity;
|
||||
uint32_t frame_size;
|
||||
uint32_t closure_size;
|
||||
uint64_t entry_point;
|
||||
uint64_t disruption_point;
|
||||
uint8_t bytecode[];
|
||||
} JSCode;
|
||||
```
|
||||
|
||||
### 3.2 Delete legacy types
|
||||
|
||||
**DELETE:**
|
||||
- `JSString` struct - use `JSText` instead
|
||||
- `JSGCObjectHeader` struct
|
||||
- `mist_text` - rename to `JSText`
|
||||
- All `p->header.ref_count` accesses
|
||||
- All `p->header.gc_obj_type` accesses
|
||||
- `JS_GC_OBJ_TYPE_*` enum values
|
||||
- `add_gc_object()`, `remove_gc_object()` functions
|
||||
- `js_alloc_string_rt()` - no runtime-level string allocation
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Implement Cheney Copying GC [DONE - needs update]
|
||||
|
||||
### 4.1 Core GC function
|
||||
|
||||
```c
|
||||
static int ctx_gc(JSContext *ctx) {
|
||||
size_t old_used = ctx->heap_free - ctx->heap_base;
|
||||
|
||||
/* Request new block from runtime */
|
||||
size_t new_size = ctx->next_block_size;
|
||||
uint8_t *new_block = buddy_alloc(&ctx->rt->buddy, new_size);
|
||||
if (!new_block) return -1;
|
||||
|
||||
uint8_t *to_base = new_block;
|
||||
uint8_t *to_free = new_block;
|
||||
uint8_t *to_end = new_block + new_size;
|
||||
|
||||
/* Reset text interning table (will be rebuilt during copy) */
|
||||
ctx->text_intern_count = 0;
|
||||
memset(ctx->text_intern_hash, 0, ctx->text_intern_size * sizeof(uint32_t));
|
||||
|
||||
/* Copy roots */
|
||||
ctx->global_obj = gc_copy_value(ctx, ctx->global_obj, &to_free, to_end);
|
||||
ctx->current_exception = gc_copy_value(ctx, ctx->current_exception, &to_free, to_end);
|
||||
for (int i = 0; i < ctx->class_count; i++) {
|
||||
ctx->class_proto[i] = gc_copy_value(ctx, ctx->class_proto[i], &to_free, to_end);
|
||||
}
|
||||
|
||||
/* Copy stack */
|
||||
for (int i = 0; i < ctx->value_stack_top; i++) {
|
||||
ctx->value_stack[i] = gc_copy_value(ctx, ctx->value_stack[i], &to_free, to_end);
|
||||
}
|
||||
|
||||
/* Cheney scan: process copied objects */
|
||||
uint8_t *scan = to_base;
|
||||
while (scan < to_free) {
|
||||
gc_scan_object(ctx, scan, &to_free, to_end);
|
||||
scan += gc_object_size(scan);
|
||||
}
|
||||
|
||||
/* Return old block */
|
||||
buddy_free(&ctx->rt->buddy, ctx->heap_base, ctx->current_block_size);
|
||||
|
||||
/* Update context */
|
||||
size_t new_used = to_free - to_base;
|
||||
size_t recovered = old_used - new_used;
|
||||
|
||||
ctx->heap_base = to_base;
|
||||
ctx->heap_free = to_free;
|
||||
ctx->heap_end = to_end;
|
||||
ctx->current_block_size = new_size;
|
||||
|
||||
/* If <10% recovered, double next block size */
|
||||
if (recovered < old_used / 10) {
|
||||
ctx->next_block_size = new_size * 2;
|
||||
}
|
||||
|
||||
return 0;
|
||||
// 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
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Copy functions per type
|
||||
### Phase 9: Remove Unused Reference Counting Code
|
||||
|
||||
```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 */
|
||||
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
|
||||
|
||||
void *ptr = JS_VALUE_GET_PTR(v);
|
||||
objhdr_t hdr = *(objhdr_t *)ptr;
|
||||
## Files to Modify
|
||||
|
||||
/* Already forwarded? */
|
||||
if (objhdr_type(hdr) == OBJ_FORWARD) {
|
||||
/* Extract forwarding address from cap56 field */
|
||||
return JS_MKPTR(JS_TAG_PTR, (void *)(uintptr_t)objhdr_cap56(hdr));
|
||||
}
|
||||
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
|
||||
|
||||
/* Copy object */
|
||||
size_t size = gc_object_size(ptr);
|
||||
void *new_ptr = *to_free;
|
||||
memcpy(new_ptr, ptr, size);
|
||||
*to_free += size;
|
||||
2. **source/quickjs.h** - Public API:
|
||||
- Remove JSGCObjectHeader
|
||||
- Update JSValue type checks if needed
|
||||
- Ensure JS_IsStone works with offset 0 headers
|
||||
|
||||
/* Install forwarding pointer in old location */
|
||||
*(objhdr_t *)ptr = objhdr_make((uint64_t)(uintptr_t)new_ptr, OBJ_FORWARD, 0, 0, 0, 0);
|
||||
## Execution Order
|
||||
|
||||
/* If it's a stoned text, re-intern it */
|
||||
if (objhdr_type(hdr) == OBJ_TEXT && objhdr_s(hdr)) {
|
||||
gc_intern_text(ctx, (JSText *)new_ptr);
|
||||
}
|
||||
|
||||
return JS_MKPTR(JS_TAG_PTR, new_ptr);
|
||||
}
|
||||
|
||||
static void gc_scan_object(JSContext *ctx, void *ptr, uint8_t **to_free, uint8_t *to_end) {
|
||||
objhdr_t hdr = *(objhdr_t *)ptr;
|
||||
switch (objhdr_type(hdr)) {
|
||||
case OBJ_ARRAY: {
|
||||
JSArray *arr = ptr;
|
||||
for (uint64_t i = 0; i < arr->len; i++) {
|
||||
arr->elem[i] = gc_copy_value(ctx, arr->elem[i], to_free, to_end);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case OBJ_RECORD: {
|
||||
JSRecord *rec = ptr;
|
||||
if (rec->proto) {
|
||||
JSValue pv = JS_MKPTR(JS_TAG_PTR, rec->proto);
|
||||
pv = gc_copy_value(ctx, pv, to_free, to_end);
|
||||
rec->proto = (JSRecord *)JS_VALUE_GET_PTR(pv);
|
||||
}
|
||||
uint64_t mask = objhdr_cap56(hdr);
|
||||
for (uint64_t i = 0; i <= mask; i++) {
|
||||
rec->slots[i*2] = gc_copy_value(ctx, rec->slots[i*2], to_free, to_end);
|
||||
rec->slots[i*2+1] = gc_copy_value(ctx, rec->slots[i*2+1], to_free, to_end);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case OBJ_FUNCTION: {
|
||||
JSFunction *fn = ptr;
|
||||
fn->code = gc_copy_value(ctx, fn->code, to_free, to_end);
|
||||
fn->outer = gc_copy_value(ctx, fn->outer, to_free, to_end);
|
||||
break;
|
||||
}
|
||||
case OBJ_FRAME: {
|
||||
JSFrame *fr = ptr;
|
||||
fr->function = gc_copy_value(ctx, fr->function, to_free, to_end);
|
||||
fr->caller = gc_copy_value(ctx, fr->caller, to_free, to_end);
|
||||
uint64_t cap = objhdr_cap56(hdr);
|
||||
for (uint64_t i = 0; i < cap; i++) {
|
||||
fr->slots[i] = gc_copy_value(ctx, fr->slots[i], to_free, to_end);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case OBJ_TEXT:
|
||||
case OBJ_BLOB:
|
||||
case OBJ_CODE:
|
||||
/* No references to scan */
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Remove Old GC Infrastructure
|
||||
|
||||
### Files: source/quickjs.c, source/quickjs.h
|
||||
|
||||
**Delete entirely:**
|
||||
- `RC_TRACE` conditional code (~100 lines)
|
||||
- `RcEvent` struct and `rc_log` array
|
||||
- `rc_log_event`, `rc_trace_inc_gc`, `rc_trace_dec_gc`, `rc_dump_history`
|
||||
- `gc_decref`, `gc_decref_child`, `gc_decref_child_dbg`, `gc_decref_child_edge`
|
||||
- `gc_free_cycles`, `free_zero_refcount`, `free_gc_object`
|
||||
- `add_gc_object`, `remove_gc_object`
|
||||
- `JS_RunGCInternal`, `JS_GC_PHASE_*` enum
|
||||
- `mark_children`, `gc_mark` (the old marking functions)
|
||||
- `JSGCPhaseEnum`, `gc_phase` field in JSRuntime
|
||||
- `gc_obj_list`, `gc_zero_ref_count_list` in JSRuntime
|
||||
- `JSRefCountHeader` struct
|
||||
- `js_alloc_string_rt` - no runtime string allocation
|
||||
- Stone arena in runtime (`st_pages`, etc.) - each context has its own
|
||||
|
||||
**Update:**
|
||||
- `JS_FreeValue` - becomes no-op (GC handles everything)
|
||||
- `JS_DupValue` - becomes no-op (just return value)
|
||||
- `__JS_FreeValueRT` - remove entirely
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Update Allocation Sites [IN PROGRESS]
|
||||
|
||||
### 6.1 Replace js_malloc with ctx_alloc
|
||||
|
||||
All object allocations change from:
|
||||
```c
|
||||
JSRecord *rec = js_mallocz(ctx, sizeof(JSRecord));
|
||||
rec->tab = js_mallocz(ctx, sizeof(JSRecordEntry) * size);
|
||||
```
|
||||
to:
|
||||
```c
|
||||
size_t total = sizeof(JSRecord) + (mask+1) * 2 * sizeof(JSValue);
|
||||
JSRecord *rec = ctx_alloc(ctx, total);
|
||||
rec->hdr = objhdr_make(mask, OBJ_RECORD, false, false, false, false);
|
||||
/* slots are inline after the struct */
|
||||
```
|
||||
|
||||
### 6.2 Update object creation functions
|
||||
|
||||
- `JS_NewObject` - use ctx_alloc, set hdr, inline slots
|
||||
- `JS_NewArray` - use ctx_alloc, set hdr, inline elements
|
||||
- `JS_NewStringLen` - use ctx_alloc, create JSText
|
||||
- `js_create_function` - use ctx_alloc
|
||||
- String concatenation, array push, etc.
|
||||
|
||||
### 6.3 Delete js_malloc/js_free usage for heap objects
|
||||
|
||||
Keep `js_malloc_rt` only for:
|
||||
- Class arrays (in runtime)
|
||||
- VM stacks (external to GC'd heap)
|
||||
- Temporary C allocations
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Delete JSString, Use JSText
|
||||
|
||||
### 7.1 Replace JSString with JSText
|
||||
|
||||
```c
|
||||
/* OLD - DELETE */
|
||||
struct JSString {
|
||||
JSRefCountHeader header;
|
||||
uint32_t pad;
|
||||
objhdr_t hdr;
|
||||
int64_t len;
|
||||
uint64_t u[];
|
||||
};
|
||||
|
||||
/* NEW - Single text type */
|
||||
typedef struct {
|
||||
objhdr_t hdr; /* type=OBJ_TEXT, cap=capacity, s=stone */
|
||||
uint64_t len_or_hash; /* length if s=0, hash if s=1 */
|
||||
uint64_t packed[]; /* 2 UTF32 chars per word */
|
||||
} JSText;
|
||||
```
|
||||
|
||||
### 7.2 Update string functions
|
||||
|
||||
- `js_alloc_string` -> allocate JSText via ctx_alloc
|
||||
- Remove `js_alloc_string_rt` entirely
|
||||
- `js_free_string` -> no-op (GC handles it)
|
||||
- Update all JSString* to JSText*
|
||||
|
||||
### 7.3 Text stoning
|
||||
|
||||
When a text is used as a record key or becomes a literal:
|
||||
1. Set stone bit: `hdr = objhdr_set_s(hdr, true)`
|
||||
2. Compute hash and store in len_or_hash
|
||||
3. Set capacity = length
|
||||
4. Add to context's intern table
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Update Type Checks
|
||||
|
||||
Replace `JSGCObjectHeader.gc_obj_type` checks with `objhdr_type`:
|
||||
|
||||
```c
|
||||
/* Old */
|
||||
((JSGCObjectHeader *)ptr)->gc_obj_type == JS_GC_OBJ_TYPE_RECORD
|
||||
|
||||
/* New */
|
||||
objhdr_type(*(objhdr_t *)ptr) == OBJ_RECORD
|
||||
```
|
||||
|
||||
Update helper functions:
|
||||
- `js_is_record(v)` - check `objhdr_type == OBJ_RECORD`
|
||||
- `js_is_array(v)` - check `objhdr_type == OBJ_ARRAY`
|
||||
- `js_is_function(v)` - check `objhdr_type == OBJ_FUNCTION`
|
||||
- `JS_IsString(v)` - check `objhdr_type == OBJ_TEXT`
|
||||
|
||||
---
|
||||
|
||||
## Phase 9: Handle C Opaque Objects
|
||||
|
||||
Per docs/memory.md, C opaque objects need special handling:
|
||||
|
||||
**9.1 Track live opaque objects**
|
||||
```c
|
||||
typedef struct {
|
||||
void *opaque;
|
||||
JSClassID class_id;
|
||||
uint8_t alive;
|
||||
} OpaqueRef;
|
||||
|
||||
/* In JSContext */
|
||||
OpaqueRef *opaque_refs;
|
||||
int opaque_ref_count;
|
||||
int opaque_ref_capacity;
|
||||
```
|
||||
|
||||
**9.2 During GC**
|
||||
1. When copying a JSRecord with opaque data, mark it alive in opaque_refs
|
||||
2. After GC, iterate opaque_refs and call finalizer for those with `alive=0`
|
||||
3. Clear all alive flags for next cycle
|
||||
|
||||
---
|
||||
|
||||
## File Changes Summary
|
||||
|
||||
### source/quickjs.c
|
||||
- Remove ~800 lines: RC_TRACE, gc_decref, gc_free_cycles, JSGCObjectHeader, JSString, runtime string arena
|
||||
- Add ~300 lines: buddy allocator, Cheney GC, JSText
|
||||
- Modify ~400 lines: allocation sites, type checks, string handling
|
||||
|
||||
### source/quickjs.h
|
||||
- Remove: JSGCObjectHeader, JSRefCountHeader from public API
|
||||
- Remove: JS_MarkFunc, JS_MarkValue
|
||||
- Update: JS_FreeValue, JS_DupValue to be no-ops
|
||||
|
||||
---
|
||||
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. **Build**: `make` should compile without errors
|
||||
2. **Basic test**: `./cell test suite` should pass
|
||||
3. **Memory test**: Run with ASAN to verify no leaks or corruption
|
||||
4. **GC trigger**: Test that GC runs when memory fills, objects survive correctly
|
||||
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)
|
||||
|
||||
## Dependencies / Order of Work
|
||||
|
||||
1. Phase 1 (Buddy) - DONE
|
||||
2. Phase 2 (JSContext bump alloc) - DONE
|
||||
3. Phase 5 (Remove old GC) - Do this early to reduce conflicts
|
||||
4. Phase 7 (Delete JSString) - Major cleanup
|
||||
5. Phase 3 (Unify headers) - Depends on 5, 7
|
||||
6. Phase 6 (Allocation sites) - Incremental, with Phase 3
|
||||
7. Phase 4 (Cheney GC) - After Phases 3, 6
|
||||
8. Phase 8 (Type checks) - With Phase 3
|
||||
9. Phase 9 (Opaque) - Last, once basic GC works
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Each context owns its memory - no runtime-level allocation except buddy blocks
|
||||
- Text interning is per-context, rebuilt during GC
|
||||
- OBJ_CODE lives in context memory (always stone)
|
||||
- Frames use caller=null to signal returnable (can be shrunk during GC)
|
||||
- Forward pointer type (7) used during GC to mark copied objects
|
||||
- `heap_free - heap_base` = total memory used by context
|
||||
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 ✓
|
||||
|
||||
@@ -166,7 +166,7 @@ static void nota_encode_value(NotaEncodeContext *enc, JSValueConst val, JSValueC
|
||||
case JS_TAG_NULL:
|
||||
nota_write_sym(&enc->nb, NOTA_NULL);
|
||||
break;
|
||||
case JS_TAG_OBJECT: {
|
||||
case JS_TAG_PTR: {
|
||||
if (js_is_blob(ctx, replaced)) {
|
||||
size_t buf_len;
|
||||
void *buf_data = js_get_blob_data(ctx, &buf_len, replaced);
|
||||
@@ -178,7 +178,7 @@ static void nota_encode_value(NotaEncodeContext *enc, JSValueConst val, JSValueC
|
||||
break;
|
||||
}
|
||||
|
||||
if (JS_IsArray(ctx, replaced)) {
|
||||
if (JS_IsArray(replaced)) {
|
||||
if (nota_stack_has(enc, replaced)) {
|
||||
enc->cycle = 1;
|
||||
break;
|
||||
|
||||
@@ -40,6 +40,7 @@ sources = []
|
||||
src += [ # core
|
||||
'monocypher.c',
|
||||
'cell.c',
|
||||
'suite.c',
|
||||
'wildmatch.c',
|
||||
'qjs_actor.c',
|
||||
'qjs_wota.c',
|
||||
|
||||
@@ -150,7 +150,7 @@ static void encode_js_value(json_encoder *enc, JSContext *js, JSValue val) {
|
||||
const char *str = JS_ToCStringLen(js, &len, val);
|
||||
enc->writeString(enc, str, len);
|
||||
JS_FreeCString(js, str);
|
||||
} else if (JS_IsArray(js, val)) {
|
||||
} else if (JS_IsArray(val)) {
|
||||
encode_js_array(enc, js, val);
|
||||
} else if (JS_IsObject(val)) {
|
||||
encode_js_object(enc, js, val);
|
||||
|
||||
8
qop.c
8
qop.c
@@ -8,10 +8,10 @@ static void js_qop_archive_finalizer(JSRuntime *rt, JSValue val) {
|
||||
qop_desc *qop = JS_GetOpaque(val, js_qop_archive_class_id);
|
||||
if (qop) {
|
||||
if (qop->hashmap) {
|
||||
js_free_rt(rt, qop->hashmap);
|
||||
js_free_rt(qop->hashmap);
|
||||
}
|
||||
qop_close(qop);
|
||||
js_free_rt(rt, qop);
|
||||
js_free_rt(qop);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@ static void js_qop_writer_finalizer(JSRuntime *rt, JSValue val) {
|
||||
qop_writer *w = JS_GetOpaque(val, js_qop_writer_class_id);
|
||||
if (w) {
|
||||
if (w->fh) fclose(w->fh);
|
||||
if (w->files) js_free_rt(rt, w->files);
|
||||
js_free_rt(rt, w);
|
||||
if (w->files) js_free_rt(w->files);
|
||||
js_free_rt(w);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
223
source/cell.c
223
source/cell.c
@@ -19,6 +19,10 @@
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
/* Test suite declarations */
|
||||
int run_c_test_suite(JSContext *ctx);
|
||||
static int run_test_suite(size_t heap_size);
|
||||
|
||||
cell_rt *root_cell = NULL;
|
||||
static char *core_path = NULL;
|
||||
|
||||
@@ -122,7 +126,7 @@ void script_startup(cell_rt *prt)
|
||||
rt = JS_NewRuntime();
|
||||
|
||||
JSContext *js = JS_NewContextRaw(rt);
|
||||
JS_SetInterruptHandler(rt, actor_interrupt_cb, prt);
|
||||
JS_SetInterruptHandler(rt, (JSInterruptHandler *)actor_interrupt_cb, prt);
|
||||
|
||||
JS_AddIntrinsicBaseObjects(js);
|
||||
JS_AddIntrinsicEval(js);
|
||||
@@ -135,36 +139,57 @@ void script_startup(cell_rt *prt)
|
||||
cell_rt *crt = JS_GetContextOpaque(js);
|
||||
JS_FreeValue(js, js_blob_use(js));
|
||||
|
||||
JSValue globalThis = JS_GetGlobalObject(js);
|
||||
/* Use GC refs to protect values during allocations.
|
||||
Initialize values to JS_NULL before adding to GC list to avoid
|
||||
scanning uninitialized memory. */
|
||||
JSGCRef globalThis_ref, cell_ref, hidden_fn_ref;
|
||||
globalThis_ref.val = JS_NULL;
|
||||
cell_ref.val = JS_NULL;
|
||||
hidden_fn_ref.val = JS_NULL;
|
||||
JS_AddGCRef(js, &globalThis_ref);
|
||||
JS_AddGCRef(js, &cell_ref);
|
||||
JS_AddGCRef(js, &hidden_fn_ref);
|
||||
|
||||
JSValue cell = JS_NewObject(js);
|
||||
JS_SetPropertyStr(js,globalThis,"cell", cell);
|
||||
globalThis_ref.val = JS_GetGlobalObject(js);
|
||||
cell_ref.val = JS_NewObject(js);
|
||||
JS_SetPropertyStr(js, globalThis_ref.val, "cell", cell_ref.val);
|
||||
|
||||
JSValue hidden_fn = JS_NewObject(js);
|
||||
hidden_fn_ref.val = JS_NewObject(js);
|
||||
JS_SetPropertyStr(js, cell_ref.val, "hidden", hidden_fn_ref.val);
|
||||
|
||||
JS_SetPropertyStr(js, cell, "hidden", hidden_fn);
|
||||
JS_SetPropertyStr(js, hidden_fn, "os", js_os_use(js));
|
||||
/* Call functions that allocate BEFORE using their results as arguments,
|
||||
to avoid GC invalidating already-evaluated arguments. */
|
||||
JSGCRef tmp_ref;
|
||||
tmp_ref.val = JS_NULL;
|
||||
JS_AddGCRef(js, &tmp_ref);
|
||||
|
||||
tmp_ref.val = js_os_use(js);
|
||||
JS_SetPropertyStr(js, hidden_fn_ref.val, "os", tmp_ref.val);
|
||||
|
||||
tmp_ref.val = JS_NewObject(js);
|
||||
crt->actor_sym = tmp_ref.val;
|
||||
tmp_ref.val = JS_DupValue(js, crt->actor_sym);
|
||||
JS_SetPropertyStr(js, hidden_fn_ref.val, "actorsym", tmp_ref.val);
|
||||
|
||||
crt->actor_sym = JS_NewObject(js);
|
||||
JS_SetPropertyStr(js, hidden_fn, "actorsym", JS_DupValue(js,crt->actor_sym));
|
||||
|
||||
if (crt->init_wota) {
|
||||
JS_SetPropertyStr(js, hidden_fn, "init", wota2value(js, crt->init_wota));
|
||||
tmp_ref.val = wota2value(js, crt->init_wota);
|
||||
JS_SetPropertyStr(js, hidden_fn_ref.val, "init", tmp_ref.val);
|
||||
// init wota can now be freed
|
||||
free(crt->init_wota);
|
||||
crt->init_wota = NULL;
|
||||
}
|
||||
|
||||
// Store the core path for scripts to use
|
||||
JSValue js_cell = JS_GetPropertyStr(js, globalThis, "cell");
|
||||
JSValue hidden = JS_GetPropertyStr(js, js_cell, "hidden");
|
||||
if (core_path) {
|
||||
JS_SetPropertyStr(js, hidden, "core_path", JS_NewString(js, core_path));
|
||||
tmp_ref.val = JS_NewString(js, core_path);
|
||||
JS_SetPropertyStr(js, hidden_fn_ref.val, "core_path", tmp_ref.val);
|
||||
}
|
||||
JS_FreeValue(js, hidden);
|
||||
JS_FreeValue(js, js_cell);
|
||||
|
||||
JS_FreeValue(js, globalThis);
|
||||
JS_DeleteGCRef(js, &tmp_ref);
|
||||
|
||||
JS_DeleteGCRef(js, &hidden_fn_ref);
|
||||
JS_DeleteGCRef(js, &cell_ref);
|
||||
JS_DeleteGCRef(js, &globalThis_ref);
|
||||
|
||||
// Load engine.cm from the core directory
|
||||
size_t engine_size;
|
||||
@@ -196,12 +221,174 @@ static void signal_handler(int sig)
|
||||
}
|
||||
#endif
|
||||
if (!str) return;
|
||||
|
||||
|
||||
exit_handler();
|
||||
}
|
||||
|
||||
/* Run the C test suite with minimal runtime setup */
|
||||
static int run_test_suite(size_t heap_size)
|
||||
{
|
||||
JSRuntime *rt = JS_NewRuntime();
|
||||
if (!rt) {
|
||||
printf("Failed to create JS runtime\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
JSContext *ctx = JS_NewContextRawWithHeapSize(rt, heap_size);
|
||||
if (!ctx) {
|
||||
printf("Failed to create JS context\n");
|
||||
JS_FreeRuntime(rt);
|
||||
return 1;
|
||||
}
|
||||
|
||||
JS_AddIntrinsicBaseObjects(ctx);
|
||||
|
||||
int result = run_c_test_suite(ctx);
|
||||
|
||||
JS_FreeContext(ctx);
|
||||
JS_FreeRuntime(rt);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Run an immediate script string */
|
||||
static int run_eval(const char *script_or_file, int print_bytecode)
|
||||
{
|
||||
if (!find_cell_shop()) return 1;
|
||||
|
||||
/* Check if argument is a file path */
|
||||
struct stat st;
|
||||
char *script = NULL;
|
||||
char *allocated_script = NULL;
|
||||
const char *filename = "<eval>";
|
||||
|
||||
if (stat(script_or_file, &st) == 0 && S_ISREG(st.st_mode)) {
|
||||
/* It's a file, read its contents */
|
||||
FILE *f = fopen(script_or_file, "r");
|
||||
if (!f) {
|
||||
printf("Failed to open file: %s\n", script_or_file);
|
||||
return 1;
|
||||
}
|
||||
allocated_script = malloc(st.st_size + 1);
|
||||
if (!allocated_script) {
|
||||
fclose(f);
|
||||
printf("Failed to allocate memory for script\n");
|
||||
return 1;
|
||||
}
|
||||
size_t read_size = fread(allocated_script, 1, st.st_size, f);
|
||||
fclose(f);
|
||||
allocated_script[read_size] = '\0';
|
||||
script = allocated_script;
|
||||
filename = script_or_file;
|
||||
} else {
|
||||
/* Treat as inline script */
|
||||
script = (char *)script_or_file;
|
||||
}
|
||||
|
||||
JSRuntime *rt = JS_NewRuntime();
|
||||
if (!rt) {
|
||||
printf("Failed to create JS runtime\n");
|
||||
free(allocated_script);
|
||||
return 1;
|
||||
}
|
||||
|
||||
JSContext *ctx = JS_NewContextRaw(rt);
|
||||
if (!ctx) {
|
||||
printf("Failed to create JS context\n");
|
||||
JS_FreeRuntime(rt);
|
||||
free(allocated_script);
|
||||
return 1;
|
||||
}
|
||||
|
||||
JS_AddIntrinsicBaseObjects(ctx);
|
||||
JS_AddIntrinsicEval(ctx);
|
||||
JS_AddIntrinsicRegExp(ctx);
|
||||
JS_AddIntrinsicJSON(ctx);
|
||||
|
||||
int result = 0;
|
||||
|
||||
if (print_bytecode) {
|
||||
/* Compile only, then dump and optionally execute */
|
||||
JSValue func = JS_Eval(ctx, script, strlen(script), filename, JS_EVAL_FLAG_COMPILE_ONLY);
|
||||
if (JS_IsException(func)) {
|
||||
uncaught_exception(ctx, func);
|
||||
result = 1;
|
||||
} else {
|
||||
printf("=== Compiled Bytecode ===\n");
|
||||
JS_DumpFunctionBytecode(ctx, func);
|
||||
|
||||
/* Link - resolve global references */
|
||||
JSValue linked = JS_LinkFunction(ctx, func);
|
||||
if (JS_IsException(linked)) {
|
||||
uncaught_exception(ctx, linked);
|
||||
result = 1;
|
||||
} else {
|
||||
printf("\n=== Linked Bytecode ===\n");
|
||||
JS_DumpFunctionBytecode(ctx, linked);
|
||||
|
||||
/* Now execute the linked bytecode */
|
||||
JSValue v = JS_EvalFunction(ctx, linked);
|
||||
if (JS_IsException(v)) {
|
||||
uncaught_exception(ctx, v);
|
||||
result = 1;
|
||||
} else {
|
||||
JS_FreeValue(ctx, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* Compile, link, execute */
|
||||
JSValue func = JS_Eval(ctx, script, strlen(script), filename, JS_EVAL_FLAG_COMPILE_ONLY);
|
||||
if (JS_IsException(func)) {
|
||||
uncaught_exception(ctx, func);
|
||||
result = 1;
|
||||
} else {
|
||||
JSValue linked = JS_LinkFunction(ctx, func);
|
||||
if (JS_IsException(linked)) {
|
||||
uncaught_exception(ctx, linked);
|
||||
result = 1;
|
||||
} else {
|
||||
JSValue v = JS_EvalFunction(ctx, linked);
|
||||
if (JS_IsException(v)) {
|
||||
uncaught_exception(ctx, v);
|
||||
result = 1;
|
||||
} else {
|
||||
JS_FreeValue(ctx, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JS_FreeContext(ctx);
|
||||
JS_FreeRuntime(rt);
|
||||
free(allocated_script);
|
||||
return result;
|
||||
}
|
||||
|
||||
int cell_init(int argc, char **argv)
|
||||
{
|
||||
/* Check for --test flag to run C test suite */
|
||||
if (argc >= 2 && strcmp(argv[1], "--test") == 0) {
|
||||
size_t heap_size = 64 * 1024; /* 64KB default */
|
||||
if (argc >= 3) {
|
||||
heap_size = strtoull(argv[2], NULL, 0);
|
||||
/* Round up to power of 2 for buddy allocator */
|
||||
size_t p = 1;
|
||||
while (p < heap_size) p <<= 1;
|
||||
heap_size = p;
|
||||
}
|
||||
return run_test_suite(heap_size);
|
||||
}
|
||||
|
||||
/* Check for -e or --eval flag to run immediate script */
|
||||
/* Also check for -p flag to print bytecode */
|
||||
if (argc >= 3 && (strcmp(argv[1], "-e") == 0 || strcmp(argv[1], "--eval") == 0)) {
|
||||
return run_eval(argv[2], 0);
|
||||
}
|
||||
if (argc >= 3 && (strcmp(argv[1], "-p") == 0 || strcmp(argv[1], "--print-bytecode") == 0)) {
|
||||
return run_eval(argv[2], 1);
|
||||
}
|
||||
|
||||
int script_start = 1;
|
||||
|
||||
/* Find the cell shop at ~/.cell */
|
||||
|
||||
@@ -19,14 +19,14 @@ typedef struct WotaEncodeContext {
|
||||
|
||||
static void wota_stack_push(WotaEncodeContext *enc, JSValueConst val)
|
||||
{
|
||||
if (!JS_IsObject(val)) return;
|
||||
/* if (!JS_IsObject(val)) return;
|
||||
|
||||
ObjectRef *ref = malloc(sizeof(ObjectRef));
|
||||
if (!ref) return;
|
||||
|
||||
ref->ptr = JS_VALUE_GET_PTR(val);
|
||||
ref->next = enc->visited_stack;
|
||||
enc->visited_stack = ref;
|
||||
enc->visited_stack = ref;*/
|
||||
}
|
||||
|
||||
static void wota_stack_pop(WotaEncodeContext *enc)
|
||||
@@ -40,7 +40,7 @@ static void wota_stack_pop(WotaEncodeContext *enc)
|
||||
|
||||
static int wota_stack_has(WotaEncodeContext *enc, JSValueConst val)
|
||||
{
|
||||
if (!JS_IsObject(val)) return 0;
|
||||
/* if (!JS_IsObject(val)) return 0;
|
||||
|
||||
void *ptr = JS_VALUE_GET_PTR(val);
|
||||
ObjectRef *current = enc->visited_stack;
|
||||
@@ -49,9 +49,10 @@ static int wota_stack_has(WotaEncodeContext *enc, JSValueConst val)
|
||||
if (current->ptr == ptr) return 1;
|
||||
current = current->next;
|
||||
}
|
||||
return 0;
|
||||
return 0;*/
|
||||
}
|
||||
|
||||
|
||||
static void wota_stack_free(WotaEncodeContext *enc)
|
||||
{
|
||||
while (enc->visited_stack) {
|
||||
@@ -156,7 +157,7 @@ static void wota_encode_value(WotaEncodeContext *enc, JSValueConst val, JSValueC
|
||||
case JS_TAG_NULL:
|
||||
wota_write_sym(&enc->wb, WOTA_NULL);
|
||||
break;
|
||||
case JS_TAG_OBJECT: {
|
||||
case JS_TAG_PTR: {
|
||||
if (js_is_blob(ctx, replaced)) {
|
||||
size_t buf_len;
|
||||
void *buf_data = js_get_blob_data(ctx, &buf_len, replaced);
|
||||
@@ -171,7 +172,7 @@ static void wota_encode_value(WotaEncodeContext *enc, JSValueConst val, JSValueC
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (JS_IsArray(ctx, replaced)) {
|
||||
if (JS_IsArray(replaced)) {
|
||||
if (wota_stack_has(enc, replaced)) {
|
||||
enc->cycle = 1;
|
||||
break;
|
||||
@@ -249,11 +250,14 @@ static char *decode_wota_value(JSContext *ctx, char *data_ptr, JSValue *out_val,
|
||||
int scode;
|
||||
data_ptr = wota_read_sym(&scode, data_ptr);
|
||||
if (scode == WOTA_PRIVATE) {
|
||||
JSValue inner = JS_NULL;
|
||||
data_ptr = decode_wota_value(ctx, data_ptr, &inner, holder, JS_NULL, reviver);
|
||||
JSGCRef inner_ref;
|
||||
JS_PushGCRef(ctx, &inner_ref);
|
||||
inner_ref.val = JS_NULL;
|
||||
data_ptr = decode_wota_value(ctx, data_ptr, &inner_ref.val, holder, JS_NULL, reviver);
|
||||
JSValue obj = JS_NewObject(ctx);
|
||||
cell_rt *crt = JS_GetContextOpaque(ctx);
|
||||
// JS_SetProperty(ctx, obj, crt->actor_sym, inner);
|
||||
// JS_SetProperty(ctx, obj, crt->actor_sym, inner_ref.val);
|
||||
JS_PopGCRef(ctx, &inner_ref);
|
||||
*out_val = obj;
|
||||
} else if (scode == WOTA_NULL) *out_val = JS_NULL;
|
||||
else if (scode == WOTA_FALSE) *out_val = JS_NewBool(ctx, 0);
|
||||
@@ -278,34 +282,40 @@ static char *decode_wota_value(JSContext *ctx, char *data_ptr, JSValue *out_val,
|
||||
}
|
||||
case WOTA_ARR: {
|
||||
long long c;
|
||||
JSGCRef arr_ref;
|
||||
data_ptr = wota_read_array(&c, data_ptr);
|
||||
JSValue arr = JS_NewArrayLen(ctx, c);
|
||||
JS_PushGCRef(ctx, &arr_ref);
|
||||
arr_ref.val = JS_NewArrayLen(ctx, c);
|
||||
for (long long i = 0; i < c; i++) {
|
||||
JSValue elem_val = JS_NULL;
|
||||
JSValue idx_key = JS_NewInt32(ctx, (int32_t)i);
|
||||
data_ptr = decode_wota_value(ctx, data_ptr, &elem_val, arr, idx_key, reviver);
|
||||
JS_SetPropertyUint32(ctx, arr, i, elem_val);
|
||||
data_ptr = decode_wota_value(ctx, data_ptr, &elem_val, arr_ref.val, idx_key, reviver);
|
||||
JS_SetPropertyUint32(ctx, arr_ref.val, i, elem_val);
|
||||
}
|
||||
*out_val = arr;
|
||||
*out_val = JS_PopGCRef(ctx, &arr_ref);
|
||||
break;
|
||||
}
|
||||
case WOTA_REC: {
|
||||
long long c;
|
||||
JSGCRef obj_ref;
|
||||
data_ptr = wota_read_record(&c, data_ptr);
|
||||
JSValue obj = JS_NewObject(ctx);
|
||||
JS_PushGCRef(ctx, &obj_ref);
|
||||
obj_ref.val = JS_NewObject(ctx);
|
||||
for (long long i = 0; i < c; i++) {
|
||||
char *tkey = NULL;
|
||||
size_t key_len;
|
||||
data_ptr = wota_read_text_len(&key_len, &tkey, data_ptr);
|
||||
if (!tkey) continue; // invalid key
|
||||
JSValue prop_key = JS_NewStringLen(ctx, tkey, key_len);
|
||||
JSGCRef key_ref;
|
||||
JS_PushGCRef(ctx, &key_ref);
|
||||
key_ref.val = JS_NewStringLen(ctx, tkey, key_len);
|
||||
JSValue sub_val = JS_NULL;
|
||||
data_ptr = decode_wota_value(ctx, data_ptr, &sub_val, obj, prop_key, reviver);
|
||||
JS_SetProperty(ctx, obj, prop_key, sub_val);
|
||||
JS_FreeValue(ctx, prop_key);
|
||||
data_ptr = decode_wota_value(ctx, data_ptr, &sub_val, obj_ref.val, key_ref.val, reviver);
|
||||
JS_SetProperty(ctx, obj_ref.val, key_ref.val, sub_val);
|
||||
JS_PopGCRef(ctx, &key_ref);
|
||||
free(tkey);
|
||||
}
|
||||
*out_val = obj;
|
||||
*out_val = JS_PopGCRef(ctx, &obj_ref);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -28,7 +28,6 @@ FMT(none)
|
||||
FMT(none_int)
|
||||
FMT(none_loc)
|
||||
FMT(none_arg)
|
||||
FMT(none_var_ref)
|
||||
FMT(u8)
|
||||
FMT(i8)
|
||||
FMT(loc8)
|
||||
@@ -42,7 +41,6 @@ FMT(npopx)
|
||||
FMT(npop_u16)
|
||||
FMT(loc)
|
||||
FMT(arg)
|
||||
FMT(var_ref)
|
||||
FMT(u32)
|
||||
FMT(i32)
|
||||
FMT(const)
|
||||
@@ -52,6 +50,7 @@ FMT(key)
|
||||
FMT(key_u8)
|
||||
FMT(key_u16)
|
||||
FMT(key_label_u16)
|
||||
FMT(u8_u16) /* 1 byte + 2 bytes for upvalue access */
|
||||
#undef FMT
|
||||
#endif /* FMT */
|
||||
|
||||
@@ -103,20 +102,18 @@ DEF( return, 1, 1, 0, none)
|
||||
DEF( return_undef, 1, 0, 0, none)
|
||||
DEF( throw, 1, 1, 0, none)
|
||||
DEF( throw_error, 6, 0, 0, key_u8)
|
||||
DEF( eval, 5, 1, 1, npop_u16) /* func args... -> ret_val */
|
||||
DEF( regexp, 1, 2, 1, none) /* create a RegExp object from the pattern and a
|
||||
bytecode string */
|
||||
|
||||
DEF( check_var, 5, 0, 1, key) /* check if a variable exists */
|
||||
DEF( get_var_undef, 5, 0, 1, key) /* push undefined if the variable does not exist */
|
||||
DEF( get_var, 5, 0, 1, key) /* throw an exception if the variable does not exist */
|
||||
DEF( put_var, 5, 1, 0, key) /* must come after get_var */
|
||||
DEF( put_var_init, 5, 1, 0, key) /* must come after put_var. Used to initialize a global lexical variable */
|
||||
DEF( put_var_strict, 5, 2, 0, key) /* for strict mode variable write */
|
||||
|
||||
DEF( get_ref_value, 1, 2, 3, none)
|
||||
DEF( put_ref_value, 1, 3, 0, none)
|
||||
/* Global variable access - resolved by linker to get/set_global_slot */
|
||||
DEF( check_var, 5, 0, 1, key) /* check if a variable exists - resolved by linker */
|
||||
DEF( get_var_undef, 5, 0, 1, key) /* resolved by linker to get_global_slot */
|
||||
DEF( get_var, 5, 0, 1, key) /* resolved by linker to get_global_slot */
|
||||
DEF( put_var, 5, 1, 0, key) /* resolved by linker to set_global_slot */
|
||||
DEF( put_var_init, 5, 1, 0, key) /* resolved by linker to set_global_slot */
|
||||
DEF( put_var_strict, 5, 2, 0, key) /* resolved by linker to set_global_slot */
|
||||
|
||||
/* Global variable opcodes - resolved by linker to get/set_global_slot */
|
||||
DEF( define_var, 6, 0, 0, key_u8)
|
||||
DEF(check_define_var, 6, 0, 0, key_u8)
|
||||
DEF( define_func, 6, 1, 0, key_u8)
|
||||
@@ -143,18 +140,11 @@ DEF( set_loc, 3, 1, 1, loc) /* must come after put_loc */
|
||||
DEF( get_arg, 3, 0, 1, arg)
|
||||
DEF( put_arg, 3, 1, 0, arg) /* must come after get_arg */
|
||||
DEF( set_arg, 3, 1, 1, arg) /* must come after put_arg */
|
||||
DEF( get_var_ref, 3, 0, 1, var_ref)
|
||||
DEF( put_var_ref, 3, 1, 0, var_ref) /* must come after get_var_ref */
|
||||
DEF( set_var_ref, 3, 1, 1, var_ref) /* must come after put_var_ref */
|
||||
DEF(set_loc_uninitialized, 3, 0, 0, loc)
|
||||
DEF( get_loc_check, 3, 0, 1, loc)
|
||||
DEF( put_loc_check, 3, 1, 0, loc) /* must come after get_loc_check */
|
||||
DEF( put_loc_check_init, 3, 1, 0, loc)
|
||||
DEF(get_loc_checkthis, 3, 0, 1, loc)
|
||||
DEF(get_var_ref_check, 3, 0, 1, var_ref)
|
||||
DEF(put_var_ref_check, 3, 1, 0, var_ref) /* must come after get_var_ref_check */
|
||||
DEF(put_var_ref_check_init, 3, 1, 0, var_ref)
|
||||
DEF( close_loc, 3, 0, 0, loc)
|
||||
DEF( if_false, 5, 1, 0, label)
|
||||
DEF( if_true, 5, 1, 0, label) /* must come after if_false */
|
||||
DEF( goto, 5, 0, 0, label) /* must come after if_true */
|
||||
@@ -163,15 +153,8 @@ DEF( gosub, 5, 0, 0, label) /* used to execute the finally block */
|
||||
DEF( ret, 1, 1, 0, none) /* used to return from the finally block */
|
||||
DEF( nip_catch, 1, 2, 1, none) /* catch ... a -> a */
|
||||
|
||||
DEF( to_object, 1, 1, 1, none)
|
||||
//DEF( to_string, 1, 1, 1, none)
|
||||
DEF( to_propkey, 1, 1, 1, none)
|
||||
|
||||
DEF( make_loc_ref, 7, 0, 2, key_u16)
|
||||
DEF( make_arg_ref, 7, 0, 2, key_u16)
|
||||
DEF(make_var_ref_ref, 7, 0, 2, key_u16)
|
||||
DEF( make_var_ref, 5, 0, 2, key)
|
||||
|
||||
/* arithmetic/logic operations */
|
||||
DEF( neg, 1, 1, 1, none)
|
||||
DEF( plus, 1, 1, 1, none)
|
||||
@@ -185,7 +168,7 @@ DEF( add_loc, 2, 1, 0, loc8)
|
||||
DEF( not, 1, 1, 1, none)
|
||||
DEF( lnot, 1, 1, 1, none)
|
||||
DEF( delete, 1, 2, 1, none)
|
||||
DEF( delete_var, 5, 0, 1, key)
|
||||
DEF( delete_var, 5, 0, 1, key) /* deprecated - global object is immutable */
|
||||
|
||||
DEF( mul, 1, 2, 1, none)
|
||||
DEF( mul_float, 1, 2, 1, none)
|
||||
@@ -213,6 +196,16 @@ DEF( or, 1, 2, 1, none)
|
||||
/* template literal concatenation - pops N parts, pushes concatenated string */
|
||||
DEF(template_concat, 3, 0, 1, npop_u16)
|
||||
|
||||
/* Upvalue access (closures via outer_frame chain) */
|
||||
DEF( get_up, 4, 0, 1, u8_u16) /* depth:u8, slot:u16 -> value */
|
||||
DEF( set_up, 4, 1, 0, u8_u16) /* value, depth:u8, slot:u16 -> */
|
||||
|
||||
/* Name resolution with bytecode patching */
|
||||
DEF( get_name, 5, 0, 1, const) /* cpool_idx -> value, patches itself */
|
||||
DEF( get_env_slot, 3, 0, 1, u16) /* slot -> value (patched from get_name) */
|
||||
DEF(get_global_slot, 3, 0, 1, u16) /* slot -> value (patched from get_var) */
|
||||
DEF(set_global_slot, 3, 1, 0, u16) /* value -> slot (patched from put_var) */
|
||||
|
||||
/* must be the last non short and non temporary opcode */
|
||||
DEF( nop, 1, 0, 0, none)
|
||||
|
||||
@@ -229,8 +222,6 @@ def(scope_get_var_undef, 7, 0, 1, key_u16) /* emitted in phase 1, removed in pha
|
||||
def( scope_get_var, 7, 0, 1, key_u16) /* emitted in phase 1, removed in phase 2 */
|
||||
def( scope_put_var, 7, 1, 0, key_u16) /* emitted in phase 1, removed in phase 2 */
|
||||
def(scope_delete_var, 7, 0, 1, key_u16) /* emitted in phase 1, removed in phase 2 */
|
||||
def( scope_make_ref, 11, 0, 2, key_label_u16) /* emitted in phase 1, removed in phase 2 */
|
||||
def( scope_get_ref, 7, 0, 2, key_u16) /* emitted in phase 1, removed in phase 2 */
|
||||
def(scope_put_var_init, 7, 0, 2, key_u16) /* emitted in phase 1, removed in phase 2 */
|
||||
def(scope_get_var_checkthis, 7, 0, 1, key_u16) /* emitted in phase 1, removed in phase 2, only used to return 'this' in derived class constructors */
|
||||
def(get_field_opt_chain, 5, 1, 1, key) /* emitted in phase 1, removed in phase 2 */
|
||||
@@ -283,18 +274,6 @@ DEF( set_arg0, 1, 1, 1, none_arg)
|
||||
DEF( set_arg1, 1, 1, 1, none_arg)
|
||||
DEF( set_arg2, 1, 1, 1, none_arg)
|
||||
DEF( set_arg3, 1, 1, 1, none_arg)
|
||||
DEF( get_var_ref0, 1, 0, 1, none_var_ref)
|
||||
DEF( get_var_ref1, 1, 0, 1, none_var_ref)
|
||||
DEF( get_var_ref2, 1, 0, 1, none_var_ref)
|
||||
DEF( get_var_ref3, 1, 0, 1, none_var_ref)
|
||||
DEF( put_var_ref0, 1, 1, 0, none_var_ref)
|
||||
DEF( put_var_ref1, 1, 1, 0, none_var_ref)
|
||||
DEF( put_var_ref2, 1, 1, 0, none_var_ref)
|
||||
DEF( put_var_ref3, 1, 1, 0, none_var_ref)
|
||||
DEF( set_var_ref0, 1, 1, 1, none_var_ref)
|
||||
DEF( set_var_ref1, 1, 1, 1, none_var_ref)
|
||||
DEF( set_var_ref2, 1, 1, 1, none_var_ref)
|
||||
DEF( set_var_ref3, 1, 1, 1, none_var_ref)
|
||||
|
||||
DEF( if_false8, 2, 1, 0, label8)
|
||||
DEF( if_true8, 2, 1, 0, label8) /* must come after if_false8 */
|
||||
|
||||
12217
source/quickjs.c
12217
source/quickjs.c
File diff suppressed because it is too large
Load Diff
187
source/quickjs.h
187
source/quickjs.h
@@ -55,14 +55,13 @@ enum mist_obj_type {
|
||||
OBJ_FORWARD = 7
|
||||
};
|
||||
|
||||
#define OBJHDR_CAP_SHIFT 8u
|
||||
#define OBJHDR_CAP_MASK (((objhdr_t)1ull << 56) - 1ull)
|
||||
|
||||
#define OBJHDR_S_BIT 3u
|
||||
#define OBJHDR_P_BIT 4u
|
||||
#define OBJHDR_A_BIT 5u
|
||||
#define OBJHDR_R_BIT 7u
|
||||
|
||||
|
||||
#define OBJHDR_FLAG(bit) ((objhdr_t)1ull << (bit))
|
||||
#define OBJHDR_S_MASK OBJHDR_FLAG (OBJHDR_S_BIT)
|
||||
#define OBJHDR_P_MASK OBJHDR_FLAG (OBJHDR_P_BIT)
|
||||
@@ -150,6 +149,10 @@ enum {
|
||||
JS_TAG_CATCH_OFFSET = 0x1F, /* 11111 */
|
||||
};
|
||||
|
||||
/* Compatibility tag aliases for external code */
|
||||
#define JS_TAG_STRING JS_TAG_STRING_IMM /* Alias: text/string type */
|
||||
#define JS_TAG_FLOAT64 JS_TAG_SHORT_FLOAT /* Alias: floats use short float encoding */
|
||||
|
||||
#define JS_EMPTY_TEXT ((JSValue)JS_TAG_STRING_IMM)
|
||||
|
||||
/* JSGCRef - for rooting values during GC */
|
||||
@@ -173,6 +176,8 @@ void JS_DeleteGCRef(JSContext *ctx, JSGCRef *ref);
|
||||
Value Extraction
|
||||
============================================================ */
|
||||
|
||||
#define JS_VALUE_GET_INT(v) ((int)(v) >> 1)
|
||||
|
||||
/* Get primary tag (low 2-3 bits) */
|
||||
static inline int
|
||||
JS_VALUE_GET_TAG (JSValue v) {
|
||||
@@ -196,9 +201,6 @@ JS_VALUE_GET_TAG (JSValue v) {
|
||||
/* Extract bool value from special tag */
|
||||
#define JS_VALUE_GET_BOOL(v) ((int)((v) >> 5) & 1)
|
||||
|
||||
/* Extract pointer (clear low bits) */
|
||||
#define JS_VALUE_GET_PTR(v) ((void *)((v) & ~((JSValue)(JSW - 1))))
|
||||
|
||||
/* Extract special tag payload (bits 5+) */
|
||||
#define JS_VALUE_GET_SPECIAL_PAYLOAD(v) ((int32_t)((v) >> 5))
|
||||
|
||||
@@ -207,9 +209,6 @@ JS_VALUE_GET_TAG (JSValue v) {
|
||||
============================================================ */
|
||||
|
||||
#define JS_MKVAL(tag, val) _JS_MkVal (tag, val)
|
||||
#define JS_MKPTR(ptr) (((JSValue)(uintptr_t)(ptr)) | JS_TAG_PTR)
|
||||
#define JS_MKFWD(ptr) (((JSValue)(uintptr_t)(ptr)) | JS_TAG_FORWARD)
|
||||
|
||||
static inline JSValue _JS_MkVal (int tag, int32_t val) {
|
||||
if (tag == JS_TAG_INT) { return ((JSValue)(uint32_t)val << 1); }
|
||||
/* Special tags encode payload in upper bits */
|
||||
@@ -235,8 +234,8 @@ __JS_NewFloat64 (JSContext *ctx, double d) {
|
||||
int exp = (u.u >> 52) & 0x7FF;
|
||||
uint64_t mantissa = u.u & ((1ULL << 52) - 1);
|
||||
|
||||
/* Zero → short float zero */
|
||||
if (exp == 0 && mantissa == 0) { return (sign << 63) | JS_TAG_SHORT_FLOAT; }
|
||||
/* Zero → short float zero (always +0, no -0) */
|
||||
if (exp == 0 && mantissa == 0) { return JS_TAG_SHORT_FLOAT; }
|
||||
|
||||
/* NaN/Inf → NULL */
|
||||
if (exp == 0x7FF) { return JS_MKVAL (JS_TAG_NULL, 0); }
|
||||
@@ -268,7 +267,7 @@ static inline double JS_VALUE_GET_FLOAT64 (JSValue v) {
|
||||
uint64_t short_exp = (v >> 55) & 0xFF;
|
||||
uint64_t mantissa = (v >> 3) & ((1ULL << 52) - 1);
|
||||
|
||||
if (short_exp == 0) return sign ? -0.0 : 0.0;
|
||||
if (short_exp == 0) return 0.0; /* Always +0, no -0 */
|
||||
|
||||
uint64_t exp = short_exp - 127 + 1023;
|
||||
union {
|
||||
@@ -298,13 +297,10 @@ static inline double JS_VALUE_GET_FLOAT64 (JSValue v);
|
||||
============================================================ */
|
||||
|
||||
static inline JS_BOOL JS_IsInt (JSValue v) { return (v & 1) == 0; }
|
||||
static inline JS_BOOL JS_IsPtr (JSValue v) { return (v & 3) == JS_TAG_PTR; }
|
||||
static inline JS_BOOL JS_IsPtr (JSValue v) { return (v & 7) == JS_TAG_PTR; }
|
||||
static inline JS_BOOL JS_IsSpecial (JSValue v) { return (v & 3) == JS_TAG_SPECIAL; }
|
||||
|
||||
static inline JS_BOOL JS_IsStone(JSValue v) {
|
||||
if (!JS_IsPtr(v)) return 1;
|
||||
return objhdr_s(*(objhdr_t *)JS_VALUE_GET_PTR(v));
|
||||
}
|
||||
|
||||
|
||||
#ifdef JS_PTR64
|
||||
static inline JS_BOOL
|
||||
@@ -381,19 +377,17 @@ JSRuntime *JS_NewRuntime (void);
|
||||
/* info lifetime must exceed that of rt */
|
||||
void JS_SetRuntimeInfo (JSRuntime *rt, const char *info);
|
||||
void JS_SetMemoryLimit (JSRuntime *rt, size_t limit);
|
||||
void JS_SetGCThreshold (JSRuntime *rt, size_t gc_threshold);
|
||||
/* use 0 to disable maximum stack size check */
|
||||
void JS_SetMaxStackSize (JSRuntime *rt, size_t stack_size);
|
||||
/* should be called when changing thread to update the stack top value
|
||||
used to check stack overflow. */
|
||||
void JS_UpdateStackTop (JSRuntime *rt);
|
||||
JSRuntime *JS_NewRuntime2 (const JSMallocFunctions *mf, void *opaque);
|
||||
void JS_FreeRuntime (JSRuntime *rt);
|
||||
void *JS_GetRuntimeOpaque (JSRuntime *rt);
|
||||
void JS_SetRuntimeOpaque (JSRuntime *rt, void *opaque);
|
||||
typedef void JS_MarkFunc (JSRuntime *rt, JSGCObjectHeader *gp);
|
||||
/* JS_MarkValue is a no-op with copying GC (values are traced from roots) */
|
||||
void JS_MarkValue (JSRuntime *rt, JSValue val, JS_MarkFunc *mark_func);
|
||||
void JS_RunGC (JSRuntime *rt);
|
||||
JS_BOOL JS_IsLiveObject (JSRuntime *rt, JSValue obj);
|
||||
|
||||
JSContext *JS_NewContext (JSRuntime *rt);
|
||||
@@ -409,6 +403,7 @@ JSValue JS_GetClassProto (JSContext *ctx, JSClassID class_id);
|
||||
/* the following functions are used to select the intrinsic object to
|
||||
save memory */
|
||||
JSContext *JS_NewContextRaw (JSRuntime *rt);
|
||||
JSContext *JS_NewContextRawWithHeapSize (JSRuntime *rt, size_t heap_size);
|
||||
|
||||
typedef struct JSMemoryUsage {
|
||||
int64_t malloc_size, malloc_limit, memory_used_size;
|
||||
@@ -454,6 +449,18 @@ int JS_IsRegisteredClass (JSRuntime *rt, JSClassID class_id);
|
||||
|
||||
extern JSClassID js_class_id_alloc;
|
||||
|
||||
/* ============================================================
|
||||
Copying GC - No Reference Counting Needed
|
||||
============================================================
|
||||
With a copying GC, reference counting is not needed since all live
|
||||
objects are discovered by tracing from roots. These macros make
|
||||
existing DupValue/FreeValue calls into no-ops.
|
||||
============================================================ */
|
||||
#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)
|
||||
|
||||
/* value handling */
|
||||
|
||||
static inline JSValue
|
||||
@@ -570,37 +577,15 @@ static inline JS_BOOL JS_IsObject (JSValue v) {
|
||||
return JS_IsPtr (v);
|
||||
}
|
||||
|
||||
static inline JS_BOOL JS_IsArray(JSValue v) {
|
||||
return JS_IsObject(v) && objhdr_type(*(objhdr_t *)JS_VALUE_GET_PTR(v)) == OBJ_ARRAY;
|
||||
}
|
||||
|
||||
static inline JS_BOOL JS_IsRecord (JSValue v) {
|
||||
return JS_IsObject(v) && objhdr_type(*(objhdr_t *)JS_VALUE_GET_PTR(v)) == OBJ_RECORD;
|
||||
}
|
||||
|
||||
static inline JS_BOOL JS_IsFunction (JSValue v) {
|
||||
return JS_IsObject(v) && objhdr_type(*(objhdr_t *)JS_VALUE_GET_PTR(v)) == OBJ_FUNCTION;
|
||||
}
|
||||
|
||||
static inline JS_BOOL JS_IsCode (JSValue v) {
|
||||
return JS_IsObject(v) && objhdr_type(*(objhdr_t *)JS_VALUE_GET_PTR(v)) == OBJ_CODE;
|
||||
}
|
||||
|
||||
static inline JS_BOOL JS_IsForwarded (JSValue v) {
|
||||
return JS_IsObject(v) && objhdr_type(*(objhdr_t *)JS_VALUE_GET_PTR(v)) == OBJ_FORWARD;
|
||||
}
|
||||
|
||||
static inline JS_BOOL JS_IsFrame (JSValue v) {
|
||||
return JS_IsObject(v) && objhdr_type(*(objhdr_t *)JS_VALUE_GET_PTR(v)) == OBJ_FRAME;
|
||||
}
|
||||
|
||||
static inline JS_BOOL JS_IsBlob (JSValue v) {
|
||||
return JS_IsObject(v) && objhdr_type(*(objhdr_t *)JS_VALUE_GET_PTR(v)) == OBJ_BLOB;
|
||||
}
|
||||
|
||||
static inline JS_BOOL JS_IsText(JSValue v) {
|
||||
return MIST_IsImmediateASCII(v) || (JS_IsObject(v) && objhdr_type(*(objhdr_t *)JS_VALUE_GET_PTR(v)) == OBJ_TEXT);
|
||||
}
|
||||
JS_BOOL JS_IsArray(JSValue v);
|
||||
JS_BOOL JS_IsRecord(JSValue v);
|
||||
JS_BOOL JS_IsFunction(JSValue v);
|
||||
JS_BOOL JS_IsCode(JSValue v);
|
||||
JS_BOOL JS_IsForwarded(JSValue v);
|
||||
JS_BOOL JS_IsFrame(JSValue v);
|
||||
JS_BOOL JS_IsBlob(JSValue v);
|
||||
JS_BOOL JS_IsText(JSValue v);
|
||||
static JS_BOOL JS_IsStone(JSValue v);
|
||||
|
||||
// Fundamental
|
||||
int JS_GetLength (JSContext *ctx, JSValue obj, int64_t *pres);
|
||||
@@ -624,7 +609,6 @@ JSValue __js_printf_like (2, 3)
|
||||
JSValue JS_ThrowOutOfMemory (JSContext *ctx);
|
||||
|
||||
JS_BOOL JS_StrictEq (JSContext *ctx, JSValue op1, JSValue op2);
|
||||
JS_BOOL JS_SameValue (JSContext *ctx, JSValue op1, JSValue op2);
|
||||
|
||||
int JS_ToBool (JSContext *ctx, JSValue val); /* return -1 for JS_EXCEPTION */
|
||||
int JS_ToInt32 (JSContext *ctx, int32_t *pres, JSValue val);
|
||||
@@ -657,8 +641,71 @@ JSValue JS_NewObject (JSContext *ctx);
|
||||
|
||||
JSValue JS_NewArray (JSContext *ctx);
|
||||
JSValue JS_NewArrayLen (JSContext *ctx, uint32_t len);
|
||||
int JS_ArrayPush (JSContext *ctx, JSValue obj, JSValue val);
|
||||
|
||||
/* GC-safe push: takes pointer to array, updates it if array grows */
|
||||
int JS_ArrayPush (JSContext *ctx, JSValue *arr_ptr, JSValue val);
|
||||
JSValue JS_ArrayPop (JSContext *ctx, JSValue obj);
|
||||
|
||||
/* Intrinsic array operations - signatures match internal functions */
|
||||
JSValue JS_Array (JSContext *ctx, JSValue arg0, JSValue arg1, JSValue arg2, JSValue arg3);
|
||||
JSValue JS_ArrayFilter (JSContext *ctx, JSValue arr, JSValue fn);
|
||||
JSValue JS_ArraySort (JSContext *ctx, JSValue arr, JSValue selector);
|
||||
JSValue JS_ArrayFind (JSContext *ctx, JSValue arr, JSValue target_or_fn, JSValue reverse, JSValue from);
|
||||
JSValue JS_ArrFor (JSContext *ctx, JSValue arr, JSValue fn, JSValue reverse, JSValue exit_val);
|
||||
JSValue JS_ArrayReduce (JSContext *ctx, JSValue arr, JSValue fn, JSValue initial, JSValue reverse);
|
||||
|
||||
/* Cell intrinsic functions - C API wrappers */
|
||||
|
||||
/* Core functions */
|
||||
JSValue JS_CellStone (JSContext *ctx, JSValue val);
|
||||
JSValue JS_CellLength (JSContext *ctx, JSValue val);
|
||||
JSValue JS_CellReverse (JSContext *ctx, JSValue val);
|
||||
JSValue JS_CellProto (JSContext *ctx, JSValue obj);
|
||||
JSValue JS_CellSplat (JSContext *ctx, JSValue val);
|
||||
JSValue JS_CellMeme (JSContext *ctx, JSValue obj, JSValue deep);
|
||||
JSValue JS_CellApply (JSContext *ctx, JSValue fn, JSValue args);
|
||||
JSValue JS_CellCall (JSContext *ctx, JSValue fn, JSValue this_val, JSValue args);
|
||||
JSValue JS_CellModulo (JSContext *ctx, JSValue a, JSValue b);
|
||||
JSValue JS_CellNeg (JSContext *ctx, JSValue val);
|
||||
JSValue JS_CellNot (JSContext *ctx, JSValue val);
|
||||
|
||||
/* Text functions */
|
||||
JSValue JS_CellText (JSContext *ctx, JSValue val);
|
||||
JSValue JS_CellLower (JSContext *ctx, JSValue text);
|
||||
JSValue JS_CellUpper (JSContext *ctx, JSValue text);
|
||||
JSValue JS_CellTrim (JSContext *ctx, JSValue text, JSValue chars);
|
||||
JSValue JS_CellCodepoint (JSContext *ctx, JSValue text, JSValue idx);
|
||||
JSValue JS_CellReplace (JSContext *ctx, JSValue text, JSValue pattern, JSValue replacement);
|
||||
JSValue JS_CellSearch (JSContext *ctx, JSValue text, JSValue pattern, JSValue from);
|
||||
JSValue JS_CellExtract (JSContext *ctx, JSValue text, JSValue from, JSValue to);
|
||||
JSValue JS_CellCharacter (JSContext *ctx, JSValue codepoint);
|
||||
|
||||
/* Number functions */
|
||||
JSValue JS_CellNumber (JSContext *ctx, JSValue val);
|
||||
JSValue JS_CellAbs (JSContext *ctx, JSValue num);
|
||||
JSValue JS_CellSign (JSContext *ctx, JSValue num);
|
||||
JSValue JS_CellFloor (JSContext *ctx, JSValue num);
|
||||
JSValue JS_CellCeiling (JSContext *ctx, JSValue num);
|
||||
JSValue JS_CellRound (JSContext *ctx, JSValue num);
|
||||
JSValue JS_CellTrunc (JSContext *ctx, JSValue num);
|
||||
JSValue JS_CellWhole (JSContext *ctx, JSValue num);
|
||||
JSValue JS_CellFraction (JSContext *ctx, JSValue num);
|
||||
JSValue JS_CellMin (JSContext *ctx, JSValue a, JSValue b);
|
||||
JSValue JS_CellMax (JSContext *ctx, JSValue a, JSValue b);
|
||||
JSValue JS_CellRemainder (JSContext *ctx, JSValue a, JSValue b);
|
||||
|
||||
/* Object functions */
|
||||
JSValue JS_CellObject (JSContext *ctx, JSValue proto, JSValue props);
|
||||
|
||||
/* Format function */
|
||||
JSValue JS_CellFormat (JSContext *ctx, JSValue text, JSValue collection, JSValue transformer);
|
||||
|
||||
/* Helper functions */
|
||||
JSValue JS_NewArrayFrom (JSContext *ctx, int count, JSValue *values);
|
||||
void JS_PrintText (JSContext *ctx, JSValue val);
|
||||
void JS_PrintTextLn (JSContext *ctx, JSValue val);
|
||||
void JS_PrintFormatted (JSContext *ctx, const char *fmt, int count, JSValue *values);
|
||||
|
||||
JSValue JS_GetProperty (JSContext *ctx, JSValue this_obj, JSValue prop);
|
||||
|
||||
// For records
|
||||
@@ -736,6 +783,23 @@ JSValue JS_ReadObject (JSContext *ctx, const uint8_t *buf, size_t buf_len,
|
||||
reading a script or module with JS_ReadObject() */
|
||||
JSValue JS_EvalFunction (JSContext *ctx, JSValue fun_obj);
|
||||
|
||||
/* Eval function with environment record for variable resolution.
|
||||
The env must be a stoned record. Variables are resolved env first,
|
||||
then global intrinsics. */
|
||||
JSValue JS_EvalFunctionEnv (JSContext *ctx, JSValue fun_obj, JSValue env);
|
||||
|
||||
/* Dump bytecode of a compiled function (for debugging) */
|
||||
void JS_DumpFunctionBytecode (JSContext *ctx, JSValue func_val);
|
||||
|
||||
/* Link compiled bytecode to context - resolves global references.
|
||||
Returns linked bytecode on success, JS_EXCEPTION on link error. */
|
||||
JSValue JS_LinkFunction (JSContext *ctx, JSValue func_val);
|
||||
|
||||
/* Link compiled bytecode with environment record for variable resolution.
|
||||
Variables are resolved: env first, then global intrinsics.
|
||||
Returns linked bytecode on success, JS_EXCEPTION on link error. */
|
||||
JSValue JS_LinkFunctionEnv (JSContext *ctx, JSValue func_val, JSValue env);
|
||||
|
||||
/* C function definition */
|
||||
typedef enum JSCFunctionEnum {
|
||||
JS_CFUNC_generic,
|
||||
@@ -991,6 +1055,25 @@ JSValue js_debugger_fn_info (JSContext *ctx, JSValue fn);
|
||||
JSValue js_debugger_fn_bytecode (JSContext *js, JSValue fn);
|
||||
void *js_debugger_val_address (JSContext *js, JSValue val);
|
||||
|
||||
/* Memory allocation functions (bump allocator) */
|
||||
void *js_malloc (JSContext *ctx, size_t size);
|
||||
void *js_mallocz (JSContext *ctx, size_t size);
|
||||
void *js_realloc (JSContext *ctx, void *ptr, size_t size);
|
||||
void js_free (JSContext *ctx, void *ptr);
|
||||
char *js_strdup (JSContext *ctx, const char *str);
|
||||
|
||||
/* Runtime-level memory functions */
|
||||
void *js_malloc_rt (size_t size);
|
||||
void *js_mallocz_rt (size_t size);
|
||||
void js_free_rt (void *ptr);
|
||||
|
||||
/* Intrinsic setup functions */
|
||||
void JS_AddIntrinsicBaseObjects (JSContext *ctx);
|
||||
void JS_AddIntrinsicBasicObjects (JSContext *ctx);
|
||||
void JS_AddIntrinsicEval (JSContext *ctx);
|
||||
void JS_AddIntrinsicRegExp (JSContext *ctx);
|
||||
void JS_AddIntrinsicJSON (JSContext *ctx);
|
||||
|
||||
#undef js_unlikely
|
||||
#undef inline
|
||||
|
||||
|
||||
2077
source/suite.c
Normal file
2077
source/suite.c
Normal file
File diff suppressed because it is too large
Load Diff
190
tests/suite.cm
190
tests/suite.cm
@@ -1244,15 +1244,16 @@ return {
|
||||
// EDGE CASES AND SPECIAL VALUES
|
||||
// ============================================================================
|
||||
|
||||
test_infinity: function() {
|
||||
test_division_by_zero_is_null: function() {
|
||||
var inf = 1 / 0
|
||||
if (!(inf > 1000000)) throw "infinity failed"
|
||||
if (!(-inf < -1000000)) throw "negative infinity failed"
|
||||
if (inf != null) throw "division by zero should be null"
|
||||
var ninf = -1 / 0
|
||||
if (ninf != null) throw "negative division by zero should be null"
|
||||
},
|
||||
|
||||
test_nan: function() {
|
||||
test_zero_div_zero_is_null: function() {
|
||||
var nan = 0 / 0
|
||||
if (nan == nan) throw "NaN should not equal itself"
|
||||
if (nan != null) throw "0/0 should be null"
|
||||
},
|
||||
|
||||
test_max_safe_integer: function() {
|
||||
@@ -1403,17 +1404,36 @@ return {
|
||||
|
||||
test_number_division_by_zero: function() {
|
||||
var result = 1 / 0
|
||||
if (!(result > 1000000)) throw "division by zero should give infinity"
|
||||
if (result != null) throw "division by zero should give null"
|
||||
},
|
||||
|
||||
test_number_negative_division_by_zero: function() {
|
||||
var result = -1 / 0
|
||||
if (!(result < -1000000)) throw "negative division by zero should give -infinity"
|
||||
if (result != null) throw "negative division by zero should give null"
|
||||
},
|
||||
|
||||
test_zero_division_by_zero: function() {
|
||||
var result = 0 / 0
|
||||
if (result == result) throw "0/0 should give NaN"
|
||||
if (result != null) throw "0/0 should give null"
|
||||
},
|
||||
|
||||
test_negative_zero_normalized: function() {
|
||||
var nz = -0
|
||||
if (nz != 0) throw "-0 should equal 0"
|
||||
var mul_nz = 0 * -1
|
||||
if (mul_nz != 0) throw "0 * -1 should be 0"
|
||||
var neg_zero = -(0)
|
||||
if (neg_zero != 0) throw "-(0) should be 0"
|
||||
},
|
||||
|
||||
test_overflow_is_null: function() {
|
||||
var result = 1e38 * 1e38
|
||||
if (result != null) throw "overflow should give null"
|
||||
},
|
||||
|
||||
test_modulo_by_zero_is_null: function() {
|
||||
var result = 5 % 0
|
||||
if (result != null) throw "modulo by zero should give null"
|
||||
},
|
||||
|
||||
// ============================================================================
|
||||
@@ -3506,4 +3526,158 @@ return {
|
||||
if (obj.beta[1] != obj) throw "text key cycle failed"
|
||||
},
|
||||
|
||||
// ============================================================================
|
||||
// OBJECT INTRINSIC TESTS
|
||||
// ============================================================================
|
||||
|
||||
test_object_shallow_copy: function() {
|
||||
var orig = {a: 1, b: 2, c: 3}
|
||||
var copy = object(orig)
|
||||
if (copy.a != 1) throw "object copy a failed"
|
||||
if (copy.b != 2) throw "object copy b failed"
|
||||
if (copy.c != 3) throw "object copy c failed"
|
||||
copy.a = 99
|
||||
if (orig.a != 1) throw "object copy should not mutate original"
|
||||
},
|
||||
|
||||
test_object_combine: function() {
|
||||
var obj1 = {a: 1, b: 2}
|
||||
var obj2 = {c: 3, d: 4}
|
||||
var combined = object(obj1, obj2)
|
||||
if (combined.a != 1) throw "object combine a failed"
|
||||
if (combined.b != 2) throw "object combine b failed"
|
||||
if (combined.c != 3) throw "object combine c failed"
|
||||
if (combined.d != 4) throw "object combine d failed"
|
||||
},
|
||||
|
||||
test_object_combine_override: function() {
|
||||
var obj1 = {a: 1, b: 2}
|
||||
var obj2 = {b: 99, c: 3}
|
||||
var combined = object(obj1, obj2)
|
||||
if (combined.a != 1) throw "object combine override a failed"
|
||||
if (combined.b != 99) throw "object combine should override with second arg"
|
||||
if (combined.c != 3) throw "object combine override c failed"
|
||||
},
|
||||
|
||||
test_object_select_keys: function() {
|
||||
var orig = {a: 1, b: 2, c: 3, d: 4}
|
||||
var selected = object(orig, ["a", "c"])
|
||||
if (selected.a != 1) throw "object select a failed"
|
||||
if (selected.c != 3) throw "object select c failed"
|
||||
if (selected.b != null) throw "object select should not include b"
|
||||
if (selected.d != null) throw "object select should not include d"
|
||||
},
|
||||
|
||||
test_object_from_keys_true: function() {
|
||||
var keys = ["x", "y", "z"]
|
||||
var obj = object(keys)
|
||||
if (obj.x != true) throw "object from keys x failed"
|
||||
if (obj.y != true) throw "object from keys y failed"
|
||||
if (obj.z != true) throw "object from keys z failed"
|
||||
},
|
||||
|
||||
test_object_from_keys_function: function() {
|
||||
var keys = ["a", "b", "c"]
|
||||
var obj = object(keys, function(k) { return k + "_val" })
|
||||
if (obj.a != "a_val") throw "object from keys func a failed"
|
||||
if (obj.b != "b_val") throw "object from keys func b failed"
|
||||
if (obj.c != "c_val") throw "object from keys func c failed"
|
||||
},
|
||||
|
||||
// ============================================================================
|
||||
// SPLAT INTRINSIC TESTS
|
||||
// ============================================================================
|
||||
|
||||
test_splat_prototype_flattening: function() {
|
||||
var proto = {x: 10, y: 20}
|
||||
var obj = {z: 30}
|
||||
obj.__proto__ = proto
|
||||
var flat = splat(obj)
|
||||
if (flat.x != 10) throw "splat x failed"
|
||||
if (flat.y != 20) throw "splat y failed"
|
||||
if (flat.z != 30) throw "splat z failed"
|
||||
},
|
||||
|
||||
// ============================================================================
|
||||
// REVERSE INTRINSIC TESTS
|
||||
// ============================================================================
|
||||
|
||||
test_reverse_array: function() {
|
||||
var arr = [1, 2, 3, 4, 5]
|
||||
var rev = reverse(arr)
|
||||
if (rev[0] != 5) throw "reverse[0] failed"
|
||||
if (rev[1] != 4) throw "reverse[1] failed"
|
||||
if (rev[2] != 3) throw "reverse[2] failed"
|
||||
if (rev[3] != 2) throw "reverse[3] failed"
|
||||
if (rev[4] != 1) throw "reverse[4] failed"
|
||||
if (arr[0] != 1) throw "reverse should not mutate original"
|
||||
},
|
||||
|
||||
// ============================================================================
|
||||
// APPLY INTRINSIC TESTS
|
||||
// ============================================================================
|
||||
|
||||
test_apply_with_array_args: function() {
|
||||
def sum = function(a, b, c) { return a + b + c }
|
||||
var result = fn.apply(sum, [1, 2, 3])
|
||||
if (result != 6) throw "apply with array args failed"
|
||||
},
|
||||
|
||||
test_apply_with_no_args: function() {
|
||||
def ret42 = function() { return 42 }
|
||||
var result = fn.apply(ret42)
|
||||
if (result != 42) throw "apply with no args failed"
|
||||
},
|
||||
|
||||
test_apply_with_single_value: function() {
|
||||
def double = function(x) { return x * 2 }
|
||||
var result = fn.apply(double, 10)
|
||||
if (result != 20) throw "apply with single value failed"
|
||||
},
|
||||
|
||||
// ============================================================================
|
||||
// GC STRESS TESTS FOR FIXED INTRINSICS
|
||||
// ============================================================================
|
||||
|
||||
test_gc_reverse_under_pressure: function() {
|
||||
// Create GC pressure by making many arrays, then reverse
|
||||
var arrays = []
|
||||
for (var i = 0; i < 100; i = i + 1) {
|
||||
arrays[i] = [i, i+1, i+2, i+3, i+4]
|
||||
}
|
||||
// Now reverse each one - this tests re-chase after allocation
|
||||
for (var i = 0; i < 100; i = i + 1) {
|
||||
var rev = reverse(arrays[i])
|
||||
if (rev[0] != i+4) throw "gc reverse stress failed at " + text(i)
|
||||
}
|
||||
},
|
||||
|
||||
test_gc_object_select_under_pressure: function() {
|
||||
// Create GC pressure
|
||||
var objs = []
|
||||
for (var i = 0; i < 100; i = i + 1) {
|
||||
objs[i] = {a: i, b: i+1, c: i+2, d: i+3}
|
||||
}
|
||||
// Select keys - tests re-chase in loop
|
||||
for (var i = 0; i < 100; i = i + 1) {
|
||||
var selected = object(objs[i], ["a", "c"])
|
||||
if (selected.a != i) throw "gc object select stress failed at " + text(i)
|
||||
if (selected.c != i+2) throw "gc object select stress c failed at " + text(i)
|
||||
}
|
||||
},
|
||||
|
||||
test_gc_object_from_keys_function_under_pressure: function() {
|
||||
// Create GC pressure
|
||||
var keysets = []
|
||||
for (var i = 0; i < 50; i = i + 1) {
|
||||
keysets[i] = ["k" + text(i), "j" + text(i), "m" + text(i)]
|
||||
}
|
||||
// Create objects with function - tests JS_PUSH/POP and re-chase
|
||||
for (var i = 0; i < 50; i = i + 1) {
|
||||
var obj = object(keysets[i], function(k) { return k + "_value" })
|
||||
var expected = "k" + text(i) + "_value"
|
||||
if (obj["k" + text(i)] != expected) throw "gc object from keys func stress failed at " + text(i)
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user