Noticed this pattern when reading some minified JS while debugging a
seemingly unrelated problem and immediately got suspicious because of my
earlier, similar fixes.
x >> 0 is a common JS idiom equivalent to ToInt32(x). We already had
this optimization for x | 0, now do it for right shift by zero as well.
This allows the asmint handler for ToInt32 to run instead of the more
expensive RightShift handler, which wastes time loading and checking the
rhs operand and performing a shift by zero.
Now that the C++ bytecode pipeline has been removed, we no longer
need to match its register allocation or block layout. This removes:
- All manual drop() calls that existed solely to match C++ register
lifetimes, replaced with scope blocks to naturally limit register
lifetimes without increasing register pressure.
- The unnecessary saved_property copy in update expressions. The
property register is now used directly since emit_update_op
doesn't evaluate user expressions that could mutate it. The copy
is retained in compound/logical assignments where the RHS can
mutate the property variable (e.g. a[i] |= a[++i]).
- All "matching C++", "Match C++", etc. comments throughout
codegen.rs and generator.rs that referenced the removed pipeline.
Replace the BytecodeFactory header with cbindgen.
This will help ensure that types and enums and constants are kept in
sync between the C++ and Rust code. It's also a step in exporting more
Rust enums directly rather than relying on magic constants for
switch statements.
The FFI functions are now all placed in the JS::FFI namespace, which
is the cause for all the churn in the scripting parts of LibJS and
LibWeb.
This error was found by asking an LLM to generate additional, related
test cases for the bug affecting https://volkswagen.de fixed in an
earlier commit.
An unconditional call to `copy_if_needed_to_preserve_evaluation_order`
in this place was showing up quiet significantly in the JS benchmarks.
To avoid the regression, there is now a small heuristic that avoids the
unnecessary Mov instruction in the vast majority of cases. This is
likely not the best way to deal with this. But the changes in the
current patch set are focussed on correctness, not performance. So I
opted for a localized, minimal-impact solution to the performance
regression.
This error was found by asking an LLM to generate additional, related
test cases for the bug affecting https://volkswagen.de fixed in an
earlier commit.
This error was found by asking an LLM to generate additional, related
test cases for the bug affecting https://volkswagen.de fixed in an
earlier commit.
Instead of storing a u32 index into a cache vector and looking up the
cache at runtime through a chain of dependent loads (load Executable*,
load vector data pointer, multiply index, add), store the actual cache
pointer as a u64 directly in the instruction stream.
A fixup pass (Executable::fixup_cache_pointers()) runs after Executable
construction in both the Rust and C++ pipelines, walking the bytecode
and replacing each index with the corresponding pointer.
The cache pointer type is encoded in Bytecode.def (e.g.
PropertyLookupCache*, GlobalVariableCache*) so the fixup switch is
auto-generated by the Python Op code generator, making it impossible
to forget updating the fixup when adding new cached instructions.
This eliminates 3-4 dependent loads on every inline cache access in
both the C++ interpreter and the assembly interpreter.
The Rust bytecode codegen was missing a TDZ check before assigning to
local let/const variables in simple assignment expressions (a = expr).
The C++ pipeline correctly emits ThrowIfTDZ before the store to ensure
temporal dead zone semantics are enforced.
Add an emit_tdz_check_if_needed helper matching the C++ equivalent,
and call it in the simple assignment path.
Replace 20 separate Put instructions (5 PutKinds x 4 forms) with
4 unified instructions (PutById, PutByIdWithThis, PutByValue,
PutByValueWithThis), each carrying a PutKind field at runtime instead
of being a separate opcode.
This reduces the number of handler entry points in the dispatch loop
and eliminates template instantiations of put_by_property_key and
put_by_value that were being duplicated 5x each when inlined by LTO.
Blocks containing non-local using declarations need a lexical
environment, just like let/const declarations. Add the missing
UsingDeclaration case to match C++ behavior.
Emit ResolveThisBinding before ResolveSuperBase in both
emit_evaluate_member_reference and emit_store_to_reference, matching
the C++ pipeline's evaluation order for super property references.
Also restructure emit_evaluate_member_reference to move non-super base
evaluation into the else branch, since the super path now handles
base evaluation differently (explicit ResolveSuperBase instead of
going through generate_expression on Super).
Keep the arg_holders vector alive through the spread arguments loop,
matching the C++ pipeline where the args Vector keeps registers held
through the loop. This ensures consistent register allocation.
Move the destination register allocation after RHS evaluation in
private identifier logical assignment (&&=, ||=, ??=), matching the
C++ pipeline's register allocation order.
When a computed member expression uses a constant string (e.g.
super["minutes"] or obj["key"]), optimize it to use the MemberId or
SuperMemberId reference form instead of the value-based form, matching
the C++ pipeline optimization.
Named class expressions don't use pending_lhs_name, but we must still
clear it to prevent it from leaking through to nested anonymous
functions inside the class body.
For computed class fields, field_name is empty and the name is set at
runtime. Avoid setting pending_lhs_name in that case, which prevents
the name from leaking into computed field initializers.
The caller is responsible for emitting ThrowIfTDZ before calling
emit_set_variable(), matching the C++ pipeline behavior. Remove the
redundant TDZ checks from both the const and non-const local paths.
Match the C++ pipeline's block creation order by creating end_block
and update_block before evaluating the for-of RHS expression. This
ensures loop structure blocks get lower block numbers than blocks
created during RHS evaluation (e.g. from conditional expressions),
producing the same block layout as C++.
Move the self-move detection before the TDZ check in
emit_set_variable, matching C++ which returns early for self-moves
without emitting anything. A self-move only happens in compound and
logical assignments where the LHS is a local variable, and the LHS
was already read with a TDZ check, making the write-back's TDZ check
redundant.
When the LHS of an assignment is a destructuring pattern, don't pass
preferred_dst when generating the RHS expression. This matches the C++
pipeline which always allocates a fresh register for the RHS value.
The difference was visible when a destructuring assignment appeared
inside a logical AND expression: the C++ pipeline would allocate the
RHS into a fresh register and then copy it to the AND result register,
while the Rust pipeline would evaluate the RHS directly into the AND
result register, omitting the copy.
When a for-of loop creates a per-iteration lexical environment for
let/const declarations, push a LeaveLexicalEnvironment boundary so
that continue/break/return properly restores the lexical environment.
The for-in codegen already did this but for-of was missing it.
When a switch statement creates a lexical environment for block-scoped
declarations (let/const), push a LeaveLexicalEnvironment boundary so
that perform_needed_unwinds correctly emits SetLexicalEnvironment
before Return/Throw instructions inside the switch body.
Add PrivateIdentifier handling in generate_update_expression so that
postfix/prefix increment/decrement on private members (e.g. this.#c++)
correctly emits GetPrivateById, PostfixIncrement, and PutPrivateById.
Previously this fell through to an empty fallback that returned an
uninitialized register.
Add missing perform_needed_unwinds() calls before Throw instructions
in four places:
- Await continuation throw path
- Yield* throw_value_block
- Yield* iterator missing throw method
- Invalid left-hand side in assignment helper
This matches the C++ pipeline which calls perform_needed_unwinds<Throw>
before every Throw to restore lexical environments when throwing out of
scopes like with statements.
Pass the fully computed var_environment_bindings_count from the SFD
metadata to the codegen, instead of using the raw
non_local_var_count_for_parameter_expressions. The full count includes
additional bindings from Annex B function hoisting and strict-mode
lexical declarations that share the var environment.