Skip to content

Commit e73773d

Browse files
committed
Use realm cells for global reads
1 parent eaf64f2 commit e73773d

7 files changed

Lines changed: 196 additions & 27 deletions

File tree

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

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,12 @@ impl Compiler {
177177
else {
178178
return self.compile_member_compound_assign(target, op, value);
179179
};
180-
let slot = self.resolve_local_slot(name);
181-
let resolved_with_object_slot = self.resolve_with_identifier_target(name, slot);
180+
let store_slot = self.resolve_local_slot(name);
181+
let load_slot = self.resolve_identifier_load_slot(name);
182+
let resolved_with_object_slot = self.resolve_with_identifier_target(name, store_slot);
182183
match op {
183184
AssignmentOp::LogicalAndAssign => {
184-
self.emit_load_identifier(name, slot, resolved_with_object_slot);
185+
self.emit_load_identifier(name, load_slot, resolved_with_object_slot);
185186
let end_jump = self.emit(Op::JumpIfFalse(usize::MAX));
186187
self.emit(Op::Pop);
187188
// `f &&= <anon>` names the anonymous value after the target
@@ -192,12 +193,12 @@ impl Compiler {
192193
self.compile_named_expr(value, name)?;
193194
}
194195
self.emit(Op::Dup);
195-
self.emit_store_identifier(name, slot, resolved_with_object_slot);
196+
self.emit_store_identifier(name, store_slot, resolved_with_object_slot);
196197
let end = self.code.len();
197198
self.patch_jump(end_jump, end);
198199
}
199200
AssignmentOp::LogicalOrAssign => {
200-
self.emit_load_identifier(name, slot, resolved_with_object_slot);
201+
self.emit_load_identifier(name, load_slot, resolved_with_object_slot);
201202
let end_jump = self.emit(Op::JumpIfTrue(usize::MAX));
202203
self.emit(Op::Pop);
203204
if *parenthesized {
@@ -206,12 +207,12 @@ impl Compiler {
206207
self.compile_named_expr(value, name)?;
207208
}
208209
self.emit(Op::Dup);
209-
self.emit_store_identifier(name, slot, resolved_with_object_slot);
210+
self.emit_store_identifier(name, store_slot, resolved_with_object_slot);
210211
let end = self.code.len();
211212
self.patch_jump(end_jump, end);
212213
}
213214
AssignmentOp::NullishAssign => {
214-
self.emit_load_identifier(name, slot, resolved_with_object_slot);
215+
self.emit_load_identifier(name, load_slot, resolved_with_object_slot);
215216
let end_jump = self.emit(Op::JumpIfNotNullish(usize::MAX));
216217
self.emit(Op::Pop);
217218
if *parenthesized {
@@ -220,7 +221,7 @@ impl Compiler {
220221
self.compile_named_expr(value, name)?;
221222
}
222223
self.emit(Op::Dup);
223-
self.emit_store_identifier(name, slot, resolved_with_object_slot);
224+
self.emit_store_identifier(name, store_slot, resolved_with_object_slot);
224225
let end = self.code.len();
225226
self.patch_jump(end_jump, end);
226227
}
@@ -229,7 +230,7 @@ impl Compiler {
229230
&& !self.inside_with()
230231
&& let Expr::Literal(Literal::String { value, .. }) = value
231232
{
232-
if let Some(slot) = slot {
233+
if let Some(slot) = store_slot {
233234
self.emit(Op::AppendStringLiteralLocal {
234235
slot,
235236
value: value.clone(),
@@ -243,11 +244,11 @@ impl Compiler {
243244
}
244245
return Ok(());
245246
}
246-
self.emit_load_identifier(name, slot, resolved_with_object_slot);
247+
self.emit_load_identifier(name, load_slot, resolved_with_object_slot);
247248
self.compile_expr(value)?;
248249
self.emit(Op::Binary(assignment_binary_op(op)?));
249250
self.emit(Op::Dup);
250-
self.emit_store_identifier(name, slot, resolved_with_object_slot);
251+
self.emit_store_identifier(name, store_slot, resolved_with_object_slot);
251252
}
252253
}
253254
Ok(())
@@ -262,19 +263,20 @@ impl Compiler {
262263
let AssignmentTarget::Identifier { name, .. } = target else {
263264
return self.compile_member_update(target, op, prefix);
264265
};
265-
let slot = self.resolve_local_slot(name);
266-
let resolved_with_object_slot = self.resolve_with_identifier_target(name, slot);
267-
self.emit_load_identifier(name, slot, resolved_with_object_slot);
266+
let store_slot = self.resolve_local_slot(name);
267+
let load_slot = self.resolve_identifier_load_slot(name);
268+
let resolved_with_object_slot = self.resolve_with_identifier_target(name, store_slot);
269+
self.emit_load_identifier(name, load_slot, resolved_with_object_slot);
268270
self.emit(Op::ToNumeric);
269271
if !prefix {
270272
self.emit(Op::Dup);
271273
}
272274
self.emit(Op::Update(op));
273275
if prefix {
274276
self.emit(Op::Dup);
275-
self.emit_store_identifier(name, slot, resolved_with_object_slot);
277+
self.emit_store_identifier(name, store_slot, resolved_with_object_slot);
276278
} else {
277-
self.emit_store_identifier(name, slot, resolved_with_object_slot);
279+
self.emit_store_identifier(name, store_slot, resolved_with_object_slot);
278280
}
279281
Ok(())
280282
}
@@ -742,14 +744,15 @@ impl Compiler {
742744
pub(super) fn compile_typeof(&mut self, argument: &Expr) -> Result<(), RuntimeError> {
743745
match argument {
744746
Expr::Identifier { name, .. } => {
745-
let slot = self.resolve_local_slot(name);
746-
if self.identifier_needs_with_resolution(slot) {
747+
let reference_slot = self.resolve_local_slot(name);
748+
let load_slot = self.resolve_identifier_load_slot(name);
749+
if self.identifier_needs_with_resolution(reference_slot) {
747750
self.emit(Op::TypeofIdentWith {
748751
name: name.clone(),
749-
slot,
752+
slot: load_slot,
750753
});
751754
return Ok(());
752-
} else if let Some(slot) = slot {
755+
} else if let Some(slot) = load_slot {
753756
self.emit(Op::LoadLocal(slot));
754757
} else {
755758
self.emit(Op::TypeofGlobal(name.clone()));

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -413,14 +413,15 @@ impl Compiler {
413413
match expr {
414414
Expr::Literal(literal) => self.compile_literal(literal),
415415
Expr::Identifier { name, .. } => {
416-
let slot = self.resolve_local_slot(name);
417-
if self.identifier_needs_with_resolution(slot) {
416+
let reference_slot = self.resolve_local_slot(name);
417+
let load_slot = self.resolve_identifier_load_slot(name);
418+
if self.identifier_needs_with_resolution(reference_slot) {
418419
self.emit(Op::LoadIdentWith {
419420
name: name.clone(),
420-
slot,
421+
slot: load_slot,
421422
is_strict: self.strict,
422423
});
423-
} else if let Some(slot) = slot {
424+
} else if let Some(slot) = load_slot {
424425
self.emit(Op::LoadLocal(slot));
425426
} else {
426427
self.emit(Op::LoadGlobal(name.clone()));

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,19 @@ impl Compiler {
9797
.filter(|slot| !self.locals[*slot].sloppy_global_fallback)
9898
}
9999

100+
/// Resolves an identifier read to an indexed binding when the binding is
101+
/// known at compile time. Global `var`/function writes deliberately keep
102+
/// using the global operations so descriptor and strictness checks remain
103+
/// centralized, while reads can use the shared realm cell installed for
104+
/// their otherwise vestigial local slot.
105+
pub(super) fn resolve_identifier_load_slot(&self, name: &str) -> Option<usize> {
106+
self.resolve_local_slot(name).or_else(|| {
107+
(self.global_scope && self.global_hoisted.contains(name))
108+
.then(|| self.local_slots.get(name).copied())
109+
.flatten()
110+
})
111+
}
112+
100113
pub(super) fn assignment_slot(&mut self, name: &str) -> usize {
101114
if let Some(slot) = self.resolve_local_slot(name) {
102115
return slot;
@@ -587,6 +600,37 @@ mod tests {
587600
);
588601
}
589602

603+
#[test]
604+
fn global_var_reads_use_the_shared_slot_but_writes_stay_global() {
605+
let script =
606+
qjs_parser::parse_script("var value = 1; value; value *= 2; value++; typeof value;")
607+
.expect("source should parse");
608+
let bytecode =
609+
super::super::compiler::compile_script(&script).expect("source should compile");
610+
let value_slot = bytecode
611+
.local_slot("value")
612+
.expect("global var should retain an indexed slot");
613+
614+
assert!(
615+
bytecode
616+
.code
617+
.iter()
618+
.any(|op| matches!(op, Op::LoadLocal(slot) if *slot == value_slot))
619+
);
620+
assert!(
621+
bytecode
622+
.code
623+
.iter()
624+
.any(|op| matches!(op, Op::StoreGlobalSloppy(name) if name == "value"))
625+
);
626+
assert!(
627+
!bytecode
628+
.code
629+
.iter()
630+
.any(|op| matches!(op, Op::LoadGlobal(name) if name == "value"))
631+
);
632+
}
633+
590634
#[test]
591635
fn direct_eval_var_capture_uses_the_name_to_cell_deopt_path() {
592636
let script = qjs_parser::parse_script("var value = 1; function read() { return value; }")

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,13 @@ impl Vm<'_> {
264264
}
265265
continue;
266266
}
267+
if bytecode.global_scope
268+
&& local.hoisted
269+
&& let Some(upvalue) = env.realm_binding_cell(&local.name)
270+
{
271+
local_upvalues[slot] = Some(upvalue);
272+
continue;
273+
}
267274
if local.sloppy_global_fallback {
268275
local_upvalues[slot] = env.realm_binding_cell(&local.name);
269276
continue;

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,11 @@ impl Vm<'_> {
102102
result.clone()
103103
});
104104
if self.realm.borrow().contains_key(&local_meta.name) {
105-
self.realm
106-
.borrow_mut()
107-
.insert(local_meta.name, result.clone());
105+
// A top-level reader may already hold the realm binding's
106+
// shared cell even when this older from-env frame still uses a
107+
// compatibility slot. Route the mirror through CallEnv so the
108+
// cell cannot retain the pre-append string.
109+
self.env.insert_realm(local_meta.name, result.clone());
108110
}
109111
}
110112
Ok(result)
@@ -136,6 +138,9 @@ impl Vm<'_> {
136138
result.clone()
137139
});
138140
}
141+
// `Rc::make_mut` can detach the realm string from a cell's
142+
// earlier clone. Refresh that cell after the in-place append.
143+
self.env.insert_realm(name.to_owned(), result.clone());
139144
self.write_through_module_live_binding(name, result.clone());
140145
return Ok(result);
141146
}

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1255,6 +1255,39 @@ fn eval_script_runs_global_declaration_instantiation_checks() {
12551255
);
12561256
}
12571257

1258+
#[test]
1259+
fn top_level_global_var_slot_tracks_realm_mutations() {
1260+
assert_eq!(
1261+
eval("var slotBackedGlobal = 1; eval('slotBackedGlobal = 3'); slotBackedGlobal;"),
1262+
Ok(Value::Number(3.0))
1263+
);
1264+
assert_eq!(
1265+
eval(
1266+
"var slotBackedGlobal = 1; \
1267+
globalThis.slotBackedGlobal = 4; \
1268+
slotBackedGlobal;"
1269+
),
1270+
Ok(Value::Number(4.0))
1271+
);
1272+
assert_eq!(
1273+
eval(
1274+
"var slotBackedGlobal = 1; \
1275+
Object.defineProperty(globalThis, 'slotBackedGlobal', { value: 5 }); \
1276+
slotBackedGlobal;"
1277+
),
1278+
Ok(Value::Number(5.0))
1279+
);
1280+
assert_eq!(
1281+
eval(
1282+
"var slotBackedGlobal = ''; \
1283+
function appendToGlobal() { slotBackedGlobal += 'x'; } \
1284+
appendToGlobal(); \
1285+
slotBackedGlobal;"
1286+
),
1287+
Ok(Value::String("x".to_owned().into()))
1288+
);
1289+
}
1290+
12581291
#[test]
12591292
fn test262_build_string_host_helper_matches_regexp_utils_shape() {
12601293
assert_eq!(

tasks/T018-broad-performance.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2592,6 +2592,82 @@ SHA-256: `d1db2d4998b95ad988670acc4b31da7219d28f300824c5f4055629ce7e36b962`.
25922592
This unit improves a class-heavy external program while preserving both other
25932593
external suites and the internal regression guard; B4 and B5 remain active.
25942594

2595+
The exact-SHA hosted workflows for
2596+
`eaf64f20f4790681a38dfbc382696eb52330ed2e` then completed successfully. CI
2597+
run `29617126163` passed comparison, Test262 subset, and full-check jobs.
2598+
Coverage run `29617310831` retained 42,671 Rust passes, one failure, zero
2599+
timeouts, and one actionable gap; its burndown and comparison-case SHA-256
2600+
values are
2601+
`0a9594104963a6815964c986c5779d09088d4a88761688f9fa9c476ef762a900`
2602+
and `3ba9a1a20f4cd430f9f33471ca97652a67a12602740b26453f2e06fd63a58329`.
2603+
2604+
Performance Preview run `29617126173` retained 25/25 broad cases, all 225/225
2605+
eligible measurements, and all 75 engine/case linearity pairs. The hosted
2606+
runner's 1.007516x candidate/base result was inconclusive; candidate/QuickJS-NG
2607+
was 0.366549x overall, but allocation still failed B4 at 2.822703x. The other
2608+
family ratios were 0.615636x array, 0.173452x binding, 0.087795x builtin,
2609+
0.528594x call, 0.188521x control, 0.170350x property, and 0.521216x string.
2610+
Hosted broad raw/report SHA-256 are
2611+
`48ecceb438c6359b17346f048e99305ebde22d46a4c53a1c361d6644e33ec467`
2612+
and `c76237ea82ad301bbb51c99929da9ec91accaade2238e3e2d2c8a53e7c2f15a4`.
2613+
2614+
The same exact-SHA artifact retained comparable coverage at 5/5 JetStream,
2615+
7/14 Kraken, and 23/26 SunSpider, but qjs-rust lost every comparable case.
2616+
Suite diagnostic ratios remained far from B5 at 10.625742x, 6.170478x, and
2617+
11.060351x QuickJS-NG. Hosted external raw/report SHA-256 are
2618+
`33e631640f11669044b630bc663e7d7849ef60a85f6077cabf853d020cc1c848`
2619+
and `59bd814501b7b4f0cf40c4be3f96b291bd44f075b84484a40026658861421f6f`.
2620+
The target JetStream class-field raytrace duration nevertheless fell from
2621+
10,044,283,046 ns at the exact base artifact to 8,697,197,004 ns here, a
2622+
0.865886x cross-artifact ratio, while its candidate/QuickJS-NG ratio improved
2623+
from 16.940592x to 14.608166x. This confirms that the mechanism generalized to
2624+
the independent external workload; it does not mask the much larger remaining
2625+
engine-wide deficit. The external results, rather than internal-only wins,
2626+
therefore continue to drive the next structural bottlenecks. B4 and B5 remain
2627+
active.
2628+
2629+
The forty-seventh v2 unit follows a fresh external bitops profile into the
2630+
general global-binding read path. A declared top-level `var` or function
2631+
previously re-hashed its source name and consulted the realm/global object on
2632+
every read even though declaration instantiation already provided a stable
2633+
shared realm cell. Such reads now use the declaration's indexed slot backed by
2634+
that cell. Writes remain on the complete global path so writable descriptors,
2635+
strict/sloppy behavior, `with`, direct eval, global-object mutations, and
2636+
accessor/deletion deoptimization keep their existing checks. The same change
2637+
made every top-level declaration install its realm cell at frame entry. It also
2638+
closed two older string-append paths that mutated the realm value table
2639+
directly: both now refresh an existing shared cell after copy-on-write. Focused
2640+
tests prove the read opcode/write opcode split and visibility after direct
2641+
eval, global-object assignment, `Object.defineProperty`, and a nested string
2642+
append. No workload name, source path, iteration count, checksum, or expected
2643+
benchmark value appears in the implementation.
2644+
2645+
Against base binary SHA-256
2646+
`d4d4930db2bb3e8939ae9228ea979dbe25c34d2bf17ddd5aad136d7b6c6d957d`,
2647+
candidate binary SHA-256
2648+
`989f3cd0c827db801b23ebf95ef8eea326c144a34e05313c1327ef2b24132926`
2649+
improved alternating outer-wall probes of six independent SunSpider sources.
2650+
Candidate/base medians were 0.804612x for `bitops-bitwise-and`, 0.979102x for
2651+
`bitops-nsieve-bits`, 0.936710x for `3d-morph`, 0.963054x for
2652+
`access-nsieve`, 0.994970x for `math-partial-sums`, and 0.994814x for
2653+
`string-validate-input`. The 9-run probes were independently repeated with 21
2654+
runs for the three closest or initially variable cases. These focused probes
2655+
use the pinned upstream source and the external preview's raw CLI mode, but are
2656+
development evidence rather than a complete suite claim; the exact-SHA hosted
2657+
preview remains authoritative.
2658+
2659+
The complete one-block three-role broad development diagnostic produced 1,473
2660+
OK samples: 498 calibration, 225 startup, 75 warmup, 75/75 eligible
2661+
measurements, and 600 linearity samples. Candidate/base was 0.987170x overall;
2662+
family ratios ranged from 0.955193x call through 1.001039x property, and the
2663+
worst individual candidate/base ratio was 1.011043x. Candidate/QuickJS-NG was
2664+
0.192872x overall, but allocation still failed B4 at 1.136663x. The strict
2665+
analyzer correctly rejected the receipt-less dirty development binaries, so
2666+
this is regression evidence rather than a fixed-hardware claim. Broad raw
2667+
SHA-256: `fc5533c3541b3fdc108c85e3744a2cd90b976e83bc19ef8e09188e3158dbfc33`.
2668+
This unit improves several structurally different external programs while
2669+
keeping the internal family guard neutral; B4 and B5 remain active.
2670+
25952671
## Historical Broad V1 Baseline
25962672

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

0 commit comments

Comments
 (0)