Compare commits
10 Commits
stack
...
tramp_loop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7af8ffe4e0 | ||
|
|
8abee37622 | ||
|
|
7c3cce1ce2 | ||
|
|
334f3a789b | ||
|
|
e21cd4e70b | ||
|
|
41eb4bf6f7 | ||
|
|
5f761cc7af | ||
|
|
7ae5a0c06b | ||
|
|
0664c11af6 | ||
|
|
058ad89c96 |
@@ -1,231 +0,0 @@
|
||||
# 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:
|
||||
|
||||
```c
|
||||
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 from `ctx->value_stack`
|
||||
- On function entry: `sp_offset` points to first free slot after arguments
|
||||
- On function exit: `sp_offset` restored to caller's expected position
|
||||
|
||||
### 3. Continuation Info (Caller State Restoration)
|
||||
|
||||
Each frame must store enough to restore caller state on return:
|
||||
|
||||
```c
|
||||
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:
|
||||
|
||||
```c
|
||||
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`:
|
||||
1. Unwind frame stack to find appropriate handler
|
||||
2. Restore `sp_offset` to handler's recorded value
|
||||
3. Push exception value
|
||||
4. Jump to `catch_pc`
|
||||
|
||||
### 5. Aliased `argv` Handling
|
||||
|
||||
When `arguments` object exists, `argv` may be aliased. The frame must track this:
|
||||
|
||||
```c
|
||||
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:
|
||||
```c
|
||||
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:
|
||||
|
||||
```c
|
||||
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:
|
||||
|
||||
```c
|
||||
// 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:
|
||||
|
||||
```c
|
||||
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
|
||||
|
||||
1. **Stack trace tests**: Verify Error().stack works through all call patterns
|
||||
2. **Exception tests**: Verify throw/catch restores correct stack depth
|
||||
3. **GC stress tests**: Verify all values are properly marked during GC
|
||||
4. **Benchmark**: Compare performance before/after
|
||||
|
||||
## Migration Steps
|
||||
|
||||
1. [ ] Add offset fields to JSStackFrame alongside existing pointers
|
||||
2. [ ] Create push_frame/pop_frame helper functions
|
||||
3. [ ] Convert OP_call to use push_frame instead of recursion (JS->JS calls)
|
||||
4. [ ] Convert OP_return to use pop_frame
|
||||
5. [ ] Update exception handling to use offset-based stack restoration
|
||||
6. [ ] Update GC to walk managed stacks
|
||||
7. [ ] Remove/deprecate recursive JS_CallInternal calls for JS functions
|
||||
8. [ ] Enable Call IC for C functions
|
||||
9. [ ] Benchmark and optimize
|
||||
|
||||
## References
|
||||
|
||||
- Current IC implementation: `source/quickjs.c` lines 12567-12722 (ICCache, prop_ic_*)
|
||||
- Current stack frame: `source/quickjs.c` JSStackFrame definition
|
||||
- OP_call_method: `source/quickjs.c` lines 13654-13718
|
||||
@@ -140,19 +140,12 @@ globalThis.isa = function(value, master) {
|
||||
var ENETSERVICE = 0.1
|
||||
var REPLYTIMEOUT = 60 // seconds before replies are ignored
|
||||
|
||||
var nullguard = false
|
||||
function caller_data(depth = 0)
|
||||
{
|
||||
var file = "nofile"
|
||||
var line = 0
|
||||
|
||||
var caller = new Error().stack.split("\n")[1+depth]
|
||||
if (!nullguard && is_null(caller)) {
|
||||
os.print(`caller_data now getting null`)
|
||||
os.print("\n")
|
||||
nullguard = true
|
||||
}
|
||||
|
||||
if (caller) {
|
||||
var md = caller.match(/\((.*)\:/)
|
||||
var m = md ? md[1] : "SCRIPT"
|
||||
@@ -815,29 +808,19 @@ if (!locator)
|
||||
stone(globalThis)
|
||||
|
||||
var rads = use_core("math/radians")
|
||||
log.console(rads)
|
||||
log.console("now, should be nofile:0")
|
||||
|
||||
$_.clock(_ => {
|
||||
log.console("in clock")
|
||||
// Get capabilities for the main program
|
||||
var file_info = shop.file_info ? shop.file_info(locator.path) : null
|
||||
var inject = shop.script_inject_for ? shop.script_inject_for(file_info) : []
|
||||
|
||||
log.console("injection")
|
||||
|
||||
// Build values array for injection
|
||||
var vals = []
|
||||
log.console(`number to inject is ${inject.length}`)
|
||||
log.console('when the log.console statements are in the loop, with backticks, it runs but with errors on the injectables especially substring not seeming to work; without them, it totally fails')
|
||||
for (var i = 0; i < inject.length; i++) {
|
||||
var key = inject[i]
|
||||
log.console(`injecting ${i}, which is ${key}`) // when this line is present, works; when not present, does not work
|
||||
|
||||
if (key && key[0] == '$') key = key.substring(1)
|
||||
if (key == 'fd') vals.push(fd)
|
||||
else vals.push($_[key])
|
||||
log.console(`split at 1 was ${key}`)
|
||||
}
|
||||
|
||||
// Create use function bound to the program's package
|
||||
|
||||
@@ -28,6 +28,10 @@
|
||||
#include <stddef.h>
|
||||
#endif
|
||||
|
||||
#ifndef NDEBUG
|
||||
#include <assert.h>
|
||||
#endif
|
||||
|
||||
struct list_head {
|
||||
struct list_head *prev;
|
||||
struct list_head *next;
|
||||
@@ -82,6 +86,29 @@ static inline int list_empty(struct list_head *el)
|
||||
return el->next == el;
|
||||
}
|
||||
|
||||
/* Move all elements from 'src' to 'dst', leaving 'src' empty.
|
||||
'dst' must be empty before this call. */
|
||||
static inline void list_splice(struct list_head *dst, struct list_head *src)
|
||||
{
|
||||
#ifndef NDEBUG
|
||||
assert(dst != src);
|
||||
assert(list_empty(dst));
|
||||
#endif
|
||||
if (!list_empty(src)) {
|
||||
struct list_head *first = src->next;
|
||||
struct list_head *last = src->prev;
|
||||
|
||||
/* Link dst to src's elements */
|
||||
dst->next = first;
|
||||
dst->prev = last;
|
||||
first->prev = dst;
|
||||
last->next = dst;
|
||||
|
||||
/* Reinitialize src as empty */
|
||||
init_list_head(src);
|
||||
}
|
||||
}
|
||||
|
||||
#define list_for_each(el, head) \
|
||||
for(el = (head)->next; el != (head); el = el->next)
|
||||
|
||||
|
||||
@@ -207,10 +207,14 @@ DEF( delete, 1, 2, 1, none)
|
||||
DEF( delete_var, 5, 0, 1, atom)
|
||||
|
||||
DEF( mul, 1, 2, 1, none)
|
||||
DEF( mul_float, 1, 2, 1, none)
|
||||
DEF( div, 1, 2, 1, none)
|
||||
DEF( div_float, 1, 2, 1, none)
|
||||
DEF( mod, 1, 2, 1, none)
|
||||
DEF( add, 1, 2, 1, none)
|
||||
DEF( add_float, 1, 2, 1, none)
|
||||
DEF( sub, 1, 2, 1, none)
|
||||
DEF( sub_float, 1, 2, 1, none)
|
||||
DEF( pow, 1, 2, 1, none)
|
||||
DEF( shl, 1, 2, 1, none)
|
||||
DEF( sar, 1, 2, 1, none)
|
||||
|
||||
1406
source/quickjs.c
1406
source/quickjs.c
File diff suppressed because it is too large
Load Diff
2
test.ce
2
test.ce
@@ -5,8 +5,6 @@ var time = use('time')
|
||||
var json = use('json')
|
||||
var blob = use('blob')
|
||||
|
||||
log.console("here")
|
||||
|
||||
if (!args) args = []
|
||||
|
||||
var target_pkg = null // null = current package
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// Tests all core features before implementing performance optimizations
|
||||
// (bytecode passes, ICs, quickening, tail call optimization)
|
||||
//
|
||||
|
||||
return {
|
||||
// ============================================================================
|
||||
// ARITHMETIC OPERATORS - Numbers
|
||||
@@ -146,26 +145,6 @@ return {
|
||||
if (!caught) throw "string + boolean should throw"
|
||||
},
|
||||
|
||||
test_null_plus_string_throws: function() {
|
||||
var caught = false
|
||||
try {
|
||||
var x = null + "hello"
|
||||
} catch (e) {
|
||||
caught = true
|
||||
}
|
||||
if (!caught) throw "null + string should throw"
|
||||
},
|
||||
|
||||
test_string_plus_null_throws: function() {
|
||||
var caught = false
|
||||
try {
|
||||
var x = "hello" + null
|
||||
} catch (e) {
|
||||
caught = true
|
||||
}
|
||||
if (!caught) throw "string + null should throw"
|
||||
},
|
||||
|
||||
// ============================================================================
|
||||
// COMPARISON OPERATORS
|
||||
// ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user