10 Commits

Author SHA1 Message Date
John Alanbrook
7af8ffe4e0 attempt 2025-12-31 16:01:02 -06:00
John Alanbrook
8abee37622 persistent current frame 2025-12-31 14:28:43 -06:00
John Alanbrook
7c3cce1ce2 works 2025-12-31 12:45:32 -06:00
John Alanbrook
334f3a789b runs 2025-12-31 11:09:18 -06:00
John Alanbrook
e21cd4e70b tail call 2025-12-31 09:19:39 -06:00
John Alanbrook
41eb4bf6f7 bench 2025-12-30 22:56:31 -06:00
John Alanbrook
5f761cc7af wire callinternal to use trampoline 2025-12-30 16:58:06 -06:00
John Alanbrook
7ae5a0c06b null + anything = null 2025-12-30 14:40:54 -06:00
John Alanbrook
0664c11af6 set up quickening for adds 2025-12-30 00:39:43 -06:00
John Alanbrook
058ad89c96 op switch 2025-12-29 20:46:12 -06:00
7 changed files with 848 additions and 860 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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
// ============================================================================