Skip to content

Commit e56aee3

Browse files
committed
Keep direct eval locals off realm cells
1 parent e73773d commit e56aee3

5 files changed

Lines changed: 114 additions & 1 deletion

File tree

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,10 @@ impl<'a> Vm<'a> {
329329
if !bytecode.has_direct_local_upvalue_routes() && !env.has_module_imports() {
330330
return Vec::new();
331331
}
332+
let direct_eval_frame = matches!(
333+
env.get_local(crate::DIRECT_EVAL_BINDING),
334+
Some(Value::Boolean(true))
335+
);
332336
let mut next_received = 0;
333337
bytecode
334338
.locals
@@ -341,6 +345,10 @@ impl<'a> Vm<'a> {
341345
return Some(upvalue);
342346
}
343347
if local.sloppy_global_fallback {
348+
if direct_eval_frame && let Some(upvalue) = env.local_binding_cell(&local.name)
349+
{
350+
return Some(upvalue);
351+
}
344352
return env.realm_binding_cell(&local.name);
345353
}
346354
if local.is_received_upvalue() {

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,17 @@ impl Vm<'_> {
264264
}
265265
continue;
266266
}
267+
// A direct-eval program is compiled as a global-scope bytecode,
268+
// but its imported `var` slots can still name the caller's local
269+
// binding. Preserve that binding identity before considering the
270+
// same-named realm cell.
271+
if direct_eval_frame
272+
&& local.hoisted
273+
&& let Some(upvalue) = env.local_binding_cell(&local.name)
274+
{
275+
local_upvalues[slot] = Some(upvalue);
276+
continue;
277+
}
267278
if bytecode.global_scope
268279
&& local.hoisted
269280
&& let Some(upvalue) = env.realm_binding_cell(&local.name)
@@ -272,7 +283,12 @@ impl Vm<'_> {
272283
continue;
273284
}
274285
if local.sloppy_global_fallback {
275-
local_upvalues[slot] = env.realm_binding_cell(&local.name);
286+
local_upvalues[slot] = if direct_eval_frame {
287+
env.local_binding_cell(&local.name)
288+
.or_else(|| env.realm_binding_cell(&local.name))
289+
} else {
290+
env.realm_binding_cell(&local.name)
291+
};
276292
continue;
277293
}
278294
if local.is_received_upvalue() {

crates/qjs-runtime/src/function/env.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1186,6 +1186,17 @@ impl CallEnv {
11861186
self.frame_bindings.cell(name)
11871187
}
11881188

1189+
/// Returns the live cell for a binding supplied by the current caller or
1190+
/// dynamic environment, without falling through to the realm. Direct eval
1191+
/// uses this to keep a same-named function local distinct from a global.
1192+
pub(crate) fn local_binding_cell(&self, name: &str) -> Option<Upvalue> {
1193+
self.frame_bindings.cell(name).or_else(|| {
1194+
self.deopt_bindings
1195+
.as_ref()
1196+
.and_then(|bindings| bindings.cell(name))
1197+
})
1198+
}
1199+
11891200
pub(crate) fn set_local(&self, name: &str, value: Value) -> bool {
11901201
self.frame_bindings.set(name, value.clone())
11911202
|| self

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1288,6 +1288,34 @@ fn top_level_global_var_slot_tracks_realm_mutations() {
12881288
);
12891289
}
12901290

1291+
#[test]
1292+
fn direct_eval_keeps_shadowing_function_var_distinct_from_global_cell() {
1293+
assert_eq!(
1294+
eval(
1295+
"var shadowedEvalGlobal = 'global'; \
1296+
(function() { \
1297+
var shadowedEvalGlobal = 'local'; \
1298+
eval('shadowedEvalGlobal = 1;'); \
1299+
if (shadowedEvalGlobal !== 1) throw new Error('direct eval local'); \
1300+
})(); \
1301+
shadowedEvalGlobal;"
1302+
),
1303+
Ok(Value::String("global".to_owned().into()))
1304+
);
1305+
assert_eq!(
1306+
eval(
1307+
"var shadowedEvalGlobal = 'global'; \
1308+
(function() { \
1309+
var shadowedEvalGlobal = 'local'; \
1310+
eval(...['shadowedEvalGlobal = 2;']); \
1311+
if (shadowedEvalGlobal !== 2) throw new Error('spread eval local'); \
1312+
})(); \
1313+
shadowedEvalGlobal;"
1314+
),
1315+
Ok(Value::String("global".to_owned().into()))
1316+
);
1317+
}
1318+
12911319
#[test]
12921320
fn test262_build_string_host_helper_matches_regexp_utils_shape() {
12931321
assert_eq!(

tasks/T018-broad-performance.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2668,6 +2668,56 @@ SHA-256: `fc5533c3541b3fdc108c85e3744a2cd90b976e83bc19ef8e09188e3158dbfc33`.
26682668
This unit improves several structurally different external programs while
26692669
keeping the internal family guard neutral; B4 and B5 remain active.
26702670

2671+
The exact-SHA audit for
2672+
`e73773d4481310490b9f6d12e39bfa9fa759c2fe` deliberately separates workflow
2673+
success from goal success. CI run `29620670097` passed all three jobs, and
2674+
Performance Preview run `29620670106` completed all 225/225 broad measurements
2675+
and all 75 linearity pairs. On the hosted runner, candidate/base was
2676+
0.991388x overall. The binding family improved to 0.969603x and the top-level
2677+
function-call case to 0.800568x, while the aggregate remained below the
2678+
campaign's practical effect threshold and several unrelated cases varied in
2679+
both directions. Candidate/QuickJS-NG was 0.368849x overall, but allocation
2680+
still failed B4 at 2.168137x; the other family ratios were 0.590612x array,
2681+
0.169022x binding, 0.100850x builtin, 0.610915x call, 0.246021x control,
2682+
0.144766x property, and 0.527411x string. Hosted broad raw/report SHA-256 are
2683+
`0c22db41a60128cda6b315342bb2eac6bf8715d54442fde9d7d5b64926903fe7`
2684+
and `e356af5fa81c678438bbba5fe895fe239fb5de36a00408147fa825671e67ec94`.
2685+
2686+
The same artifact is the authoritative warning against treating an internal
2687+
win as general engine speed. It retained 5/5 JetStream, 8/14 Kraken, and 23/26
2688+
SunSpider comparable cases, but qjs-rust still won none. Suite diagnostic
2689+
ratios were 10.710538x, 7.155944x, and 11.051522x QuickJS-NG: effectively no
2690+
suite-level closure of the external deficit. Some individual ratios improved,
2691+
including `string-validate-input` from 52.765730x to 36.587812x and the
2692+
class-field raytrace from 14.608166x to 13.369248x, while the targeted
2693+
`bitops-bitwise-and` ratio moved from 20.896859x to 23.019352x on the new
2694+
hosted run. Cross-run absolute durations moved for both engines and are not a
2695+
fixed-hardware comparison, so those mixed ratios outrank the favorable local
2696+
probes. External raw/report/manifest SHA-256 are
2697+
`65ad40f0cbc57fe71bef0fcad441428fa89e452782a683f19d68ab329eff24d6`,
2698+
`2fe13bab16b5600e7093d4829aeb2698bcafdf4a91cb8d78c186b08b5a33c965`,
2699+
and `fbcd37039908f72342effd1c2d9d3b12156f180bec745e95ff9a8bae2d56a93a`.
2700+
B4 and B5 remain active, and the next optimization must again target a general
2701+
external-profiled mechanism rather than an internal workload shape.
2702+
2703+
Coverage run `29620811076` also exposed why a green workflow is not sufficient
2704+
evidence. It completed successfully, but Rust moved from 42,671 pass / 1 fail
2705+
to 42,668 pass / 4 fail, creating three new actionable gaps in the direct-eval
2706+
spread family. The global-read change made a pre-existing bad cell route
2707+
observable: an unresolved assignment in direct eval could attach a
2708+
same-named function local to the realm cell, so the later top-level indexed
2709+
read observed the local assignment. The immediate correctness follow-up now
2710+
resolves a direct-eval caller or deoptimized binding cell before considering
2711+
the realm cell, in both regular and direct VM initialization, and covers plain
2712+
and spread eval with a shadowing function `var`. All four exact upstream
2713+
`eval-spread*` cases, 114 focused eval/call cases, 1,389 runtime tests, and the
2714+
full 5,139-case curated subset pass locally. The regressed coverage artifact's
2715+
burndown/comparison SHA-256 are
2716+
`8ef8c44a2ea9d4b676f5c0797c76350504f4ace9efc6829a6b7ac7a04548aaaa`
2717+
and `9961943568f0540bda862cf368c7175e105bf7ac4bd811dc955c5fc49186a696`;
2718+
the next exact-SHA coverage artifact must restore the former one-gap baseline
2719+
before this optimization unit is accepted.
2720+
26712721
## Historical Broad V1 Baseline
26722722

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

0 commit comments

Comments
 (0)