fix: class constructor scope index mismatch and arguments binding slot allocation#5352
fix: class constructor scope index mismatch and arguments binding slot allocation#5352acsses wants to merge 3 commits intoboa-dev:mainfrom
Conversation
…cation Two bugs caused a runtime panic in class constructors: 1. `num_bindings_non_local()` in `boa_ast/src/scope.rs` excluded the `arguments` binding because it lacks the `ESCAPES` flag, resulting in `FunctionEnvironment` being created with 0 slots while the bytecode emitted `PutLexicalValue(index=0)`. 2. `compile_class()` in `boa_engine/src/bytecompiler/class.rs` always pushed the constructor's function scope to `constant_scope(0)`, while `scope_analyzer` assigned it a higher index via `visit_function_like`, causing a mismatch at runtime. Fix (1) by counting `arguments` bindings unconditionally in `num_bindings_non_local()`. Fix (2) by applying the same scope push logic as `FunctionCompiler`: push `name_scope` first if non-local, then conditionally push `function_scope` based on `all_bindings_local()` and `requires_function_scope()`. Fixes boa-dev#5351
Test262 conformance changes
New panics (7):Tested main commit: |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #5352 +/- ##
===========================================
+ Coverage 47.24% 59.95% +12.71%
===========================================
Files 476 566 +90
Lines 46892 62829 +15937
===========================================
+ Hits 22154 37671 +15517
- Misses 24738 25158 +420 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
zhuzhu81998
left a comment
There was a problem hiding this comment.
the test cases you posted are not actually panicking on main?
|
And the PR adds panics that weren't there. This is obvious AI slop. |
|
No It's actually happened with: EnviromentCodeCargo.toml[package]
name = "dino_runtime"
version = "0.1.3"
edition = "2021"
description = "A Rust runtime for Deno"
license = "MIT"
[dependencies]
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "time", "net", "sync"] }
reqwest = { version = "0.11", features = ["blocking", "json"] }
serde = { version = "1", features = ["derive"] }
base64 = "0.22.1"
boa_engine = "0.21.1"
boa_gc = "0.21.1"
boa_runtime = {version = "0.21.1",features = ["fetch","reqwest-blocking"]}
getrandom = "0.2"
sha1 = "0.10"
sha2 = "0.10"
num-bigint = "0.4"
num-traits = "0.2"main.rsuse boa_engine::{Context, Source, js_string};
use boa_engine::object::ObjectInitializer;
use boa_engine::property::Attribute;
use std::fs;
use std::path::Path;
mod base64;
mod event;
mod crypto;
mod abort;
mod bigint_polyfill;
use crate::base64::{register_atob_extension, register_btoa_extension, register_base64_extension};
use crate::event::register_event_extension;
use crate::crypto::register_crypto_extension;
use crate::abort::register_abort_extension;
use crate::bigint_polyfill::register_bigint_polyfill;
/// Boa v0.21 workaround:
fn main() {
let path_name = "./dist/test.js";
let path = Path::new(path_name);
let script_code = fs::read_to_string(path)
.expect("failed to liad js file");
let mut context = Context::default();
register_base64_extension(&mut context);
register_btoa_extension(&mut context);
register_atob_extension(&mut context);
register_event_extension(&mut context);
let _ = boa_runtime::register(
(
// Register the default logger.
boa_runtime::extensions::ConsoleExtension::default(),
boa_runtime::extensions::EncodingExtension{},
boa_runtime::extensions::TimeoutExtension{},
boa_runtime::extensions::FetchExtension(
boa_runtime::fetch::BlockingReqwestFetcher::default()
),
),
None,
&mut context,
);
context.eval(Source::from_bytes(br#"
(function() {
var OriginalTextDecoder = TextDecoder;
globalThis.TextDecoder = function(label, options) {
if (typeof label === 'string') {
label = label.toLowerCase().replace(/[^a-z0-9\-]/g, '');
if (label === 'utf8') label = 'utf-8';
}
return new OriginalTextDecoder(label, options);
};
globalThis.TextDecoder.prototype = OriginalTextDecoder.prototype;
})();
"#)).expect("Failed to patch TextDecoder");
register_crypto_extension(&mut context);
register_bigint_polyfill(&mut context);
{
let navigator_obj = ObjectInitializer::new(&mut context)
.property(js_string!("userAgent"), js_string!("Node.js"), Attribute::all())
.build();
context.register_global_property(
js_string!("navigator"),
navigator_obj,
Attribute::all(),
).ok();
}
register_abort_extension(&mut context);
let wrapped_code = format!(
r#"try {{ {} }} catch(e) {{ throw new Error("Bundle error: " + (e && e.message ? e.message : String(e)) + "\nStack: " + (e && e.stack ? e.stack : "no stack")); }}"#,
script_code
);
match context.eval(Source::from_bytes(wrapped_code.as_bytes())) {
Ok(res) => {
// The result is a promise, so we need to await it.
if let Some(promise) = res.as_promise() {
match promise.await_blocking(&mut context) {
Ok(result) => {
println!("Promise resolved with: {}", result.to_string(&mut context).unwrap().to_std_string_escaped());
}
Err(err) => {
if let Ok(native_error) = err.try_native(&mut context) {
eprintln!("Native error: {:?}", native_error);
}
}
}
} else {
println!("Script executed with result: {}", res.to_string(&mut context).unwrap().to_std_string_escaped());
}
}
Err(err) => {
// Pretty print the error
eprintln!("Eval error: {}", err);
if let Ok(native_error) = err.try_native(&mut context) {
eprintln!("Native error: {:?}", native_error);
}
}
}
loop {
context.run_jobs().expect("Job execution failed");
}
}event.rsuse boa_engine::{
class::{Class, ClassBuilder},
js_string,
native_function::NativeFunction,
Context, JsArgs, JsData, JsObject, JsResult, JsValue,
};
use boa_gc::{Finalize, Trace};
// ─── Event ───
#[derive(Debug, Trace, Finalize, JsData)]
struct Event;
impl Class for Event {
const NAME: &'static str = "Event";
const LENGTH: usize = 1;
fn data_constructor(
_new_target: &JsValue,
_args: &[JsValue],
_context: &mut Context,
) -> JsResult<Self> {
Ok(Self)
}
fn object_constructor(
instance: &JsObject,
args: &[JsValue],
context: &mut Context,
) -> JsResult<()> {
let event_type = args.get_or_undefined(0).to_string(context)?;
let options = args.get_or_undefined(1);
let bubbles = if let Some(obj) = options.as_object() {
obj.get(js_string!("bubbles"), context)?
.to_boolean()
} else {
false
};
let cancelable = if let Some(obj) = options.as_object() {
obj.get(js_string!("cancelable"), context)?
.to_boolean()
} else {
false
};
instance.set(js_string!("type"), JsValue::from(event_type), true, context)?;
instance.set(js_string!("bubbles"), JsValue::from(bubbles), true, context)?;
instance.set(js_string!("cancelable"), JsValue::from(cancelable), true, context)?;
instance.set(js_string!("defaultPrevented"), JsValue::from(false), true, context)?;
Ok(())
}
fn init(class: &mut ClassBuilder<'_>) -> JsResult<()> {
class.method(
js_string!("preventDefault"),
0,
NativeFunction::from_fn_ptr(|this, _args, context| {
if let Some(obj) = this.as_object() {
let cancelable = obj
.get(js_string!("cancelable"), context)?
.to_boolean();
if cancelable {
obj.set(js_string!("defaultPrevented"), JsValue::from(true), true, context)?;
}
}
Ok(JsValue::undefined())
}),
);
class.method(
js_string!("stopPropagation"),
0,
NativeFunction::from_fn_ptr(|_this, _args, _ctx| Ok(JsValue::undefined())),
);
class.method(
js_string!("stopImmediatePropagation"),
0,
NativeFunction::from_fn_ptr(|_this, _args, _ctx| Ok(JsValue::undefined())),
);
Ok(())
}
}
// ─── EventTarget ───
#[derive(Debug, Trace, Finalize, JsData)]
struct EventTarget;
impl Class for EventTarget {
const NAME: &'static str = "EventTarget";
const LENGTH: usize = 0;
fn data_constructor(
_new_target: &JsValue,
_args: &[JsValue],
_context: &mut Context,
) -> JsResult<Self> {
Ok(Self)
}
fn object_constructor(
instance: &JsObject,
_args: &[JsValue],
context: &mut Context,
) -> JsResult<()> {
let listeners = boa_engine::object::ObjectInitializer::new(context).build();
instance.set(js_string!("_listeners"), listeners, true, context)?;
Ok(())
}
fn init(class: &mut ClassBuilder<'_>) -> JsResult<()> {
class.method(
js_string!("addEventListener"),
2,
NativeFunction::from_fn_ptr(|this, args, context| {
let event_type = args.get_or_undefined(0).to_string(context)?;
let callback = args.get_or_undefined(1).clone();
if let Some(obj) = this.as_object() {
let listeners = obj.get(js_string!("_listeners"), context)?;
if let Some(listeners_obj) = listeners.as_object() {
let existing = listeners_obj.get(event_type.clone(), context)?;
let arr = if let Some(arr_obj) = existing.as_object() {
let len = arr_obj.get(js_string!("length"), context)?
.to_number(context)? as u32;
arr_obj.set(js_string!(len.to_string()), callback, true, context)?;
arr_obj.set(js_string!("length"), JsValue::from(len + 1), true, context)?;
existing
} else {
let arr = boa_engine::object::builtins::JsArray::new(context);
arr.push(callback, context)?;
let arr_val = JsValue::from(arr);
listeners_obj.set(event_type, arr_val.clone(), true, context)?;
arr_val
};
let _ = arr;
}
}
Ok(JsValue::undefined())
}),
);
class.method(
js_string!("removeEventListener"),
2,
NativeFunction::from_fn_ptr(|this, args, context| {
let event_type = args.get_or_undefined(0).to_string(context)?;
let callback = args.get_or_undefined(1);
if let Some(obj) = this.as_object() {
let listeners = obj.get(js_string!("_listeners"), context)?;
if let Some(listeners_obj) = listeners.as_object() {
let existing = listeners_obj.get(event_type.clone(), context)?;
if let Some(arr_obj) = existing.as_object() {
let arr = boa_engine::object::builtins::JsArray::from_object(arr_obj.clone())?;
let len = arr.length(context)?;
let new_arr = boa_engine::object::builtins::JsArray::new(context);
for i in 0..len {
let item = arr.get(i, context)?;
if !JsValue::same_value(&item, callback) {
new_arr.push(item, context)?;
}
}
listeners_obj.set(event_type, JsValue::from(new_arr), true, context)?;
}
}
}
Ok(JsValue::undefined())
}),
);
class.method(
js_string!("dispatchEvent"),
1,
NativeFunction::from_fn_ptr(|this, args, context| {
let event = args.get_or_undefined(0);
let event_type = if let Some(ev_obj) = event.as_object() {
ev_obj.get(js_string!("type"), context)?.to_string(context)?
} else {
return Ok(JsValue::from(true));
};
if let Some(obj) = this.as_object() {
let listeners = obj.get(js_string!("_listeners"), context)?;
if let Some(listeners_obj) = listeners.as_object() {
let existing = listeners_obj.get(event_type, context)?;
if let Some(arr_obj) = existing.as_object() {
let arr = boa_engine::object::builtins::JsArray::from_object(arr_obj.clone())?;
let len = arr.length(context)?;
for i in 0..len {
let cb = arr.get(i, context)?;
if let Some(cb_obj) = cb.as_object() {
if cb_obj.is_callable() {
cb_obj.call(&JsValue::undefined(), &[event.clone()], context)?;
}
}
}
}
}
}
let default_prevented = if let Some(ev_obj) = event.as_object() {
ev_obj.get(js_string!("defaultPrevented"), context)?.to_boolean()
} else {
false
};
Ok(JsValue::from(!default_prevented))
}),
);
Ok(())
}
}
// ─── Request ───
#[derive(Debug, Trace, Finalize, JsData)]
struct Request;
impl Class for Request {
const NAME: &'static str = "Request";
const LENGTH: usize = 1;
fn data_constructor(
_new_target: &JsValue,
_args: &[JsValue],
_context: &mut Context,
) -> JsResult<Self> {
Ok(Self)
}
fn object_constructor(
instance: &JsObject,
args: &[JsValue],
context: &mut Context,
) -> JsResult<()> {
let input = args.get_or_undefined(0).clone();
let init = args.get_or_undefined(1).clone();
let method = if let Some(obj) = init.as_object() {
let m = obj.get(js_string!("method"), context)?;
if m.is_undefined() {
JsValue::from(js_string!("GET"))
} else {
m
}
} else {
JsValue::from(js_string!("GET"))
};
instance.set(js_string!("url"), input, true, context)?;
instance.set(js_string!("method"), method, true, context)?;
if let Some(obj) = init.as_object() {
let headers = obj.get(js_string!("headers"), context)?;
if !headers.is_undefined() {
instance.set(js_string!("headers"), headers, true, context)?;
}
let body = obj.get(js_string!("body"), context)?;
if !body.is_undefined() {
instance.set(js_string!("body"), body, true, context)?;
}
}
Ok(())
}
fn init(_class: &mut ClassBuilder<'_>) -> JsResult<()> {
Ok(())
}
}
// ─── Response ───
#[derive(Debug, Trace, Finalize, JsData)]
struct Response;
impl Class for Response {
const NAME: &'static str = "Response";
const LENGTH: usize = 0;
fn data_constructor(
_new_target: &JsValue,
_args: &[JsValue],
_context: &mut Context,
) -> JsResult<Self> {
Ok(Self)
}
fn object_constructor(
instance: &JsObject,
args: &[JsValue],
context: &mut Context,
) -> JsResult<()> {
let body = args.get_or_undefined(0).clone();
let init = args.get_or_undefined(1).clone();
let status = if let Some(obj) = init.as_object() {
let s = obj.get(js_string!("status"), context)?;
if s.is_undefined() {
JsValue::from(200)
} else {
s
}
} else {
JsValue::from(200)
};
instance.set(js_string!("body"), body, true, context)?;
instance.set(js_string!("status"), status, true, context)?;
instance.set(js_string!("ok"), JsValue::from(true), true, context)?;
if let Some(obj) = init.as_object() {
let headers = obj.get(js_string!("headers"), context)?;
if !headers.is_undefined() {
instance.set(js_string!("headers"), headers, true, context)?;
}
}
Ok(())
}
fn init(_class: &mut ClassBuilder<'_>) -> JsResult<()> {
Ok(())
}
}
// ─── setting function ───
pub fn register_event_extension(context: &mut Context) {
context.register_global_class::<Event>().expect("Failed to register Event class");
context.register_global_class::<EventTarget>().expect("Failed to register EventTarget class");
context.register_global_class::<Request>().expect("Failed to register Request class");
context.register_global_class::<Response>().expect("Failed to register Response class");
context.register_global_class::<CustomEvent>().expect("Failed to register CustomEvent class");
context.register_global_class::<BroadcastChannel>().expect("Failed to register BroadcastChannel class");
}
// ─── CustomEvent ───
#[derive(Debug, Trace, Finalize, JsData)]
struct CustomEvent;
impl Class for CustomEvent {
const NAME: &'static str = "CustomEvent";
const LENGTH: usize = 1;
fn data_constructor(
_new_target: &JsValue,
_args: &[JsValue],
_context: &mut Context,
) -> JsResult<Self> {
Ok(Self)
}
fn object_constructor(
instance: &JsObject,
args: &[JsValue],
context: &mut Context,
) -> JsResult<()> {
let event_type = args.get_or_undefined(0).to_string(context)?;
let options = args.get_or_undefined(1);
let bubbles = if let Some(obj) = options.as_object() {
obj.get(js_string!("bubbles"), context)?.to_boolean()
} else {
false
};
let cancelable = if let Some(obj) = options.as_object() {
obj.get(js_string!("cancelable"), context)?.to_boolean()
} else {
false
};
let detail = if let Some(obj) = options.as_object() {
let d = obj.get(js_string!("detail"), context)?;
if d.is_undefined() { JsValue::null() } else { d }
} else {
JsValue::null()
};
instance.set(js_string!("type"), JsValue::from(event_type), true, context)?;
instance.set(js_string!("bubbles"), JsValue::from(bubbles), true, context)?;
instance.set(js_string!("cancelable"), JsValue::from(cancelable), true, context)?;
instance.set(js_string!("defaultPrevented"), JsValue::from(false), true, context)?;
instance.set(js_string!("detail"), detail, true, context)?;
Ok(())
}
fn init(class: &mut ClassBuilder<'_>) -> JsResult<()> {
class.method(
js_string!("preventDefault"),
0,
NativeFunction::from_fn_ptr(|this, _args, context| {
if let Some(obj) = this.as_object() {
let cancelable = obj.get(js_string!("cancelable"), context)?.to_boolean();
if cancelable {
obj.set(js_string!("defaultPrevented"), JsValue::from(true), true, context)?;
}
}
Ok(JsValue::undefined())
}),
);
class.method(
js_string!("stopPropagation"),
0,
NativeFunction::from_fn_ptr(|_this, _args, _ctx| Ok(JsValue::undefined())),
);
class.method(
js_string!("stopImmediatePropagation"),
0,
NativeFunction::from_fn_ptr(|_this, _args, _ctx| Ok(JsValue::undefined())),
);
Ok(())
}
}
// ─── BroadcastChannel ───
#[derive(Debug, Trace, Finalize, JsData)]
struct BroadcastChannel;
impl Class for BroadcastChannel {
const NAME: &'static str = "BroadcastChannel";
const LENGTH: usize = 1;
fn data_constructor(
_new_target: &JsValue,
_args: &[JsValue],
_context: &mut Context,
) -> JsResult<Self> {
Ok(Self)
}
fn object_constructor(
instance: &JsObject,
args: &[JsValue],
context: &mut Context,
) -> JsResult<()> {
let name = args.get_or_undefined(0).to_string(context)?;
instance.set(js_string!("name"), JsValue::from(name), true, context)?;
instance.set(js_string!("onmessage"), JsValue::null(), true, context)?;
instance.set(js_string!("onmessageerror"), JsValue::null(), true, context)?;
let listeners = boa_engine::object::ObjectInitializer::new(context).build();
instance.set(js_string!("_listeners"), listeners, true, context)?;
Ok(())
}
fn init(class: &mut ClassBuilder<'_>) -> JsResult<()> {
class.method(
js_string!("postMessage"),
1,
NativeFunction::from_fn_ptr(|_this, _args, _ctx| Ok(JsValue::undefined())),
);
class.method(
js_string!("close"),
0,
NativeFunction::from_fn_ptr(|_this, _args, _ctx| Ok(JsValue::undefined())),
);
class.method(
js_string!("addEventListener"),
2,
NativeFunction::from_fn_ptr(|this, args, context| {
let event_type = args.get_or_undefined(0).to_string(context)?;
let callback = args.get_or_undefined(1).clone();
if let Some(obj) = this.as_object() {
let listeners = obj.get(js_string!("_listeners"), context)?;
if let Some(listeners_obj) = listeners.as_object() {
let existing = listeners_obj.get(event_type.clone(), context)?;
if let Some(arr_obj) = existing.as_object() {
let len = arr_obj.get(js_string!("length"), context)?
.to_number(context)? as u32;
arr_obj.set(js_string!(len.to_string()), callback, true, context)?;
arr_obj.set(js_string!("length"), JsValue::from(len + 1), true, context)?;
} else {
let arr = boa_engine::object::builtins::JsArray::new(context);
arr.push(callback, context)?;
listeners_obj.set(event_type, JsValue::from(arr), true, context)?;
}
}
}
Ok(JsValue::undefined())
}),
);
class.method(
js_string!("removeEventListener"),
2,
NativeFunction::from_fn_ptr(|this, args, context| {
let event_type = args.get_or_undefined(0).to_string(context)?;
let callback = args.get_or_undefined(1);
if let Some(obj) = this.as_object() {
let listeners = obj.get(js_string!("_listeners"), context)?;
if let Some(listeners_obj) = listeners.as_object() {
let existing = listeners_obj.get(event_type.clone(), context)?;
if let Some(arr_obj) = existing.as_object() {
let arr = boa_engine::object::builtins::JsArray::from_object(arr_obj.clone())?;
let len = arr.length(context)?;
let new_arr = boa_engine::object::builtins::JsArray::new(context);
for i in 0..len {
let item = arr.get(i, context)?;
if !JsValue::same_value(&item, callback) {
new_arr.push(item, context)?;
}
}
listeners_obj.set(event_type, JsValue::from(new_arr), true, context)?;
}
}
}
Ok(JsValue::undefined())
}),
);
class.method(
js_string!("dispatchEvent"),
1,
NativeFunction::from_fn_ptr(|_this, _args, _ctx| Ok(JsValue::from(true))),
);
Ok(())
}
}crypto.rsuse boa_engine::{
js_string,
native_function::NativeFunction,
object::builtins::{JsArrayBuffer, JsPromise, JsUint8Array},
object::ObjectInitializer,
property::Attribute,
Context, JsArgs, JsNativeError, JsValue,
};
use boa_engine::object::builtins::AlignedVec;
use sha2::{Digest, Sha256, Sha384, Sha512};
use sha1::Sha1;
fn digest_hash(algorithm: &str, data: &[u8]) -> Result<Vec<u8>, String> {
match algorithm {
"SHA-1" => {
let mut hasher = Sha1::new();
hasher.update(data);
Ok(hasher.finalize().to_vec())
}
"SHA-256" => {
let mut hasher = Sha256::new();
hasher.update(data);
Ok(hasher.finalize().to_vec())
}
"SHA-384" => {
let mut hasher = Sha384::new();
hasher.update(data);
Ok(hasher.finalize().to_vec())
}
"SHA-512" => {
let mut hasher = Sha512::new();
hasher.update(data);
Ok(hasher.finalize().to_vec())
}
_ => Err(format!("Unsupported algorithm: {}", algorithm)),
}
}
fn vec_to_aligned(v: Vec<u8>) -> AlignedVec<u8> {
let mut aligned = AlignedVec::new(v.len());
for b in v {
aligned.push(b);
}
aligned
}
pub fn register_crypto_extension(context: &mut Context) {
// --- crypto.getRandomValues ---
let get_random_values = NativeFunction::from_copy_closure(|_this, args, context| {
let array = args.get_or_undefined(0);
let obj = array
.as_object()
.ok_or_else(|| {
JsNativeError::typ().with_message("argument must be a TypedArray")
})?
.clone();
let typed_array = JsUint8Array::from_object(obj)
.map_err(|_| JsNativeError::typ().with_message("argument must be a Uint8Array"))?;
let len = typed_array.length(context)?;
let mut random_bytes = vec![0u8; len];
getrandom::getrandom(&mut random_bytes)
.map_err(|e| JsNativeError::typ().with_message(e.to_string()))?;
for i in 0..len {
typed_array.set(i as u64, JsValue::from(random_bytes[i]), true, context)?;
}
Ok(array.clone())
})
.to_js_function(context.realm());
// --- crypto.randomUUID ---
let random_uuid = NativeFunction::from_copy_closure(|_this, _args, _context| {
let mut bytes = [0u8; 16];
getrandom::getrandom(&mut bytes)
.map_err(|e| JsNativeError::typ().with_message(e.to_string()))?;
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
let uuid = format!(
"{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
bytes[0], bytes[1], bytes[2], bytes[3],
bytes[4], bytes[5], bytes[6], bytes[7],
bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]
);
Ok(JsValue::from(js_string!(uuid)))
})
.to_js_function(context.realm());
// --- crypto.subtle.digest ---
let subtle_digest = NativeFunction::from_copy_closure(|_this, args, context| {
let algorithm = args.get_or_undefined(0);
let algo_name = if let Some(obj) = algorithm.as_object() {
obj.get(js_string!("name"), context)?
.to_string(context)?
.to_std_string_escaped()
} else {
algorithm.to_string(context)?.to_std_string_escaped()
};
let data_arg = args.get_or_undefined(1);
let data_bytes: Vec<u8> = if let Some(obj) = data_arg.as_object() {
// Try reading via 'length' property (works for TypedArray, Array-like)
let length_val = obj.get(js_string!("length"), context)?;
if !length_val.is_undefined() && !length_val.is_null() {
let len = length_val.to_number(context)? as usize;
let mut bytes = Vec::with_capacity(len);
for i in 0..len {
let v = obj.get(i as u32, context)?;
bytes.push(v.to_number(context).unwrap_or(0.0) as u8);
}
bytes
} else {
return Err(JsNativeError::typ()
.with_message("data must be a TypedArray (e.g. Uint8Array)")
.into());
}
} else {
return Err(JsNativeError::typ()
.with_message("data must be a BufferSource")
.into());
};
let hash_result = digest_hash(&algo_name, &data_bytes).map_err(|e| {
JsNativeError::typ().with_message(e)
})?;
let aligned = vec_to_aligned(hash_result);
let buffer = JsArrayBuffer::from_byte_block(aligned, context)?;
Ok(buffer.into())
})
.to_js_function(context.realm());
// --- crypto.subtle.generateKey (stub) ---
let subtle_generate_key =
NativeFunction::from_copy_closure(|_this, _args, context| {
let obj = ObjectInitializer::new(context).build();
let promise = JsPromise::resolve(obj, context);
Ok(promise.into())
})
.to_js_function(context.realm());
// --- crypto.subtle.exportKey (stub) ---
let subtle_export_key =
NativeFunction::from_copy_closure(|_this, _args, context| {
let obj = ObjectInitializer::new(context).build();
let promise = JsPromise::resolve(obj, context);
Ok(promise.into())
})
.to_js_function(context.realm());
// --- crypto.subtle.importKey (stub) ---
let subtle_import_key =
NativeFunction::from_copy_closure(|_this, _args, context| {
let obj = ObjectInitializer::new(context).build();
let promise = JsPromise::resolve(obj, context);
Ok(promise.into())
})
.to_js_function(context.realm());
// --- crypto.subtle.sign (stub) ---
let subtle_sign = NativeFunction::from_copy_closure(|_this, _args, context| {
let aligned = vec_to_aligned(vec![0u8; 64]);
let buffer = JsArrayBuffer::from_byte_block(aligned, context)?;
let promise = JsPromise::resolve(buffer, context);
Ok(promise.into())
})
.to_js_function(context.realm());
// --- crypto.subtle.verify (stub) ---
let subtle_verify =
NativeFunction::from_copy_closure(|_this, _args, context| {
let promise = JsPromise::resolve(JsValue::from(true), context);
Ok(promise.into())
})
.to_js_function(context.realm());
// --- crypto.subtle.deriveKey (stub) ---
let subtle_derive_key =
NativeFunction::from_copy_closure(|_this, _args, context| {
let obj = ObjectInitializer::new(context).build();
let promise = JsPromise::resolve(obj, context);
Ok(promise.into())
})
.to_js_function(context.realm());
// --- crypto.subtle.deriveBits (stub) ---
let subtle_derive_bits =
NativeFunction::from_copy_closure(|_this, _args, context| {
let aligned = vec_to_aligned(vec![0u8; 32]);
let buffer = JsArrayBuffer::from_byte_block(aligned, context)?;
let promise = JsPromise::resolve(buffer, context);
Ok(promise.into())
})
.to_js_function(context.realm());
// --- crypto.subtle.encrypt (stub) ---
let subtle_encrypt =
NativeFunction::from_copy_closure(|_this, _args, context| {
let aligned = vec_to_aligned(vec![0u8; 32]);
let buffer = JsArrayBuffer::from_byte_block(aligned, context)?;
let promise = JsPromise::resolve(buffer, context);
Ok(promise.into())
})
.to_js_function(context.realm());
// --- crypto.subtle.decrypt (stub) ---
let subtle_decrypt =
NativeFunction::from_copy_closure(|_this, _args, context| {
let aligned = vec_to_aligned(vec![0u8; 32]);
let buffer = JsArrayBuffer::from_byte_block(aligned, context)?;
let promise = JsPromise::resolve(buffer, context);
Ok(promise.into())
})
.to_js_function(context.realm());
// --- Build crypto.subtle object ---
let subtle_obj = ObjectInitializer::new(context)
.property(js_string!("digest"), subtle_digest, Attribute::all())
.property(js_string!("generateKey"), subtle_generate_key, Attribute::all())
.property(js_string!("exportKey"), subtle_export_key, Attribute::all())
.property(js_string!("importKey"), subtle_import_key, Attribute::all())
.property(js_string!("sign"), subtle_sign, Attribute::all())
.property(js_string!("verify"), subtle_verify, Attribute::all())
.property(js_string!("deriveKey"), subtle_derive_key, Attribute::all())
.property(js_string!("deriveBits"), subtle_derive_bits, Attribute::all())
.property(js_string!("encrypt"), subtle_encrypt, Attribute::all())
.property(js_string!("decrypt"), subtle_decrypt, Attribute::all())
.build();
// --- Build crypto object ---
let crypto_obj = ObjectInitializer::new(context)
.property(js_string!("getRandomValues"), get_random_values, Attribute::all())
.property(js_string!("randomUUID"), random_uuid, Attribute::all())
.property(js_string!("subtle"), subtle_obj, Attribute::all())
.build();
context.register_global_property(
js_string!("crypto"),
crypto_obj,
Attribute::all(),
).ok();
}bigint_polyfill.rsuse boa_engine::{
js_string,
native_function::NativeFunction,
Context, JsArgs, JsNativeError, JsObject, JsResult, JsValue,
};
use num_bigint::BigInt;
use num_traits::ToPrimitive;
use std::str::FromStr;
fn data_view_prototype(context: &mut Context) -> JsResult<JsObject> {
let data_view = context.global_object().get(js_string!("DataView"), context)?;
let data_view = data_view
.as_object()
.ok_or_else(|| JsNativeError::typ().with_message("DataView is not available"))?;
let prototype = data_view.get(js_string!("prototype"), context)?;
prototype
.as_object()
.ok_or_else(|| JsNativeError::typ().with_message("DataView.prototype is not available").into())
}
fn call_method(
target: &JsObject,
name: &'static str,
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let method = target.get(js_string!(name), context)?;
let method = method
.as_object()
.ok_or_else(|| JsNativeError::typ().with_message(format!("{name} is not callable")))?;
method.call(this, args, context)
}
fn call_bigint(value: JsValue, context: &mut Context) -> JsResult<JsValue> {
let bigint = context.global_object().get(js_string!("BigInt"), context)?;
let bigint = bigint
.as_object()
.ok_or_else(|| JsNativeError::typ().with_message("BigInt is not callable"))?;
bigint.call(&JsValue::undefined(), &[value], context)
}
fn bigint_from_decimal(value: impl Into<String>, context: &mut Context) -> JsResult<JsValue> {
call_bigint(JsValue::from(js_string!(value.into())), context)
}
fn parse_bigint_arg(value: &JsValue, context: &mut Context) -> JsResult<BigInt> {
let bigint = call_bigint(value.clone(), context)?;
let bigint_text = bigint.to_string(context)?.to_std_string_escaped();
BigInt::from_str(&bigint_text).map_err(|_| {
JsNativeError::typ()
.with_message("failed to parse BigInt argument")
.into()
})
}
fn wrap_to_u64(value: BigInt) -> JsResult<u64> {
let modulus = BigInt::from(1u128) << 64;
let mut wrapped: BigInt = value % &modulus;
if wrapped < BigInt::from(0u8) {
wrapped += &modulus;
}
wrapped.to_u64().ok_or_else(|| {
JsNativeError::typ()
.with_message("failed to normalize BigInt into 64 bits")
.into()
})
}
fn get_u32_parts(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<(u32, u32)> {
let this_obj = this
.as_object()
.ok_or_else(|| JsNativeError::typ().with_message("DataView method called on incompatible receiver"))?
.clone();
let byte_offset = args.get_or_undefined(0).clone();
let little_endian = args.get_or_undefined(1).clone();
let little_endian_flag = little_endian.to_boolean();
let base_offset = byte_offset.to_number(context)?;
let low_offset = if little_endian_flag { 0.0 } else { 4.0 };
let high_offset = if little_endian_flag { 4.0 } else { 0.0 };
let lo = call_method(
&this_obj,
"getUint32",
this,
&[
JsValue::from(base_offset + low_offset),
little_endian.clone(),
],
context,
)?
.to_number(context)? as u32;
let hi = call_method(
&this_obj,
"getUint32",
this,
&[
JsValue::from(base_offset + high_offset),
little_endian,
],
context,
)?
.to_number(context)? as u32;
Ok((hi, lo))
}
fn get_biguint64(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let (hi, lo) = get_u32_parts(this, args, context)?;
let value = ((hi as u64) << 32) | lo as u64;
bigint_from_decimal(value.to_string(), context)
}
fn get_bigint64(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let (hi, lo) = get_u32_parts(this, args, context)?;
let value = ((hi as u64) << 32) | lo as u64;
if (value & (1u64 << 63)) != 0 {
let signed = (value as i128) - (1i128 << 64);
bigint_from_decimal(signed.to_string(), context)
} else {
bigint_from_decimal(value.to_string(), context)
}
}
fn set_biguint64(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let this_obj = this
.as_object()
.ok_or_else(|| JsNativeError::typ().with_message("DataView method called on incompatible receiver"))?
.clone();
let byte_offset = args.get_or_undefined(0).clone();
let little_endian = args.get_or_undefined(2).clone();
let little_endian_flag = little_endian.to_boolean();
let base_offset = byte_offset.to_number(context)?;
let value = wrap_to_u64(parse_bigint_arg(args.get_or_undefined(1), context)?)?;
let lo = (value & 0xFFFF_FFFF) as u32;
let hi = (value >> 32) as u32;
let low_offset = if little_endian_flag { 0.0 } else { 4.0 };
let high_offset = if little_endian_flag { 4.0 } else { 0.0 };
call_method(
&this_obj,
"setUint32",
this,
&[
JsValue::from(base_offset + low_offset),
JsValue::from(lo),
little_endian.clone(),
],
context,
)?;
call_method(
&this_obj,
"setUint32",
this,
&[
JsValue::from(base_offset + high_offset),
JsValue::from(hi),
little_endian,
],
context,
)?;
Ok(JsValue::undefined())
}
fn set_bigint64(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
set_biguint64(this, args, context)
}
fn install_if_missing(
prototype: &JsObject,
name: &'static str,
function: fn(&JsValue, &[JsValue], &mut Context) -> JsResult<JsValue>,
context: &mut Context,
) -> JsResult<()> {
if prototype.get(js_string!(name), context)?.is_undefined() {
let function = NativeFunction::from_fn_ptr(function).to_js_function(context.realm());
prototype.set(js_string!(name), function, true, context)?;
}
Ok(())
}
/// Register DataView BigInt method polyfills for Boa v0.21 using Rust-native host functions.
pub fn register_bigint_polyfill(context: &mut Context) {
let prototype = data_view_prototype(context)
.expect("Failed to access DataView.prototype for BigInt polyfill registration");
install_if_missing(&prototype, "getBigUint64", get_biguint64, context)
.expect("Failed to register DataView.prototype.getBigUint64");
install_if_missing(&prototype, "getBigInt64", get_bigint64, context)
.expect("Failed to register DataView.prototype.getBigInt64");
install_if_missing(&prototype, "setBigUint64", set_biguint64, context)
.expect("Failed to register DataView.prototype.setBigUint64");
install_if_missing(&prototype, "setBigInt64", set_bigint64, context)
.expect("Failed to register DataView.prototype.setBigInt64");
}base64.rsuse base64::{engine::general_purpose, Engine};
use boa_engine::{
js_string, Context, JsNativeError, JsValue, NativeFunction,JsArgs,
object::builtins::JsUint8Array,property::Attribute,JsError
};
pub fn register_base64_extension(context: &mut Context) {
let uint8_array_constructor = context
.global_object()
.get(js_string!("Uint8Array"), context)
.expect("Uint8Array not found")
.as_object()
.expect("Uint8Array is not an object")
.clone();
let from_base64 = NativeFunction::from_copy_closure(|_this, args: &[JsValue], context| {
let input = args
.get_or_undefined(0)
.to_string(context)?
.to_std_string_escaped();
let bytes = general_purpose::STANDARD
.decode(input)
.map_err(|e| {
JsNativeError::typ()
.with_message(e.to_string())
})?;
let js_uint8 = JsUint8Array::from_iter(bytes, context)?;
Ok(js_uint8.into())
}).to_js_function(context.realm());
let to_base64 = NativeFunction::from_copy_closure(|this, _args: &[JsValue], context| {
let obj = this
.as_object()
.ok_or_else(|| JsNativeError::typ().with_message("expected a Uint8Array"))?
.clone();
let uint8_array = JsUint8Array::from_object(obj)
.map_err(|_| JsNativeError::typ().with_message("expected a Uint8Array"))?;
let mut bytes = vec![0u8; uint8_array.length(context)?];
for i in 0..bytes.len() {
bytes[i] = uint8_array.at(i as i64, context)?.to_uint8(context)?;
}
let encoded = general_purpose::STANDARD.encode(&bytes);
Ok(JsValue::from(js_string!(encoded)))
})
.to_js_function(context.realm());
uint8_array_constructor
.set(
js_string!("fromBase64"),
from_base64,
true,
context,
)
.expect("failed to set Uint8Array.fromBase64");
let prototype = uint8_array_constructor
.get(js_string!("prototype"), context)
.expect("Uint8Array.prototype not found")
.as_object()
.expect("prototype is not an object")
.clone();
prototype
.set(
js_string!("toBase64"),
to_base64,
true,
context,
)
.expect("failed to set Uint8Array.prototype.toBase64");
}
pub fn register_btoa_extension(context: &mut Context) {
let btoa_fn = NativeFunction::from_copy_closure(|_this, args, context| {
let input = args.get_or_undefined(0)
.to_string(context)?
.to_std_string_escaped();
if input.chars().any(|c| c as u32 > 255) {
return Err(JsNativeError::range()
.with_message(
"The string to be encoded contains characters outside of the Latin1 range.",
)
.into());
}
let encoded = general_purpose::STANDARD.encode(input);
Ok(JsValue::new(js_string!(encoded)))
}).to_js_function(context.realm());
context.register_global_property(
js_string!("btoa"),
btoa_fn,
Attribute::WRITABLE | Attribute::CONFIGURABLE,
).expect("failed to register btoa");
}
pub fn register_atob_extension(context: &mut Context) {
let atob_fn = NativeFunction::from_copy_closure(|_this, args, context| {
let input = args.get_or_undefined(0)
.to_string(context)?
.to_std_string_escaped();
if input.chars().any(|c| c as u32 > 255) {
return Err(JsNativeError::range()
.with_message(
"The string to be decoded contains characters outside of the Latin1 range.",
)
.into());
}
// 3. Base64 decide
let decoded = general_purpose::STANDARD.decode(input)
.map_err(|e| JsError::from(JsNativeError::typ().with_message(e.to_string())))?;
Ok(JsValue::new(js_string!(decoded.into_iter().map(|b| b as char).collect::<String>())))
}).to_js_function(context.realm());
context.register_global_property(
js_string!("atob"),
atob_fn,
Attribute::WRITABLE | Attribute::CONFIGURABLE,
).expect("failed to register atob");
}abort.rsuse boa_engine::{
class::{Class, ClassBuilder},
js_string,
native_function::NativeFunction,
Context, JsArgs, JsData, JsNativeError, JsObject, JsResult, JsValue,
};
use boa_gc::{Finalize, Trace};
// ─── DOMException ───
#[derive(Debug, Trace, Finalize, JsData)]
struct DOMException;
impl Class for DOMException {
const NAME: &'static str = "DOMException";
const LENGTH: usize = 2;
fn data_constructor(
_new_target: &JsValue,
_args: &[JsValue],
_context: &mut Context,
) -> JsResult<Self> {
Ok(Self)
}
fn object_constructor(
instance: &JsObject,
args: &[JsValue],
context: &mut Context,
) -> JsResult<()> {
let message = if args.get_or_undefined(0).is_undefined() {
js_string!("")
} else {
args.get_or_undefined(0).to_string(context)?
};
let name = if args.get_or_undefined(1).is_undefined() {
js_string!("DOMException")
} else {
args.get_or_undefined(1).to_string(context)?
};
instance.set(js_string!("message"), JsValue::from(message), true, context)?;
instance.set(js_string!("name"), JsValue::from(name), true, context)?;
Ok(())
}
fn init(class: &mut ClassBuilder<'_>) -> JsResult<()> {
class.method(
js_string!("toString"),
0,
NativeFunction::from_fn_ptr(|this, _args, context| {
let name = this.as_object()
.map(|o| o.get(js_string!("name"), context))
.transpose()?
.unwrap_or(JsValue::from(js_string!("DOMException")));
let message = this.as_object()
.map(|o| o.get(js_string!("message"), context))
.transpose()?
.unwrap_or(JsValue::from(js_string!("")));
let n = name.to_string(context)?;
let m = message.to_string(context)?;
let s = format!("{}: {}", n.to_std_string_escaped(), m.to_std_string_escaped());
Ok(JsValue::from(js_string!(&*s)))
}),
);
Ok(())
}
}
// ─── AbortSignal ───
#[derive(Debug, Trace, Finalize, JsData)]
struct AbortSignal;
impl Class for AbortSignal {
const NAME: &'static str = "AbortSignal";
const LENGTH: usize = 0;
fn data_constructor(
_new_target: &JsValue,
_args: &[JsValue],
_context: &mut Context,
) -> JsResult<Self> {
Ok(Self)
}
fn object_constructor(
instance: &JsObject,
_args: &[JsValue],
context: &mut Context,
) -> JsResult<()> {
instance.set(js_string!("aborted"), JsValue::from(false), true, context)?;
instance.set(js_string!("reason"), JsValue::undefined(), true, context)?;
instance.set(js_string!("onabort"), JsValue::null(), true, context)?;
// listeners 配列を EventTarget 互換で保持
let listeners = boa_engine::object::builtins::JsArray::new(context);
let listeners_val: JsValue = listeners.into();
instance.set(js_string!("_listeners"), listeners_val, true, context)?;
Ok(())
}
fn init(class: &mut ClassBuilder<'_>) -> JsResult<()> {
class.method(
js_string!("throwIfAborted"),
0,
NativeFunction::from_fn_ptr(|this, _args, context| {
let obj = this.as_object().ok_or_else(|| {
JsNativeError::typ().with_message("not an object")
})?;
let aborted = obj.get(js_string!("aborted"), context)?.to_boolean();
if aborted {
let reason = obj.get(js_string!("reason"), context)?;
return Err(JsNativeError::typ()
.with_message(reason.to_string(context)?.to_std_string_escaped())
.into());
}
Ok(JsValue::undefined())
}),
);
class.method(
js_string!("addEventListener"),
2,
NativeFunction::from_fn_ptr(|this, args, context| {
let obj = this.as_object().ok_or_else(|| {
JsNativeError::typ().with_message("not an object")
})?;
let listeners = obj.get(js_string!("_listeners"), context)?;
if let Some(arr) = listeners.as_object() {
let len = arr.get(js_string!("length"), context)?.to_number(context)? as u32;
let pair = boa_engine::object::builtins::JsArray::new(context);
pair.push(args.get_or_undefined(0).clone(), context)?;
pair.push(args.get_or_undefined(1).clone(), context)?;
arr.set(len, pair, true, context)?;
}
Ok(JsValue::undefined())
}),
);
class.method(
js_string!("removeEventListener"),
2,
NativeFunction::from_fn_ptr(|_this, _args, _context| {
Ok(JsValue::undefined())
}),
);
class.method(
js_string!("dispatchEvent"),
1,
NativeFunction::from_fn_ptr(|this, args, context| {
let obj = this.as_object().ok_or_else(|| {
JsNativeError::typ().with_message("not an object")
})?;
let event = args.get_or_undefined(0);
let event_type = if let Some(e) = event.as_object() {
e.get(js_string!("type"), context)?
} else {
return Ok(JsValue::from(false));
};
let listeners = obj.get(js_string!("_listeners"), context)?;
if let Some(arr) = listeners.as_object() {
let len = arr.get(js_string!("length"), context)?.to_number(context)? as u32;
for i in 0..len {
let pair = arr.get(i, context)?;
if let Some(p) = pair.as_object() {
let t = p.get(0, context)?;
if t.to_string(context)? == event_type.to_string(context)? {
let cb = p.get(1, context)?;
if let Some(func) = cb.as_callable() {
func.call(this, &[event.clone()], context)?;
}
}
}
}
}
Ok(JsValue::from(true))
}),
);
Ok(())
}
}
// ─── AbortController ───
#[derive(Debug, Trace, Finalize, JsData)]
struct AbortCtrl;
impl Class for AbortCtrl {
const NAME: &'static str = "AbortController";
const LENGTH: usize = 0;
fn data_constructor(
_new_target: &JsValue,
_args: &[JsValue],
_context: &mut Context,
) -> JsResult<Self> {
Ok(Self)
}
fn object_constructor(
instance: &JsObject,
_args: &[JsValue],
context: &mut Context,
) -> JsResult<()> {
// new AbortSignal() を作成してセット
let signal = context.eval(boa_engine::Source::from_bytes(b"new AbortSignal()"))?;
instance.set(js_string!("signal"), signal, true, context)?;
Ok(())
}
fn init(class: &mut ClassBuilder<'_>) -> JsResult<()> {
class.method(
js_string!("abort"),
1,
NativeFunction::from_fn_ptr(|this, args, context| {
let obj = this.as_object().ok_or_else(|| {
JsNativeError::typ().with_message("not an object")
})?;
let signal_val = obj.get(js_string!("signal"), context)?;
let signal = signal_val.as_object().ok_or_else(|| {
JsNativeError::typ().with_message("signal is not an object")
})?;
let already_aborted = signal.get(js_string!("aborted"), context)?.to_boolean();
if already_aborted {
return Ok(JsValue::undefined());
}
signal.set(js_string!("aborted"), JsValue::from(true), true, context)?;
let reason = if args.get_or_undefined(0).is_undefined() {
// DOMException('The operation was aborted.', 'AbortError')
context.eval(boa_engine::Source::from_bytes(
b"new DOMException('The operation was aborted.', 'AbortError')",
))?
} else {
args.get_or_undefined(0).clone()
};
signal.set(js_string!("reason"), reason.clone(), true, context)?;
// onabort コールバック
let onabort = signal.get(js_string!("onabort"), context)?;
if let Some(func) = onabort.as_callable() {
func.call(&signal_val, &[], context)?;
}
// abort イベント dispatch
let event = context.eval(boa_engine::Source::from_bytes(b"new Event('abort')"))?;
if let Some(signal_obj) = signal_val.as_object() {
if signal_obj.has_property(js_string!("dispatchEvent"), context)? {
let dispatch = signal_obj.get(js_string!("dispatchEvent"), context)?;
if let Some(func) = dispatch.as_callable() {
func.call(&signal_val, &[event], context)?;
}
}
}
Ok(JsValue::undefined())
}),
);
Ok(())
}
}
pub fn register_abort_extension(context: &mut Context) {
context.register_global_class::<DOMException>().expect("Failed to register DOMException");
context.register_global_class::<AbortSignal>().expect("Failed to register AbortSignal");
context.register_global_class::<AbortCtrl>().expect("Failed to register AbortController");
} |
|
And here's a self-contained reproduction using only boa_engine 0.21.1: Cargo.toml: [package]
name = "boa_repro"
version = "0.1.0"
edition = "2021"
[dependencies]
boa_engine = "0.21.1"src/main.rs: use boa_engine::{Context, Source};
fn main() {
let mut context = Context::default();
// Bug 1: panics with "index out of bounds: the len is 0, but the index is 0"
context.eval(Source::from_bytes(br#"
class A {
constructor() {
const x = 1;
}
}
new A();
"#)).unwrap();
// Bug 2: same panic with closure over non-local binding
context.eval(Source::from_bytes(br#"
var B = class {
constructor(components) {
const { getHasher } = components;
this.fn = () => getHasher("test");
}
};
new B({ getHasher: (x) => x });
"#)).unwrap();
}Run with Environment:
|
i tried it with 0.21.1, it does panic. but if you use [package]
name = "boa_repro"
version = "0.1.0"
edition = "2021"
[dependencies]
boa_engine = { git = "https://github.com/boa-dev/boa.git", branch = "main" }
then it works fine. |
Description
This PR fixes two related bugs in the bytecode compiler that caused a panic
(
index out of bounds: the len is 0 but the index is 0) when executingesbuild-bundled JavaScript code targeting ES2020.
Bug 1:
argumentsbinding missing slot inFunctionEnvironmentRoot cause:
scope_analyzer.rsalways creates anargumentsbinding in the functionscope, but without the
ESCAPESflag:num_bindings_non_local()only counts bindings withESCAPES=true, so theargumentsbinding was excluded from the slot count. This causedFunctionEnvironmentto be created with 0 bindings, while the bytecode stillemitted
PutLexicalValue(index=0)for it, resulting in a panic at runtime.Fix (
boa_ast/src/scope.rs):Include the
argumentsbinding in the slot count unconditionally:Bug 2: Class constructor scope index mismatch
Root cause:
scope_analyzer.rsassigns scope indices to class constructor scopes viavisit_function_like, which incrementsself.indexbefore callingfunction_scope.set_index(self.index). For example, whenfunction_scope.all_bindings_local()is false:However,
compile_class()inclass.rsalways calledpush_scope()exactlyonce for the function scope, placing it at
constant_scope(0). At runtime,function_constructusescode.constant_scope(last_env)to retrieve thefunction scope. Since the
function_scopehadscope_index=1but was storedat
constant_scope(0), the wrong (or nonexistent) scope was accessed, causinga panic.
This is in contrast to
function.rs(FunctionCompiler), which correctlymirrors the scope index assignment by:
name_scopefirst if it has non-local bindingsfunction_scopebased onall_bindings_local()andrequires_function_scope()Fix (
boa_engine/src/bytecompiler/class.rs):Apply the same scoping logic as
FunctionCompilerto the class constructor:Test cases
How it was found
The panic was triggered by esbuild-bundled code with
--target=es2020, whichtransforms class fields into
__publicField()helper calls. The combinationof multiple
__publicFieldcalls followed byconstdeclarations in a classconstructor exposed both bugs simultaneously.