7.1 KiB
Managed Stack Frames Implementation Plan
This document outlines the requirements and invariants for implementing fully managed stack frames in QuickJS, eliminating recursion through the C stack for JS->JS calls.
Overview
The goal is to maintain interpreter state entirely on managed stacks (value stack + frame stack) rather than relying on C stack frames. This enables:
- Call IC fast path: Direct dispatch to C functions without js_call_c_function overhead
- Proper stack traces: Error().stack works correctly even through optimized paths
- Tail call optimization: Possible without C stack growth
- Debugging/profiling: Full interpreter state always inspectable
Current State
- Property IC: Implemented with per-function polymorphic IC (up to 4 shapes per site)
- Call IC: Infrastructure exists but disabled (
CALL_IC_ENABLED 0) because it bypasses stack frame setup required for Error().stack
Golden Invariant
At any time, the entire live interpreter state must be reconstructible from:
(ctx->value_stack, value_top) + (ctx->frame_stack, frame_top)
No critical state may live only in C locals.
Implementation Requirements
1. Offset Semantics (use size_t / uint32_t)
Replace pointer-based addressing with offset-based addressing:
typedef struct JSStackFrame {
uint32_t sp_offset; // Offset into ctx->value_stack
uint32_t var_offset; // Start of local variables
uint32_t arg_offset; // Start of arguments
// ... continuation info below
} JSStackFrame;
Rationale: Offsets survive stack reallocation, pointers don't.
2. Consistent sp_offset Semantics
Define clearly and consistently:
sp_offset= current stack pointer offset fromctx->value_stack- On function entry:
sp_offsetpoints to first free slot after arguments - On function exit:
sp_offsetrestored to caller's expected position
3. Continuation Info (Caller State Restoration)
Each frame must store enough to restore caller state on return:
typedef struct JSStackFrame {
// ... other fields
// Continuation info
const uint8_t *caller_pc; // Return address in caller's bytecode
uint32_t caller_sp_offset; // Caller's stack pointer
JSFunctionBytecode *caller_b; // Caller's bytecode (for IC cache)
// Current function info
JSFunctionBytecode *b; // Current function's bytecode
JSValue *var_buf; // Can be offset-based
JSValue *arg_buf; // Can be offset-based
JSValue this_val;
} JSStackFrame;
4. Exception Handler Stack Depth Restoration
Exception handlers must record the sp_offset at handler entry so throw can restore the correct stack depth:
typedef struct JSExceptionHandler {
uint32_t sp_offset; // Stack depth to restore on throw
const uint8_t *catch_pc; // Where to jump on exception
// ...
} JSExceptionHandler;
On throw:
- Unwind frame stack to find appropriate handler
- Restore
sp_offsetto handler's recorded value - Push exception value
- Jump to
catch_pc
5. Aliased argv Handling
When arguments object exists, argv may be aliased. The frame must track this:
typedef struct JSStackFrame {
// ...
uint16_t flags;
#define JS_FRAME_ALIASED_ARGV (1 << 0)
#define JS_FRAME_STRICT (1 << 1)
// ...
JSObject *arguments_obj; // Non-NULL if arguments object created
} JSStackFrame;
When JS_FRAME_ALIASED_ARGV is set, writes to arguments[i] must update the corresponding local variable.
6. Stack Trace Accuracy (sf->cur_pc)
Critical: sf->cur_pc must be updated before any operation that could:
- Throw an exception
- Call into another function
- Trigger GC
Currently the interpreter does:
sf->cur_pc = pc; // Before potentially-throwing ops
With managed frames, ensure this is consistently done or use a different mechanism (e.g., store pc in frame on every call).
7. GC Integration
The GC must be able to mark all live values on the managed stacks:
void js_gc_mark_value_stack(JSRuntime *rt) {
for (JSContext *ctx = rt->context_list; ctx; ctx = ctx->link) {
JSValue *p = ctx->value_stack;
JSValue *end = ctx->value_stack + ctx->value_top;
while (p < end) {
JS_MarkValue(rt, *p);
p++;
}
}
}
void js_gc_mark_frame_stack(JSRuntime *rt) {
for (JSContext *ctx = rt->context_list; ctx; ctx = ctx->link) {
JSStackFrame *sf = ctx->frame_stack;
JSStackFrame *end = ctx->frame_stack + ctx->frame_top;
while (sf < end) {
JS_MarkValue(rt, sf->this_val);
// Mark any other JSValue fields in frame
sf++;
}
}
}
8. Main Interpreter Loop Changes
Transform from recursive to iterative:
// Current (recursive):
JSValue JS_CallInternal(...) {
// ...
CASE(OP_call):
// Recursive call to JS_CallInternal
ret = JS_CallInternal(ctx, func, ...);
// ...
}
// Target (iterative):
JSValue JS_CallInternal(...) {
// ...
CASE(OP_call):
// Push new frame, update pc to callee entry
push_frame(ctx, ...);
pc = new_func->byte_code_buf;
BREAK; // Continue in same loop iteration
CASE(OP_return):
// Pop frame, restore caller state
ret_val = sp[-1];
pop_frame(ctx, &pc, &sp, &b);
sp[0] = ret_val;
BREAK; // Continue executing caller
// ...
}
Call IC Integration (After Managed Frames)
Once managed frames are complete, Call IC becomes safe:
CASE(OP_call_method):
// ... resolve method ...
if (JS_VALUE_GET_TAG(method) == JS_TAG_OBJECT) {
JSObject *p = JS_VALUE_GET_OBJ(method);
// Check Call IC
CallICEntry *entry = call_ic_lookup(cache, pc_offset, p->shape);
if (entry && entry->cfunc) {
// Direct C call - safe because frame is on managed stack
push_minimal_frame(ctx, pc, sp_offset);
ret = entry->cfunc(ctx, this_val, argc, argv);
pop_minimal_frame(ctx);
// Handle return...
}
}
// Slow path: full call
Testing Strategy
- Stack trace tests: Verify Error().stack works through all call patterns
- Exception tests: Verify throw/catch restores correct stack depth
- GC stress tests: Verify all values are properly marked during GC
- Benchmark: Compare performance before/after
Migration Steps
- Add offset fields to JSStackFrame alongside existing pointers
- Create push_frame/pop_frame helper functions
- Convert OP_call to use push_frame instead of recursion (JS->JS calls)
- Convert OP_return to use pop_frame
- Update exception handling to use offset-based stack restoration
- Update GC to walk managed stacks
- Remove/deprecate recursive JS_CallInternal calls for JS functions
- Enable Call IC for C functions
- Benchmark and optimize
References
- Current IC implementation:
source/quickjs.clines 12567-12722 (ICCache, prop_ic_*) - Current stack frame:
source/quickjs.cJSStackFrame definition - OP_call_method:
source/quickjs.clines 13654-13718