Replace the check_is_double pattern that loaded the full 64-bit
CANON_NAN_BITS constant (10-byte movabs on x86_64) and masked the
entire value, with a cheaper approach: extract the upper 16-bit tag
and check if (tag & NAN_BASE_TAG) == NAN_BASE_TAG.
This saves instructions at every double-check site. Additionally,
add a check_tag_is_double macro for call sites where the tag has
already been extracted into a register, avoiding redundant
extract_tag operations. This is used in 11 call sites across
coerce_to_doubles, strict_equality_core, numeric_compare, Div,
UnaryPlus, UnaryMinus, and ToInt32.
Replace the pattern of 64-bit arithmetic + sign-extend + compare
with dedicated 32-bit overflow instructions that use the hardware
overflow flag directly.
Before: add t3, t4 / unbox_int32 t5, t3 / branch_ne t3, t5, .overflow
After: add32_overflow t3, t4, .overflow
On x86_64 this compiles to `add r32, r32; jo label` (the 32-bit
register write implicitly zeros the upper 32 bits). On aarch64,
`adds w, w, w; b.vs label` for add/sub, `smull + sxtw + cmp + b.ne`
for multiply, and `negs + b.vs` for negate.
Nine call sites updated: Add, Sub, Mul, Increment, Decrement,
PostfixIncrement, PostfixDecrement, UnaryMinus, and CallBuiltin(abs).
Add a new interpreter that executes bytecode via generated assembly,
written in a custom DSL (asmint.asm) that AsmIntGen compiles to
native x86_64 or aarch64 code.
The interpreter keeps the bytecode program counter and register file
pointer in machine registers for fast access, dispatching opcodes
through a jump table. Hot paths (arithmetic, comparisons, property
access on simple objects) are handled entirely in assembly, with
cold/complex operations calling into C++ helper functions defined
in AsmInterpreter.cpp.
A small build-time tool (gen_asm_offsets) uses offsetof() to emit
struct field offsets as constants consumed by the DSL, ensuring the
assembly stays in sync with C++ struct layouts.
The interpreter is enabled by default on platforms that support it.
The C++ interpreter can be selected via LIBJS_USE_CPP_INTERPRETER=1.
Currently supported platforms:
- Linux/x86_64
- Linux/aarch64
- macOS/x86_64
- macOS/aarch64
Move Interpreter::get() and set() from the .cpp file into the header
as inline methods. Make handle_exception(), perform_call(),
perform_call_impl(), and the HandleExceptionResponse enum public so
they can be called by the upcoming assembly interpreter's C++ glue
code. Also add set_running_execution_context() for the same reason.
This path will replace manual execution-context stack resizing with
vm().pop_execution_context() in the inline unwind paths. Apply this
in both exception unwinding and inline return handling so frame
teardown consistently goes through the VM’s canonical pop logic,
reducing the risk of execution-context stack desynchronization.
Instead of recursing through 5 native stack frames per JS function
call (execute_call -> internal_call -> ordinary_call_evaluate_body ->
run_executable -> run_bytecode), handle Call and CallConstruct for
normal ECMAScript functions directly in the dispatch loop.
The fast path allocates the callee's execution context on the
InterpreterStack, copies arguments, sets up the environment, and
jumps to the callee's bytecode entry point. Return and End unwind
inline frames by restoring the caller's state. Exception unwinding
walks through inline frames to find handlers.
The fast path code is kept in NEVER_INLINE helper functions
(try_inline_call, try_inline_call_construct, pop_inline_frame) to
minimize register pressure in the dispatch loop. handle_exception
takes program_counter by value to avoid forcing it onto the stack.
Reloading of bytecode/program_counter after frame switches is done
inline at each call site via RELOAD_AND_GOTO_START to preserve a
single dispatch entry point for optimal indirect branch prediction.
Replace alloca-based execution context allocation with InterpreterStack
bump allocation across all call sites: bytecode call instructions,
AbstractOperations call/construct, script evaluation, module evaluation,
and LibWeb module script evaluation.
Also replace the native stack space check with an InterpreterStack
exhaustion check, and remove the now-unused alloca macros from
ExecutionContext.h.
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.
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.
When the left-hand side of an assignment, update, or for-in loop is
invalid (e.g. `foo() = "bar"`), the bytecode generator emits a Throw
instruction. Previously, it would also create a dead basic block after
the Throw, resulting in unreachable instructions in the output.
Fix this by returning early from the relevant codegen paths after
emitting the Throw, and by guarding for-in/for-of body generation
with an is_current_block_terminated() check.
Implement a complete Rust reimplementation of the LibJS frontend:
lexer, parser, AST, scope collector, and bytecode code generator.
The Rust pipeline is built via Corrosion (CMake-Cargo bridge) and
linked into LibJS as a static library. It is gated behind a build
flag (ENABLE_RUST, on by default except on Windows) and two runtime
environment variables:
- LIBJS_CPP: Use the C++ pipeline instead of Rust
- LIBJS_COMPARE_PIPELINES=1: Run both pipelines in lockstep,
aborting on any difference in AST or bytecode generated.
The C++ side communicates with Rust through a C FFI layer
(RustIntegration.cpp/h) that passes source text to Rust and receives
a populated Executable back via a BytecodeFactory interface.
Add the ability to dump AST and bytecode to a String instead of only
to stdout/stderr. This is done by adding an optional StringBuilder
output sink to ASTDumpState, and a new dump_to_string() method on
both ASTNode and Bytecode::Executable.
These will be used for comparing output between compilation pipelines.
The scope collector uses HashMaps for identifier groups and variables,
which means their iteration order is non-deterministic. This causes
local variable indices and function declaration instantiation (FDI)
bytecode to vary between runs.
Fix this by sorting identifier group keys alphabetically before
assigning local variable indices, and sorting vars_to_initialize by
name before emitting FDI bytecode.
Also make register allocation deterministic by always picking the
lowest-numbered free register instead of whichever one happens to be
at the end of the free list.
This is preparation for bringing in a new source->bytecode pipeline
written in Rust. Checking for regressions is significantly easier
if we can expect identical output from both pipelines.
For `export default (class Name { })`, two things were wrong:
The parser extracted the class expression's name as the export's
local binding name instead of `*default*`. Per the spec, this is
`export default AssignmentExpression ;` whose BoundNames is
`*default*`, not the class name.
The bytecode generator had a special case for ClassExpression that
skipped emitting InitializeLexicalBinding for named classes.
These two bugs compensated for each other (no crash, but wrong
behavior). Fix both: always use `*default*` as the local binding
name for expression exports, and always emit InitializeLexicalBinding
for the `*default*` binding.
When a statement in a switch case body doesn't produce a result (e.g.
a variable declaration), we were incorrectly resetting the completion
value to undefined. This caused the completion value of preceding
expression statements to be lost.
Per step 13 of ScriptEvaluation in the ECMA-262 spec, the script body
should only be evaluated if GlobalDeclarationInstantiation returned a
normal completion.
This can't currently be triggered since we always create fresh Script
objects, but if we ever start reusing cached executables across
evaluations, this would prevent a subtle bug where the script body
runs despite GDI failing.
When emitting block declaration instantiation, we were not calling
set_local_initialized() after writing block-scoped function
declarations to local variables via Mov. This caused unnecessary
ThrowIfTDZ checks to be emitted when those locals were later read.
Block-scoped function declarations are always initialized at block
entry (via NewFunction + Mov), so TDZ checks for them are redundant.
Move the duplicated ThrowIfTDZ emission logic from three places in
ASTCodegen.cpp into a single Generator::emit_tdz_check_if_needed()
helper. This handles both argument TDZ (which requires a Mov to
empty first) and lexically-declared variable TDZ uniformly.
This avoids emitting some unnecessary ThrowIfTDZ instructions.
The find_source_record lambda was doing a reverse linear scan through
the entire source map for every instruction emitted, resulting in
quadratic behavior. This was catastrophic for large scripts like
Octane/mandreel.js, where compile() dominated the profile at ~30s.
Since both source map entries and instruction iteration are ordered by
offset, replace the per-instruction scan with a forward cursor that
advances in lockstep with instruction emission.
The compile() function was adding source map entries for all
instructions in a block upfront, before processing assembly-time
optimizations (Jump-to-next-block elision, Jump-to-Return/End inlining,
JumpIf-to-JumpTrue/JumpFalse conversion). When a Jump was skipped,
its phantom source map entry remained at the offset where the next
block's first instruction would be placed, causing binary_search to
find the wrong source location for error messages.
Fix by building source map entries inline with instruction emission,
ensuring only actually-emitted instructions get entries. For blocks
with duplicate source map entries at the same offset (from rewind in
fuse_compare_and_jump), the last entry is used.
Add ThisExpression handling to the expression_identifier() helper used
for base_identifier in bytecode instructions. This makes PutById and
GetById emit base_identifier:this when the base is a this expression.
When MemberExpression::generate_bytecode calls emit_load_from_reference,
it only uses the loaded_value and discards the reference operands. For
computed member expressions (e.g. a[0]), this was generating an
unnecessary Mov to save the property register for potential store-back.
Add a ReferenceMode parameter to emit_load_from_reference. When LoadOnly
is passed, the computed property path skips the register save and Mov.
Per AssignmentRestElement and AssignmentElement in the specification,
the DestructuringAssignmentTarget reference must be evaluated before
iterating or stepping the iterator. We were doing it in the wrong
order, which caused observable differences when the target evaluation
has side effects, and could lead to infinite loops when the iterator
never completes.
Add Generator::emit_evaluate_reference() to evaluate a member
expression's base and property into ReferenceOperands without performing
a load or store, then use the pre-evaluated reference for the store
after iteration completes.
When a function has parameter expressions (e.g. destructured params with
defaults), CreateVariableEnvironment creates a separate variable
environment for function declarations and sets it as the current lexical
environment at runtime. However, the bytecode generator's
m_lexical_environment_register_stack was not updated to reflect this, so
subsequent CreateLexicalEnvironment ops would parent themselves to the
old (pre-variable-environment) lexical environment, skipping the
variable environment entirely.
This meant function declarations hoisted into the variable environment
were invisible to closures created in the function body.
Fix this by capturing the new lexical environment into a register after
CreateVariableEnvironment and pushing it onto the environment register
stack.
This fixes a problem where https://tumblr.com/ wouldn't load the feed.
AsyncIteratorClose is now fully inlined as bytecode in ASTCodegen.cpp,
using the Await bytecode op to yield naturally. The C++ implementation
used synchronous await() which spins the event loop, violating
assertions when execution contexts are on the stack.
The AsyncIteratorClose bytecode op calls async_iterator_close() which
uses synchronous await() internally. This spins the event loop while
execution contexts are on the stack, violating the microtask checkpoint
assertion in LibWeb.
Replace AsyncIteratorClose op emissions in for-await-of close handlers
with inline bytecode that uses the proper Await op, allowing the async
function to yield and resume naturally through the event loop.
For the non-throw path (break/return/continue-to-outer): emit
GetMethod, Call, Await, and ThrowIfNotObject inline.
For the throw path: wrap the close steps in an exception handler so
that any error from GetMethod/Call/Await is discarded and the original
exception is rethrown, per spec step 5.
The else branch already throws ReferenceError and switches to a dead
basic block, so the emit_todo() in the PutValue section is unreachable.
Return early after the throw and replace emit_todo() with
VERIFY_NOT_REACHED().
CallExpression is accepted as an assignment target for web compatibility
(Annex B), but must throw ReferenceError at runtime. We were incorrectly
throwing TypeError with a TODO message.
Replace emit_todo() calls in three codegen paths (simple assignment,
compound assignment/update, and for-in/of) with proper ReferenceError
using the "Invalid left-hand side in assignment" message, matching the
behavior of V8 and JSC.
When a for-of or for-await-of loop exits via break, return, throw,
or continue-to-outer-loop, we now correctly call IteratorClose
(or AsyncIteratorClose) to give the iterator a chance to clean
up resources.
This uses a synthetic FinallyContext that wraps the LHS assignment
and loop body, reusing the existing try/finally completion record
machinery. The ReturnToFinally boundary is placed between Break
and Continue so that continue-to-same-loop bypasses the close
(zero overhead on normal iteration) while all other abrupt exits
route through the iterator close dispatch chain.
for-in (enumerate) does not require iterator close per spec.
Change the completion_value field from Optional<Value> to Operand
in both IteratorClose and AsyncIteratorClose bytecode instructions.
This allows passing a dynamic value from a register, which is needed
for iterator close on abrupt completion where the exception value
is not known at codegen time.
Remove CodeGenerationError and make all bytecode generation functions
return their results directly instead of wrapping them in
CodeGenerationErrorOr.
For the few remaining sites where codegen encounters an unimplemented
or unexpected AST node, we now use a new emit_todo() helper that emits
a NewTypeError + Throw sequence at compile time (preserving the runtime
behavior) and then switches to a dead basic block so subsequent codegen
for the same function can continue without issue.
This allows us to remove error handling from all callers of the
bytecode compiler, simplifying the code significantly.
These checks validate engine-internal usage of builtin abstract
operations (arity, argument types, known operation names), not user JS
code. Replace CodeGenerationError returns with VERIFY() assertions:
- Spread argument check becomes VERIFY(!argument.is_spread)
- Arity checks become VERIFY(arguments.size() == N)
- StringLiteral type checks become VERIFY(message)
- Unknown operation/constant fallthroughs become VERIFY_NOT_REACHED()
Replace CodeGenerationError returns with VERIFY_NOT_REACHED() or
VERIFY() at sites that are provably unreachable:
- Non-computed member expression fallbacks in emit_load_from_reference,
emit_store_to_reference, and emit_delete_reference (member expression
properties are always computed, identifier, or private identifier)
- Two non-computed member expression fallbacks in AssignmentExpression
- Default case in compound assignment switch (all 15 AssignmentOp values
are handled)
- BindingPattern Empty/Expression name+alias pair (computed property
names always require an alias)
- Two assignment+destructuring combinations in for-in/of body evaluation
(is_destructuring is only set for VariableDeclaration lhs, which
always has VarBinding or LexicalBinding kind, never Assignment)
When a class field has a BigInt literal key like `128n = class {}`,
the anonymous class should get the name "128". The codegen path
handles Identifier, StringLiteral, and NumericLiteral keys but was
missing BigInt keys, causing the name to be empty.
Parse the BigInt literal value at codegen time and convert it to a
decimal string for both the field_name (anonymous function naming)
and class_field_initializer_name (eval("arguments") checking) paths.
Add static factory methods create_for_function_node() on
SharedFunctionInstanceData and update all callers to use them instead
of FunctionNode::ensure_shared_data().
This removes the GC::Root<SharedFunctionInstanceData> cache from
FunctionNode, eliminating the coupling between the RefCounted AST
and GC-managed runtime objects. The cache was effectively dead code:
hoisted declarations use m_functions_to_initialize directly, and
function expressions always create fresh instances during codegen.
After compiling the bytecode executable on first run, null out the
AST (m_parse_node) and clear AnnexB candidates since they are no
longer needed. This frees the memory held by the entire AST for the
script's lifetime.
The parse_node() accessor now returns a nullable pointer. Callers
(js.cpp for AST dumping, Interpreter for first compilation) access
the AST before it is dropped.
Add Script::global_declaration_instantiation() that performs the GDI
algorithm using pre-computed name lists and shared function data
instead of walking the AST.
Runtime checks (has_lexical_declaration, can_declare_global_function,
etc.) remain since they depend on global environment state. AnnexB
iterates pre-collected candidates and calls
set_should_do_additional_annexB_steps() on stored refs.
The Interpreter::run(Script&) now calls the Script method instead of
the Program method.
Extract FunctionParsingInsights into its own header and introduce
FunctionLocal as a standalone mirror of Identifier::Local. This
allows SharedFunctionInstanceData.h to avoid pulling in the full
AST type hierarchy, reducing transitive include bloat.
The AST.h include is kept in SharedFunctionInstanceData.cpp where
it's needed for the constructor that accesses AST node types.
Replace the runtime uses of formal_parameters() with pre-computed data:
- m_formal_parameter_count stores the parameter count
- m_parameter_names_for_mapped_arguments stores ordered parameter names
for simple parameter lists (used by create_mapped_arguments_object)
Change create_mapped_arguments_object to take Span<Utf16FlyString>
instead of NonnullRefPtr<FunctionParameters const>.
Remove virtual formal_parameters() from FunctionObject as it is no
longer needed.
Pre-compute the data that emit_function_declaration_instantiation
previously obtained by querying ScopeNode methods at codegen time:
- m_has_scope_body: whether ecmascript_code is a ScopeNode
- m_has_non_local_lexical_declarations: from ScopeNode query
- m_lexical_bindings: non-local lexically-scoped identifier names and
their constant-declaration status
After this change, emit_function_declaration_instantiation no longer
casts m_ecmascript_code to ScopeNode or calls any ScopeNode methods.
Replace Vector<FunctionDeclaration const&> with a FunctionToInitialize
struct that stores a pre-created SharedFunctionInstanceData, function
name, and local index. The SharedFunctionInstanceData for each hoisted
function is created eagerly during the parent's construction, removing
the need to reference FunctionDeclaration AST nodes after construction.
Replace VariableNameToInitialize (which holds Identifier const&) with a
VarBinding struct that stores pre-extracted values: name, local index,
parameter_binding, and function_name. This removes a reference to AST
Identifier nodes from SharedFunctionInstanceData, allowing the AST to
be freed after compilation.
Replace the ClassExpression const& reference in the NewClass
instruction with a u32 class_blueprint_index. The interpreter now
reads from the ClassBlueprint stored on the Executable and calls
construct_class() instead of the AST-based create_class_constructor().
Literal field initializers (numbers, booleans, null, strings, negated
numbers) are used directly in construct_class() without creating an
ECMAScriptFunctionObject, avoiding function creation overhead for
common field patterns like `x = 0` or `name = "hello"`.
Set class_field_initializer_name on SharedFunctionInstanceData at
codegen time for statically-known field keys (identifiers, private
identifiers, string literals, and numeric literals). For computed
keys, the name is set at runtime in construct_class().
ClassExpression AST nodes are no longer referenced from bytecode.
Build a ClassBlueprint from ClassExpression elements at codegen time:
- Methods/getters/setters: register SharedFunctionInstanceData from
the method's FunctionExpression
- Field initializers with literal values (numbers, booleans, null,
strings, negated numbers): store the value directly, avoiding
function creation entirely
- Field initializers with non-literal values: wrap in
ClassFieldInitializerStatement and create SharedFunctionInstanceData
- Static initializers: create SharedFunctionInstanceData from the
function body
- Constructor: register SharedFunctionInstanceData from the
constructor's FunctionExpression
Add public accessors to ClassMethod::function() and
StaticInitializer::function_body() for codegen access.
The blueprint is registered but not yet used by NewClass (dual path).
No behavioral change.
Introduce ClassBlueprint and ClassElementDescriptor structs that will
replace the AST-backed class construction path. ClassBlueprint stores
pre-compiled function data indices and element metadata, following the
same pattern as SharedFunctionInstanceData for NewFunction.
Add Vector<ClassBlueprint> to Executable for storage.
No behavioral change.
Replace the FunctionNode const& stored on the NewFunction bytecode
instruction with an index into a table of pre-created
SharedFunctionInstanceData objects on the Executable.
During bytecode compilation, we now eagerly create
SharedFunctionInstanceData for each function that will be
instantiated by NewFunction, and store it on both the FunctionNode
(for caching) and the Executable (for GC tracing).
At runtime, NewFunction simply looks up the SharedFunctionInstanceData
by index and calls create_from_function_data() directly, bypassing
the AST entirely. This removes one of the main reasons the AST had
to stay alive after compilation.
The instantiate_ordinary_function_expression() helper in
Interpreter.cpp is removed as its non-trivial code path (creating a
scope for named function expressions) was dead code -- it was only
called when !has_name(), so the has_own_name branch never executed.
After successful bytecode compilation, the m_functions_to_initialize
and m_var_names_to_initialize_binding vectors are no longer needed
as they are only consumed by emit_function_declaration_instantiation()
during code generation.
Add clear_compile_inputs() to release these vectors post-compile,
and call it from both ECMAScriptFunctionObject::get_stack_frame_size()
and NativeJavaScriptBackedFunction::bytecode_executable() after their
respective lazy compilation succeeds.
Also add a pre-compile assertion in Generator::generate_from_function()
to verify we never try to compile the same function data twice, and a
VERIFY in ECMAScriptFunctionObject::ecmascript_code() to guard against
null dereference.
delete super.x and delete super[expr] always throw a ReferenceError
per spec. Instead of deferring this to runtime via DeleteByIdWithThis
and DeleteByValueWithThis instructions, emit the throw directly during
bytecode generation.
Remove the now-unused DeleteByIdWithThis and DeleteByValueWithThis
instructions, and add a NewReferenceError instruction.
Each of the three blocks in a TryStatement (try body, catch body,
finally body) needs its own CompletionRegisterScope so that
break/continue inside any of them carries the block's own
completion value rather than leaking a value from a surrounding
statement or a different block.
Previously, statements inside these blocks would update the
enclosing scope's completion register (e.g. a for-loop's
register), and if break/continue fired with no prior expression
value, the enclosing register's stale value would leak through
as the completion value instead of undefined.
Each block now allocates a fresh register initialized to
undefined and uses it as the current completion register during
body generation. This matches the pattern already used by loops
and switch statements.
When a loop or switch body produces an abrupt completion (break or
continue) with an empty value, the ES spec requires UpdateEmpty to
replace the empty value with the last non-empty completion value V.
The bytecode compiler was failing to do this because it only updated
the completion register after body codegen, guarded by
!is_current_block_terminated(). When break/continue terminated the
block, the update was skipped.
Fix this with three changes:
1. Introduce a CompletionRegisterScope that tells
ScopeNode::generate_bytecode to eagerly emit Mov instructions
into the completion register after each value-producing
statement. This ensures the register is up to date before any
break or continue fires.
2. Give IfStatement its own CompletionRegisterScope (initialized
to undefined) during branch evaluation. This models the spec's
UpdateEmpty(stmtCompletion, undefined) for if-statements: when
break/continue fires inside an if-branch, the scoped jump
propagation sees that the if's completion register differs from
the loop's and emits a Mov, correctly replacing the eagerly
written value with undefined. Without this, code like
{ 3; if (true) { break; } else { } } would incorrectly carry
the value 3 instead of undefined through the break.
3. Capture loop body results and emit a fallback Mov for
non-ScopeNode bodies (e.g. bare expression statements like
do x=1; while(false)) that don't participate in the eager
CompletionRegisterScope update mechanism.
For labelled break/continue that cross loop boundaries, the jump
codegen now propagates the inner completion register to the target
scope's completion register before emitting the jump.
Also fix ForStatement to use a proper completion register
(previously it returned the body result directly, which was wrong
for empty bodies and break-with-no-value cases).
The exception handler table is sorted by start_offset, so use
binary_search instead of a linear scan. This matches the pattern
already used by source_range_at() in the same file.
This comment referenced the old runtime unwind context stack behavior
where a flag had to be set to prevent yield from going through a
finally statement. That mechanism was removed and finally is now
handled purely through explicit completion records in bytecode.
After replacing the runtime unwind context stack with explicit
completion records for try/finally dispatch, the distinction between
"handler" (catch) and "finalizer" (finally) in the exception handler
table is no longer meaningful at runtime.
handle_exception() checked handler first, then finalizer, but they
did the exact same thing (set the PC). When both were present, the
finalizer was dead code.
Collapse both fields into a single handler_offset (now non-optional,
since an entry always has a target), remove the finalizer concept
from BasicBlock, UnwindContext, and ExceptionHandlers, and simplify
handle_exception() to a direct assignment.
The runtime unwind context stack was pushed by EnterUnwindContext
and popped by LeaveUnwindContext. With both opcodes removed, it is
no longer read or written by anything.
Remove UnwindInfo, the unwind_contexts vector, its GC visit loop,
its copy in ExecutionContext::copy(), and the VERIFY assertions that
referenced it in handle_exception() and catch_exception().
LeaveUnwindContext popped the runtime unwind context stack. With the
stack being removed, all emission sites become dead code. Remove the
opcode and all its emissions.
EnterUnwindContext pushed an UnwindInfo and jumped to entry_point.
Without the stack push, it's just a Jump. Replace the single emission
site with a Jump and remove the opcode entirely.
Replace the saved_lexical_environments stack in ExecutionContextRareData
with explicit register-based environment tracking. Environments are now
stored in registers and restored via SetLexicalEnvironment, making the
environment flow visible in bytecode.
Key changes:
- Add GetLexicalEnvironment and SetLexicalEnvironment opcodes
- CreateLexicalEnvironment takes explicit parent and dst operands
- EnterObjectEnvironment stores new environment in a dst register
- NewClass takes an explicit class_environment operand
- Remove LeaveLexicalEnvironment opcode (instead: SetLexicalEnvironment)
- Remove saved_lexical_environments from ExecutionContextRareData
- Use a reserved register for the saved lexical environment to avoid
dominance issues with lazily-emitted GetLexicalEnvironment
Each finally scope gets two registers (completion_type and
completion_value) that form an explicit completion record. Every path
into the finally body sets these before jumping, and a dispatch chain
after the finally body routes to the correct continuation.
This replaces the old implicit protocol that relied on the exception
register, a saved_return_value register, and a scheduled_jump field
on ExecutionContext, allowing us to remove:
- 5 opcodes (ContinuePendingUnwind, ScheduleJump, LeaveFinally,
RestoreScheduledJump, PrepareYield)
- 1 reserved register (saved_return_value)
- 2 ExecutionContext fields (scheduled_jump, previously_scheduled_jumps)
The spec for PropertyDefinitionEvaluation requires that when evaluating
a property definition with a computed key (PropertyDefinition :
PropertyName : AssignmentExpression), the PropertyName is fully
evaluated (including ToPropertyKey, which calls ToPrimitive) before the
value's AssignmentExpression is evaluated.
Our bytecode compiler was evaluating the key expression first, then
the value expression, and only performing ToPropertyKey later inside
PutByValue at runtime. This meant user-observable side effects from
ToPrimitive (such as calling Symbol.toPrimitive or toString on the key
object) would fire after the value expression had already been
evaluated.
Fix this by using a new ToPrimitiveWithStringHint instruction that
performs ToPrimitive with string hint(!), and emitting it between the
key and value evaluations in ObjectExpression codegen.
After ToPrimitive, the key is already a primitive, so the subsequent
ToPropertyKey inside PutByValue becomes a no-op from the perspective
of user-observable side
effects.
Also update an existing test that was asserting the old (incorrect)
evaluation order, and add comprehensive new tests for computed property
key evaluation order.
When the rest element in an object destructuring assignment targets a
MemberExpression (e.g. `({a, ...t.rest} = obj)`), we were incorrectly
storing the original source object to the reference instead of the
rest object produced by CopyObjectExcludingProperties.
For example, `({a, ...t.rest} = {a:1, b:2, c:3})` would set t.rest
to `{a:1, b:2, c:3}` instead of the correct `{b:2, c:3}`.
The fix is to pass the result of CopyObjectExcludingProperties
to emit_store_to_reference instead of the original RHS.
The FIXME comments suggested that ToPropertyKey was called at the wrong
time for computed super property access. However, extensive testing
shows that both Ladybird and V8 implement the correct ordering according
to the ECMA262 specification.
Remove the outdated FIXME comments and add comprehensive test coverage
for super property computed keys with Symbol.toPrimitive to prevent
regressions.
Route tagged template identifier lookup through
GetCalleeAndThisFromEnvironment only when the identifier is non-local.
Keep local and global identifiers on Identifier::generate_bytecode so
TDZ checks and ordinary undefined-this behavior stay intact.
Expand runtime coverage with a tagged-template TDZ regression case,
sequential with-binding calls, and getter-returned tag functions.
For non-Reference calls (e.g. (0, fn)(), (cond ? fn : x)()), the
codegen correctly passes undefined as the thisValue, matching step 2b
of EvaluateCall in the spec. OrdinaryCallBindThis then coerces
undefined to the global object in sloppy mode at runtime. Replace the
stale FIXME with a clarifying comment.
Also add comprehensive tests for this-value behavior in non-Reference
call patterns (comma, ternary, logical, assignment, nullish coalescing)
in both sloppy and strict mode.
Both SetFunctionName and MakeConstructor are already performed by
ECMAScriptFunctionObject::initialize() when the object is created
via create_from_function_node:
- SetFunctionName: The name is passed to SharedFunctionInstanceData,
and initialize() creates the "name" property from it.
- MakeConstructor: has_constructor() returns true for all normal
non-arrow functions, m_constructor_kind defaults to Base, and
m_may_need_lazy_prototype_instantiation handles the prototype
property creation lazily.
The FIXME claimed that IsAnonymousFunctionDefinition + NamedEvaluation
was missing for simple assignment expressions like `x = function() {}`.
However, the code directly below the FIXME already implements this
correctly via emit_named_evaluation_if_anonymous_function.
The GetGlobal bytecode optimization bypasses the normal environment
record lookup for global variable access. When a global property is
an accessor (getter), the receiver passed to the getter must be the
global object, not undefined.
The spec's Get(O, P) abstract operation is defined as O.[[Get]](P, O),
meaning the object itself is always the receiver. The global
environment's GetBindingValue delegates to its object record's
GetBindingValue, which calls Get(bindingObject, N), so the receiver
should be the binding object (the global object).
Both the cached path (calling the getter directly from get_direct)
and the non-cached path (calling internal_get) were passing
js_undefined() as the receiver. This caused strict-mode getters on
global properties to receive undefined as their this-value instead
of globalThis.
Notably, the corresponding SetGlobal paths already correctly passed
&binding_object for setter calls.
The i32 multiplication fast path in Mul::execute_impl was producing
+0 instead of -0 when one operand was negative and the other was
zero (e.g. `var a = -1, b = 0; a * b`).
This happened because i32 can't represent -0, so `Value(0)` was
always positive zero. We now fall through to the double path when
the i32 result is zero, which correctly handles the sign.
Also add comprehensive multiplication tests covering negative zero,
basic arithmetic, large integers, type coercion, NaN, and Infinity.
There is no need to concat empty string literals when building template
literals. Now strings will only be concatenated if they need to be.
To handle the edge case where the first segment is not a string
literal, a new `ToString` op code has been added to ensure the value is
a string concatenating more strings.
In addition, basic const folding is now supported for template literal
constants (templates with no interpolated values), which is commonly
used for multi-line string constants.
This improves and expands the ability to do dead code elimination on
conditions which are always truthy or falsey.
The following cases are now optimized:
* `if (true){}` -> Only emit `if` block, ignore `else`
* `if (false){}` -> Only emit `else if`/`else` block
* `while (false){}` -> Ignore `while` loop entirely
* `for (x;false;){}` -> Only emit `x` (if it exists), skip `for` block
* Ternary -> Directly return left/right hand side if condition is const
Previously, when direct eval() was called, we would mark the entire
environment chain as "permanently screwed by eval", disabling variable
access caching all the way up to the global scope.
This was overly conservative. According to the ECMAScript specification,
a sloppy direct eval() can only inject var declarations into its
containing function's variable environment - it cannot inject variables
into parent function scopes.
This patch makes two changes:
1. Stop propagating the "screwed by eval" flag at function boundaries.
When set_permanently_screwed_by_eval() hits a FunctionEnvironment or
GlobalEnvironment, it no longer continues to outer environments.
2. Check each environment during cache lookup traversal. If any
environment in the path is marked as screwed, we bail to the slow
path. This catches the case where we're inside a function with eval
and have a cached coordinate pointing to an outer scope.
The second change is necessary because eval can create local variables
that shadow outer bindings. When looking up a variable from inside a
function that called eval, we can't trust cached coordinates that point
to outer scopes, since eval may have created a closer binding.
This improves performance for code with nested functions where an inner
function uses eval but parent functions perform many variable accesses.
The parent functions can now use cached environment coordinates.
All 29 new tests verify behavior matches V8.
Bytecode source map entries are always added in order of increasing
bytecode offset, and lookups only happen during error handling (a cold
path). This makes a sorted vector with binary search a better fit than
a hash map.
This change reduces memory overhead and speeds up bytecode generation
by avoiding hash table operations during compilation. Lookups remain
fast via binary search, and since source_range_at() is only called
when generating stack traces, the O(log n) lookup is acceptable.
Add VERIFY guards to catch bytecode programs that exceed u32::max bytes
and narrow the bytecode_offset parameter in add_source_map_entry() to
u32. This is a preparatory change for optimizing source map storage.
Logical expressions like `true || false` are now constant folded. This
also allows for dead code elimination if we know the right-hand side of
the expression will never be evaluated (such as `false && f()` or
`true || f()`).
In the test suites, the values are now being constant folded at compile
time. To ensure that the actual evaluation logic is being called
properly, I had to duplicate the tests and call them via a function so
the compiler would not optimize the evaluation logic away.
This also demotes `NaN` and `Infinity` identifiers to `nan` and
`inf` double literals, which will further help with const folding.
This is a common way to convert a value to a boolean. Instead of doing
a boolean conversion and 2 negate operations, we replace this with a
single `ToBoolean` op code.
This adds a new `test-js-bytecode` target which ensures that codegen
changes do not impact emitted bytecode IR, or if it does, it is known
and the tests are updated accordingly.
Similar to the LibWeb tests, the tests are stored in the following
format:
* `Libraries/LibJS/Bytecode/Tests/input`: Input `.js` files
* `Libraries/LibJS/Bytecode/Tests/expected`: Expected `.txt` bytecode
* `Libraries/LibJS/Bytecode/Tests/output`: Emitted `.txt` bytecode
The `output` dir is git-ignored, but stores the output so you can diff
and inspect failed tests more easily.
There is only one test so far, which is a baseline test that should not
change dramatically unless we change the bytecode output format.
Numeric string keys like "0" are converted to numeric property keys and
stored in indexed storage rather than shape-based storage. The shape
caching optimization introduced in 505fe0a977 didn't account for this,
causing properties with numeric keys to be lost on subsequent calls.
The fix excludes object literals with numeric string keys from the
shape caching fast path by checking if any key would become a numeric
property index.
Every function call allocates an ExecutionContext with a trailing array
of Values for registers, locals, constants, and arguments. Previously,
the constructor would initialize all slots to js_special_empty_value(),
but constant slots were then immediately overwritten by the interpreter
copying in values from the Executable before execution began.
To eliminate this redundant initialization, we rearrange the layout from
[registers | constants | locals] to [registers | locals | constants].
This groups registers and locals together at the front, allowing us to
initialize only those slots while leaving constant slots uninitialized
until they're populated with their actual values.
This reduces the per-call initialization cost from O(registers + locals
+ constants) to O(registers + locals).
Also tightens up the types involved (size_t -> u32) and adds VERIFYs to
guard against overflow when computing the combined slot counts, and to
ensure the total fits within the 29-bit operand index field.
When a function creates object literals with simple property names,
we now cache the resulting shape after the first instantiation. On
subsequent calls, we create the object with the cached shape directly
and write property values at their known offsets.
This avoids repeated shape transitions and property offset lookups
for a common JavaScript pattern.
The optimization uses two new bytecode instructions:
- CacheObjectShape: Captures the final shape after object construction
- InitObjectLiteralProperty: Writes properties using cached offsets
Only "simple" object literals are optimized (string literal keys with
simple value expressions). Complex cases like computed properties,
getters/setters, and spread elements use the existing slow path.
3.4x speedup on a microbenchmark that repeatedly instantiates an object
literal with 26 properties. Small progressions on various benchmarks.
When instantiating an object literal, we go through the list of
properties and add them one by one to the new object. However, we were
totally neglecting to cache the shape transitions incurred by this
process, even though we had allocated a PropertyLookupCache for it.
1.25x speedup on Octane/splay.js and just generally very useful.
We know the length they're gonna end up with up front since we're
instantiating array literals. Pre-sizing them allows us to skip
incremental resizing of the property storage.
This adds visit_edges(Cell::Visitor&) methods to various helper structs
that contain GC pointers, and makes sure they are called from owning
GC-heap-allocated objects as needed.
These were found by our Clang plugin after expanding its capabilities.
The added rules will be enforced by CI going forward.
This resolves a FIXME in its code generation, particularly for:
- Caching the template object
- Setting the correct property attributes
- Freezing the resulting objects
This allows archive.org to load, which uses the Lit library.
The Lit library caches these template objects to determine if a
template has changed, allowing it to determine to do a full template
rerender or only partially update the rendering. Before, we would
always cause a full rerender on update because we didn't return the
same template object.
This caused issues with archive.org's code, I believe particularly with
its router library, where we would constantly detach and reattach nodes
unexpectedly, ending up with the page content not being attached to the
router's custom element.
The AddOwnProperty inline cache would incorrectly apply to frozen,
sealed, or non-extensible objects because it only checked if the
object's shape matched the cached "from_shape", not whether the object
was actually extensible.
Since Object.freeze(), Object.seal(), and Object.preventExtensions()
don't change the object's shape, a normal empty object {} and a
frozen Object.freeze({}) would share the same shape. The IC cache
populated from adding a property to the normal object would then be
incorrectly used for the frozen object, allowing property addition
to what should be a non-extensible object.
The fix adds an extensibility check before applying the AddOwnProperty
cache. Also adds comprehensive tests for dictionary shapes and
non-extensible object IC behavior.
Use `Op::Call` directly instead of creating a single-element array and
using `CallWithArgumentArray` when calling iterator methods (`next`,
`throw`, `return`) in `yield*` expressions.
Instead of storing a list of builtin function objects with the realm,
just move the builtin field from NativeFunction up to FunctionObject.
Now you can ask any FunctionObject for its builtin(), and we no longer
need the get_builtin_value() API.
Fixes 10 test262 tests that were querying the realm builtins at a
bad time.
Regressed in 54b755126c.
We have so many inline caches that this kind of thing becomes profitable
on complex pages. Also the memory access pattern is slightly nicer for
polymorphic caches.
Reduces memory usage on my x.com home feed by 4.9 MiB.
Reorder members and use u32 instead of Optional<u32> for things that
didn't actually need the "empty" state other than for assertions.
Reduces memory usage on my x.com home feed by 9.9 MiB.
This fixes an issue where we'd incorrectly retain objects via the
[[HomeObject]] slot. This common pattern was affected:
Object.defineProperty(o, "foo", {
get: function() { return 123; }
});
Above, the object literal would get assigned to the [[HomeObject]]
slot even though "get" is not a "method" per the spec.
This frees about 30,000 objects on my x.com home feed.
For StringPrototype functions that defer to RegExpPrototype builtins,
we can skip the generic call stuff (eliding the execution context etc)
and just call the builtin directly.
1.03x speedup on Octane/regexp.js
These were helpful when PropertyKey instantiation happened in the
interpreter, but now that we've moved it to bytecode generation time,
we can use the basic Put*ById* instructions instead.
Instead of creating PropertyKeys on the fly during interpreter
execution, we now store fully-formed ones in the Executable.
This avoids a whole bunch of busywork in property access instructions
and substantially reduces code size bloat.
These instructions are not necessarily rarely used, but they are very
large in terms of code size. By putting them out of line we keep the hot
path of the interpreter smaller and tighter.
No need to check this at runtime, we have all the necessary info already
when generating bytecode.
Also mark the "yes, we are indeed calling the builtin" path [[likely]]
since it's exceedingly rare for anyone to replace the global functions.
This doesn't affect interpreter size directly, but let's inform the
compiler that we're not terribly worried about code using the `with`
statement in JS.
While we're in the bytecode compiler, we want to know which type of
Operand we're dealing with, but once we've generated the bytecode
stream, we only ever need its index.
This patch simplifies Operand by removing the aarch64 bitfield hacks
and makes it 32-bit on all platforms. We keep 3 type bits in the high
bits of the index while compiling, and then zero them out when
flattening the final bytecode stream.
This makes bytecode more compact on x86_64, and avoids bit twiddling
on aarch64. Everyone wins something!
When stringifying bytecode for debugging output, we now have an API in
Executable that can look at a raw operand index and tell you what type
of operand it was, based on known quantities of each type in the stack
frame.
In our process architecture, there's only ever one JS::VM per process.
This allows us to have a VM::the() singleton getter that optimizes
down to a single global access everywhere.
Seeing 1-2% speed-up on all JS benchmarks from this.
This allows us to use the bytecode implementation of await, which
correctly suspends execution contexts and handles completion
injections.
This gains us 4 test262 tests around mutating Array.fromAsync's
iterable whilst it's suspended as well.
This is also one step towards removing spin_until, which the
non-bytecode implementation of await uses.
```
Duration:
-5.98s
Summary:
Diff Tests:
+4 ✅ -4 ❌
Diff Tests:
[...]/Array/fromAsync/asyncitems-array-add-to-singleton.js ❌ -> ✅
[...]/Array/fromAsync/asyncitems-array-add.js ❌ -> ✅
[...]/Array/fromAsync/asyncitems-array-mutate.js ❌ -> ✅
[...]/Array/fromAsync/asyncitems-array-remove.js ❌ -> ✅
```
This hosts the ability to compile and run JavaScript to implement
native functions. This is particularly useful for any native function
that is not a normal function, for example async functions such as
Array.fromAsync, which require yielding.
These functions are not allowed to observe anything from outside their
environment. Any global identifiers will instead be assumed to be a
reference to an abstract operation or a constant. The generator will
inject the appropriate bytecode if the name of the global identifier
matches a known name. Anything else will cause a code generation error.
All the data we need for compilation is in SharedFunctionInstanceData,
so we shouldn't depend on ECMAScriptFunctionObject.
Allows NativeJavaScriptBackedFunction to compile bytecode.
By adding static_asserts to prove that all of our generated instruction
classes are trivially destructible, we can confidently remove the
destructor walk in BasicBlock and save ourselves some unnecessary work.
This commit adds a new Bytecode.def file that describes all the LibJS
bytecode instructions.
From this, we are able to generate the full declarations for all C++
bytecode instruction classes, as well as their serialization code.
Note that some of the bytecode compiler was updated since instructions
no longer have default constructor arguments.
The big immediate benefit here is that we lose a couple thousand lines
of hand-written C++ code. Going forward, this also allows us to do more
tooling for the bytecode VM, now that we have an authoritative
description of its instructions.
Key things to know about:
- Instructions can inherit from one another. At the moment, everything
simply inherits from the base "Instruction".
- @terminator means the instruction terminates a basic block.
- @nothrow means the instruction cannot throw. This affects how the
interpreter interacts with it.
- Variable-length instructions are automatically supported. Just put an
array of something as the last field of the instruction.
- The m_length field is magical. If present, it will be populated with
the full length of the instruction. This is used for variable-length
instructions.
For example, this:
```
Exception handlers:
from 678 to 698 handler 658 finalizer 0
from 698 to 6f8 handler 658 finalizer 0
from 6f8 to 708 handler 658 finalizer 0
from 708 to 750 handler 658 finalizer 0
from 750 to 788 handler 658 finalizer 0
from 788 to 7a0 handler 658 finalizer 0
from 7a0 to 7a8 handler 658 finalizer 0
```
Becomes:
```
Exception handlers:
from 678 to 7a8 handler 658 finalizer 0
```
With this change, `GetIterator` no longer GC-allocates an
`IteratorRecord`. Instead, it stores the iterator record fields in
bytecode registers. This avoids per-iteration allocations in patterns
like: `for (let [x] of array) {}`.
`IteratorRecord` now inherits from `IteratorRecordImpl`, which holds the
iteration state. This allows the existing iteration helpers
(`iterator_next()`, `iterator_step()`, etc.) operate on both the
GC-allocated and the register-backed forms.
Microbenchmarks:
1.1x array-destructuring-assignment-rest.js
1.226x array-destructuring-assignment.js