Skip to content

fix: class constructor scope index mismatch and arguments binding slot allocation#5352

Closed
acsses wants to merge 3 commits intoboa-dev:mainfrom
acsses:main
Closed

fix: class constructor scope index mismatch and arguments binding slot allocation#5352
acsses wants to merge 3 commits intoboa-dev:mainfrom
acsses:main

Conversation

@acsses
Copy link
Copy Markdown

@acsses acsses commented Apr 28, 2026

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 executing
esbuild-bundled JavaScript code targeting ES2020.


Bug 1: arguments binding missing slot in FunctionEnvironment

Root cause:

scope_analyzer.rs always creates an arguments binding in the function
scope, but without the ESCAPES flag:

// scope_analyzer.rs
drop(env.create_mutable_binding(arguments.clone(), false));
// → BindingFlags: MUTABLE | LEX (no ESCAPES)

num_bindings_non_local() only counts bindings with ESCAPES=true, so the
arguments binding was excluded from the slot count. This caused
FunctionEnvironment to be created with 0 bindings, while the bytecode still
emitted PutLexicalValue(index=0) for it, resulting in a panic at runtime.

Fix (boa_ast/src/scope.rs):

Include the arguments binding in the slot count unconditionally:

pub fn num_bindings_non_local(&self) -> u32 {
    let arguments = JsString::from("arguments");
    self.inner
        .bindings
        .borrow()
        .iter()
        .filter(|binding| binding.escapes() || binding.name == arguments)
        .count() as u32
}

Bug 2: Class constructor scope index mismatch

Root cause:

scope_analyzer.rs assigns scope indices to class constructor scopes via
visit_function_like, which increments self.index before calling
function_scope.set_index(self.index). For example, when
function_scope.all_bindings_local() is false:

self.index += 1;  // e.g. index: 0 → 1
scopes.function_scope.set_index(self.index);  // scope_index = 1

However, compile_class() in class.rs always called push_scope() exactly
once for the function scope, placing it at constant_scope(0). At runtime,
function_construct uses code.constant_scope(last_env) to retrieve the
function scope. Since the function_scope had scope_index=1 but was stored
at constant_scope(0), the wrong (or nonexistent) scope was accessed, causing
a panic.

This is in contrast to function.rs (FunctionCompiler), which correctly
mirrors the scope index assignment by:

  1. Pushing name_scope first if it has non-local bindings
  2. Conditionally pushing function_scope based on all_bindings_local() and
    requires_function_scope()

Fix (boa_engine/src/bytecompiler/class.rs):

Apply the same scoping logic as FunctionCompiler to the class constructor:

if let Some(expr) = &class.constructor {
    // Mirror scope_analyzer: push name_scope first if non-local
    if let Some(name_scope) = class.name_scope {
        if !name_scope.all_bindings_local() {
            compiler.code_block_flags |= CodeBlockFlags::HAS_BINDING_IDENTIFIER;
            let _ = compiler.push_scope(name_scope);
        }
    }

    // Mirror FunctionCompiler logic for HAS_FUNCTION_SCOPE
    let contains_direct_eval = expr.contains_direct_eval();
    if contains_direct_eval || !expr.scopes().function_scope().all_bindings_local() {
        compiler.code_block_flags |= CodeBlockFlags::HAS_FUNCTION_SCOPE;
    } else {
        compiler.code_block_flags.set(
            CodeBlockFlags::HAS_FUNCTION_SCOPE,
            expr.scopes().requires_function_scope(),
        );
    }

    if compiler.code_block_flags.has_function_scope() {
        let _ = compiler.push_scope(expr.scopes().function_scope());
    } else {
        compiler.variable_scope = expr.scopes().function_scope().clone();
        compiler.lexical_scope = expr.scopes().function_scope().clone();
    }
    // ... rest of constructor compilation
}

Test cases

// Bug 1: arguments binding slot
class A {
  constructor() {
    const x = 1;
  }
}
new A();

// Bug 2: class constructor with non-local bindings (closure)
var B = class {
  constructor(components) {
    const { getHasher } = components;
    this.fn = () => getHasher("sha2-256");
  }
};
new B({ getHasher: (x) => x });

How it was found

The panic was triggered by esbuild-bundled code with --target=es2020, which
transforms class fields into __publicField() helper calls. The combination
of multiple __publicField calls followed by const declarations in a class
constructor exposed both bugs simultaneously.

acsses added 3 commits April 27, 2026 22:32
…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
@acsses acsses requested a review from a team as a code owner April 28, 2026 02:44
@github-actions github-actions Bot added the Waiting On Review Waiting on reviews from the maintainers label Apr 28, 2026
@github-actions github-actions Bot added this to the v1.0.0 milestone Apr 28, 2026
@github-actions github-actions Bot added C-VM Issues and PRs related to the Boa Virtual Machine. C-AST Issue surrounding the abstract syntax tree labels Apr 28, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 1, 2026

Test262 conformance changes

Test result main count PR count difference
Total 53,125 53,125 0
Passed 51,051 51,049 -2
Ignored 1,482 1,482 0
Failed 592 594 +2
Panics 0 7 +7
Conformance 96.10% 96.09% -0.00%
New panics (7):
test/staging/sm/class/superElemDelete.js (previously Passed)
test/staging/sm/expressions/destructuring-array-default-simple.js (previously Failed)
test/staging/sm/expressions/destructuring-array-default-function.js (previously Failed)
test/staging/sm/expressions/destructuring-array-default-function-nested.js (previously Failed)
test/staging/sm/expressions/destructuring-array-default-class.js (previously Failed)
test/staging/sm/expressions/destructuring-array-default-call.js (previously Failed)
test/language/statements/class/arguments/access.js (previously Passed)

Tested main commit: 2cc4791a912eecf64d21244bd8ecad25318fbc3f
Tested PR commit: bd812f286eae0e1e41ba71d2827cfa63654b3365
Compare commits: 2cc4791...bd812f2

@codecov
Copy link
Copy Markdown

codecov Bot commented May 1, 2026

Codecov Report

❌ Patch coverage is 83.33333% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.95%. Comparing base (6ddc2b4) to head (bd812f2).
⚠️ Report is 961 commits behind head on main.

Files with missing lines Patch % Lines
core/engine/src/bytecompiler/class.rs 78.57% 3 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

@zhuzhu81998 zhuzhu81998 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the test cases you posted are not actually panicking on main?

@hansl
Copy link
Copy Markdown
Contributor

hansl commented May 6, 2026

And the PR adds panics that weren't there. This is obvious AI slop.

@hansl hansl closed this May 6, 2026
@github-actions github-actions Bot removed the Waiting On Review Waiting on reviews from the maintainers label May 6, 2026
@acsses
Copy link
Copy Markdown
Author

acsses commented May 7, 2026

No It's actually happened with:

Enviroment

rustc 1.94.1 (e408947bf 2026-03-25)
cargo 1.94.1 (29ea6fb6a 2026-03-24)
Default host: aarch64-apple-darwin
rustup home:  /Users/<my-name>/.rustup

installed toolchains
--------------------
stable-aarch64-apple-darwin (active, default)

active toolchain
----------------
name: stable-aarch64-apple-darwin
active because: it's the default toolchain
installed targets:
  aarch64-apple-darwin
  aarch64-apple-ios
  aarch64-apple-ios-sim
  x86_64-apple-ios

Code

Cargo.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.rs

use 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.rs

use 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.rs

use 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.rs

use 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.rs

use 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.rs

use 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");
}

@acsses
Copy link
Copy Markdown
Author

acsses commented May 7, 2026

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 RUST_BACKTRACE=1 cargo run to see the panic.

Environment:

  • rustc 1.94.1 (e408947bf 2026-03-25)
  • aarch64-apple-darwin
  • boa_engine 0.21.1

@zhuzhu81998
Copy link
Copy Markdown
Contributor

And here's a self-contained reproduction using only boa_engine 0.21.1:

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

C-AST Issue surrounding the abstract syntax tree C-VM Issues and PRs related to the Boa Virtual Machine.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants