The completion value of a switch case is incorrectly reset to undefined
when a statement without a result (like a variable declaration) follows
an expression statement. This will be fixed in the next commit.
When a function has parameter expressions (default values), body var
declarations that shadow a name referenced in a default parameter
expression must not be optimized to local variables. The default
expression needs to resolve the name from the outer scope via the
environment chain, not read the uninitialized local.
We now mark identifiers referenced during formal parameter parsing
with an IsReferencedInFormalParameters flag, and skip local variable
optimization for body vars that carry both this flag and IsVar (but
not IsForbiddenLexical, which indicates parameter names themselves).
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.
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.
Previously, the function only handled a single level of member access,
producing strings like "<object>.isWall" for chained expressions like
"graphSet[j][k].isWall". Now it recurses through nested member
expressions, identifiers, string/numeric literals, and `this`.
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.
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.
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.
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.
Add bytecode tests verifying identifier resolution produces correct
register-backed locals, global lookups, argument indices, and
environment lookups for eval/with/captured cases.
Add runtime tests for destructuring assignment patterns with
expression defaults: class expressions (named/anonymous), function
expressions, arrow functions, nested destructuring, eval in
defaults, MemberExpression targets with setter functions, and class
name scoping.
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)
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 parsing a named function expression like
`Oops = function Oops() { Oops }`, the parser set a group-level flag
`might_be_variable_in_lexical_scope_in_named_function_assignment` that
propagated to the parent scope. This incorrectly prevented ALL `Oops`
identifiers from being marked as global, including those outside the
function expression.
Fix this by marking identifiers individually using
`set_is_inside_scope_with_eval()` only for identifiers inside the
function scope. This allows identifiers outside the function expression
to correctly use GetGlobal/SetGlobal while identifiers inside still
use GetBinding (since they may refer to the function's name binding).
Previously, when a nested function contained eval(), the parser would
mark all identifiers in parent functions as "inside scope with eval".
This prevented those identifiers from being marked as global, forcing
them to use GetBinding instead of GetGlobal.
However, eval() can only inject variables into its containing function's
scope, not into parent function scopes. So a parent function's reference
to a global like `Number` should still be able to use GetGlobal even if
a nested function contains eval().
This change adds a new flag `m_eval_in_current_function` that propagates
through block scopes within the same function but stops at function
boundaries. This flag is used for marking identifiers, while the
existing `m_screwed_by_eval_in_scope_chain` continues to propagate
across functions for local variable deoptimization (since eval can
access closure variables).
Before: `new Number(42)` in outer() with eval in inner() -> GetBinding
After: `new Number(42)` in outer() with eval in inner() -> GetGlobal