Bug Report
Description
Executing JavaScript code containing class constructors with const declarations
causes a panic in boa_engine:
thread 'main' panicked at 'index out of bounds: the len is 0 but the index is 0'
boa_engine/src/environments/runtime/declarative/mod.rs:310
Stack trace
6: boa_engine::environments::runtime::declarative::PoisonableEnvironment::set
at boa_engine-0.21.1/src/environments/runtime/declarative/mod.rs:310
7: boa_engine::environments::runtime::declarative::function::FunctionEnvironment::set
at boa_engine-0.21.1/src/environments/runtime/declarative/function.rs:66
10: boa_engine::environments::runtime::EnvironmentStack::put_lexical_value
at boa_engine-0.21.1/src/environments/runtime/mod.rs:280
11: boa_engine::vm::opcode::define::PutLexicalValue::operation
at boa_engine-0.21.1/src/vm/opcode/define/mod.rs:82
Minimal reproduction
// Case 1: arguments binding
class A {
constructor() {
const x = 1;
}
}
new A();
// Case 2: class constructor with closure over non-local binding
var B = class {
constructor(components) {
const { getHasher } = components;
this.fn = () => getHasher("sha2-256");
}
};
new B({ getHasher: (x) => x });
Expected behavior
Both cases execute without panic.
Actual behavior
Panic at runtime: index out of bounds: the len is 0 but the index is 0
Root cause analysis
Two bugs were identified:
Bug 1 (boa_ast/src/scope.rs):
scope_analyzer always creates an arguments binding in the function scope
without the ESCAPES flag. num_bindings_non_local() only counts bindings
with ESCAPES=true, so arguments is excluded from the slot count.
FunctionEnvironment is created with 0 slots, but the bytecode still emits
PutLexicalValue(index=0) for it, causing a panic.
Bug 2 (boa_engine/src/bytecompiler/class.rs):
compile_class() always calls push_scope() once for the constructor's
function scope, placing it at constant_scope(0). However, scope_analyzer
assigns scope indices via visit_function_like, which increments self.index
before calling function_scope.set_index(). This causes a mismatch between
the constant_scope index and the scope index expected at runtime, resulting
in the wrong (or nonexistent) scope being accessed.
FunctionCompiler in function.rs handles this correctly by mirroring the
scope index assignment, but compile_class() did not follow the same logic.
Version
boa_engine 0.21.1
Additional context
This bug was triggered by esbuild-bundled JavaScript code with --target=es2020,
which transforms class fields into __publicField() helper calls. The combination
of class constructors with const declarations and closures exposed both bugs.
Bug Report
Description
Executing JavaScript code containing class constructors with
constdeclarationscauses a panic in boa_engine:
thread 'main' panicked at 'index out of bounds: the len is 0 but the index is 0'
boa_engine/src/environments/runtime/declarative/mod.rs:310
Stack trace
6: boa_engine::environments::runtime::declarative::PoisonableEnvironment::set
at boa_engine-0.21.1/src/environments/runtime/declarative/mod.rs:310
7: boa_engine::environments::runtime::declarative::function::FunctionEnvironment::set
at boa_engine-0.21.1/src/environments/runtime/declarative/function.rs:66
10: boa_engine::environments::runtime::EnvironmentStack::put_lexical_value
at boa_engine-0.21.1/src/environments/runtime/mod.rs:280
11: boa_engine::vm::opcode::define::PutLexicalValue::operation
at boa_engine-0.21.1/src/vm/opcode/define/mod.rs:82
Minimal reproduction
Expected behavior
Both cases execute without panic.
Actual behavior
Panic at runtime:
index out of bounds: the len is 0 but the index is 0Root cause analysis
Two bugs were identified:
Bug 1 (
boa_ast/src/scope.rs):scope_analyzeralways creates anargumentsbinding in the function scopewithout the
ESCAPESflag.num_bindings_non_local()only counts bindingswith
ESCAPES=true, soargumentsis excluded from the slot count.FunctionEnvironmentis created with 0 slots, but the bytecode still emitsPutLexicalValue(index=0)for it, causing a panic.Bug 2 (
boa_engine/src/bytecompiler/class.rs):compile_class()always callspush_scope()once for the constructor'sfunction scope, placing it at
constant_scope(0). However,scope_analyzerassigns scope indices via
visit_function_like, which incrementsself.indexbefore calling
function_scope.set_index(). This causes a mismatch betweenthe
constant_scopeindex and the scope index expected at runtime, resultingin the wrong (or nonexistent) scope being accessed.
FunctionCompilerinfunction.rshandles this correctly by mirroring thescope index assignment, but
compile_class()did not follow the same logic.Version
boa_engine 0.21.1
Additional context
This bug was triggered by esbuild-bundled JavaScript code with
--target=es2020,which transforms class fields into
__publicField()helper calls. The combinationof class constructors with
constdeclarations and closures exposed both bugs.