Skip to content

Commit 562c3ec

Browse files
committed
Avoid caller snapshots for native calls
1 parent 212f140 commit 562c3ec

3 files changed

Lines changed: 79 additions & 1 deletion

File tree

crates/qjs-runtime/src/bytecode/vm_call.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,19 @@ impl Vm<'_> {
119119
let effective_direct_eval = direct_eval
120120
&& matches!(&callee, Value::Function(function) if function.native == Some(NativeFunction::Eval));
121121
let in_parameter_scope = effective_direct_eval && self.in_parameter_prologue();
122-
let mut env = self.call_env(&callee);
122+
// Native functions do not inherit their caller's lexical environment.
123+
// Any user callbacks they invoke already carry their own upvalue cells,
124+
// while realm writes are shared through the realm itself. Only a direct
125+
// eval call needs the active frame's dynamic-name view.
126+
let frame_independent_native = !effective_direct_eval
127+
&& matches!(&callee, Value::Function(function) if function.native.is_some());
128+
let mut env = if frame_independent_native {
129+
super::vm::VmCallEnv {
130+
env: self.realm_env(),
131+
}
132+
} else {
133+
self.call_env(&callee)
134+
};
123135
if effective_direct_eval {
124136
env.env
125137
.insert(crate::DIRECT_EVAL_BINDING.to_owned(), Value::Boolean(true));

crates/qjs-runtime/src/tests/global.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,33 @@ fn eval_line_comment_with_line_terminator_still_evaluates_tail() {
102102
);
103103
}
104104

105+
#[test]
106+
fn native_calls_use_callback_cells_without_losing_direct_eval_scope() {
107+
assert_eq!(
108+
eval(
109+
"function updateThroughNative() { \
110+
let captured = 1; \
111+
Math.max({ valueOf() { captured = 3; return 2; } }, 1); \
112+
[0].forEach(function () { captured = 5; }); \
113+
return captured; \
114+
} \
115+
updateThroughNative();"
116+
),
117+
Ok(Value::Number(5.0))
118+
);
119+
assert_eq!(
120+
eval(
121+
"function updateThroughDirectEval() { \
122+
let local = 1; \
123+
eval('local = 7'); \
124+
return local; \
125+
} \
126+
updateThroughDirectEval();"
127+
),
128+
Ok(Value::Number(7.0))
129+
);
130+
}
131+
105132
#[test]
106133
fn global_nan_is_non_writable() {
107134
// Sloppy mode: assignment silently fails, NaN remains a number.

tasks/T018-broad-performance.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3019,6 +3019,45 @@ All three prototypes were discarded without a runtime commit. This is an
30193019
intentional application of the external anti-overfitting contract: internal
30203020
benchmark speedups alone are not T018 progress.
30213021

3022+
The fifty-second v2 unit instead comes directly from independent external
3023+
profiles. macOS sampling of Kraken `ai-astar` and `audio-oscillator` placed
3024+
`apply_env` at the top of both stacks (44 and 57 samples respectively), with
3025+
the time below it dominated by caller-local snapshots, string clones, and
3026+
`HashMap` insertion/hashing. Ordinary native functions do not dynamically
3027+
inherit their caller's lexical environment: user callbacks already carry
3028+
their exact closure/upvalue cells, and global state is shared by the realm.
3029+
The VM therefore gives frame-independent native calls a realm-only `CallEnv`
3030+
instead of reconstructing every active caller slot and name map. Direct eval
3031+
is explicitly excluded and retains the active dynamic-name view. The focused
3032+
correctness test covers both coercion and array callbacks mutating captured
3033+
locals, plus direct eval mutating a caller local. No workload name, source
3034+
path, iteration count, checksum, or expected result appears in the runtime
3035+
implementation.
3036+
3037+
Against the exact unit-51 local base, the independent one-block external
3038+
inventory retained full 5/5 JetStream, 14/14 Kraken, and 26/26 SunSpider
3039+
coverage. Common-case candidate/base geometric means were **0.955718x**,
3040+
**0.886864x**, and **0.996554x** respectively. JetStream improved in all 5/5
3041+
cases; Kraken improved in 11/14, led by `audio-oscillator` at 0.299309x and
3042+
bounded by a 1.028269x worst observed ratio; SunSpider split 14/26 faster and
3043+
12/26 slower, with a 1.107301x worst single-block observation. Candidate/
3044+
QuickJS-NG remains far from B5 at 7.617x, 5.480x, and 10.169x, so this is a
3045+
general structural improvement rather than a target-completion claim.
3046+
External raw/report SHA-256 are
3047+
`4fa259b01a1642b0e60b35319bf5965970995a4ee17c10b1ee3f78924e203d9a`
3048+
and `0bd046453dee13903dde12663c35a0ee9ae2ed57fbbcd9ada8b127362c140fd4`.
3049+
3050+
The accompanying receipt-less one-block broad diagnostic retained all 25
3051+
cases. Its manual protocol-equivalent normalization measured candidate/base
3052+
at 1.005854x overall, with 11/25 cases faster and all individual ratios
3053+
between 0.990890x and 1.050665x. Candidate/QuickJS-NG was 0.186030x in this
3054+
diagnostic. The strict report tool correctly rejected the run as unverified
3055+
because development binaries had no build receipts; the complete raw JSONL
3056+
is retained only as regression evidence with SHA-256
3057+
`59244edb2b9eb5ddf39ab0ec0ad54bf7c5578dc060d7fd6c627fe540c4ff8236`.
3058+
Exact pushed performance and coverage artifacts remain required before this
3059+
unit is closed.
3060+
30223061
## Historical Broad V1 Baseline
30233062

30243063
The first complete baseline was recorded on 2026-07-15 at commit

0 commit comments

Comments
 (0)