boa icon indicating copy to clipboard operation
boa copied to clipboard

Inconsistent function vs object assignment in indirect eval

Open ThinerDAS opened this issue 9 months ago • 0 comments

Description

When executing an indirect eval call ((1,eval)) that contains both a function declaration and a subsequent variable assignment with the same name, Boa exhibits inconsistent behavior that may not conform to ECMAScript standards.

Reproduction Code

t = []

var s1 = `
function core() { t.push(1) }

core.prototype.a = function () { t.push(2) }
core.prototype.b = function () { t.push(3) }
`;
var s2 = `
function core() { t.push(1) }

core.prototype.a = function () { t.push(2) }
core.prototype.b = function () { t.push(3) }
var core = new core();
`;
var s3 = `
function core() { t.push(1) }
var core = new core();
`;

function run_ctx(s) {
    (1,eval)(s);
}

function test() {
    run_ctx(s1);
    var core1 = new core()
  
    run_ctx(s2);
    var core2 = core
  
    run_ctx(s3);
    var core3 = core
    return [core1,core2,core3]
}

test()

Expected Behavior

According to ECMAScript standards, when a function declaration is followed by a variable assignment that overwrites it with an instance of itself (e.g., var core = new core()), the final global binding should be the object instance, not the function.

All three elements in the returned array should be object instances.

And in most of my javascript environments, it is like:

"[object Object],[object Object],[object Object]"

Actual Behavior

v0.20.0

"[object Object],function () { [native code] },[object Object]"

git master

"[object Object],function () { t.push(3) },[object Object]"

The second element remains a function instead of becoming an object instance as expected.

Questions

  • Which behavior is conformant with the ECMAScript standard?
  • Is this a bug or a specification edge case?

Environment

  • Boa version: 0.20.0 and git master
  • Platform: rust 1.88

Rust source code

use boa_engine::{Context, Source, JsValue, JsError, js_string};
use std::{env, fs, path::Path, error::Error};

fn main() -> Result<(), Box<dyn Error>> {
    let args: Vec<String> = env::args().skip(1).collect();
    if args.is_empty() {
        eprintln!("Usage: {} <file1.js> [file2.js ...]", env::args().next().unwrap());
        return Ok(());
    }

    let mut context = Context::default();
    let mut last_result = None;

    for file in args {
        let js_code = fs::read_to_string(&file)?;
        match context.eval(Source::from_bytes(&js_code).with_path(Path::new(&file))) {
            Ok(result) => {
                last_result = Some(result);
            },
            Err(err) => {
                return Ok(());  // Exit on first error
            }
        }
    }

    if let Some(result) = last_result {
        let result_str = result.to_string(&mut context)?;
        println!("{:?}", result_str);
    }

    Ok(())
}

ThinerDAS avatar Jul 24 '25 07:07 ThinerDAS