diff --git a/.isu/issues.json b/.isu/issues.json index 88c7a34..1b3f32d 100644 --- a/.isu/issues.json +++ b/.isu/issues.json @@ -4947,7 +4947,7 @@ ], "assigned": [], "author": "piefev", - "state": "open", + "state": "closed", "created_at": "2026-07-18T08:05:58Z" } ] diff --git a/crates/js/src/builtins.rs b/crates/js/src/builtins.rs index 0e0f70e..9d785d8 100644 --- a/crates/js/src/builtins.rs +++ b/crates/js/src/builtins.rs @@ -7472,13 +7472,20 @@ fn init_console_object(vm: &mut Vm) { ]; for &(name, callback) in methods { let func = make_native(&mut vm.gc, name, callback); - set_builtin_prop( - &mut vm.gc, - &mut vm.shapes, - console_ref, - name, - Value::Function(func), - ); + if let Some(HeapObject::Object(data)) = vm.gc.get_mut(console_ref) { + data.insert_property( + name.to_string(), + Property { + value: Value::Function(func), + getter: None, + setter: None, + writable: true, + enumerable: true, + configurable: true, + }, + &mut vm.shapes, + ); + } } vm.set_global("console", Value::Object(console_ref)); diff --git a/crates/js/src/compiler.rs b/crates/js/src/compiler.rs index bb5a78b..ef457de 100644 --- a/crates/js/src/compiler.rs +++ b/crates/js/src/compiler.rs @@ -4206,10 +4206,15 @@ fn compile_expr(fc: &mut FunctionCompiler, expr: &Expr, dst: Reg) -> Result<(), let func_reg = fc.alloc_reg(); compile_expr(fc, callee, func_reg)?; + let this_ni = fc.builder.add_name("this"); if arguments_have_spread(arguments) { let arr_reg = fc.alloc_reg(); compile_spread_arguments(fc, arguments, arr_reg)?; + let saved_this_reg = fc.alloc_reg(); + fc.builder.emit_load_global(saved_this_reg, this_ni); fc.builder.emit_new_call_spread(dst, func_reg, arr_reg); + fc.builder.emit_store_global(this_ni, saved_this_reg); + fc.free_reg(saved_this_reg); fc.free_reg(arr_reg); } else { let args_start = fc.next_reg; @@ -4219,8 +4224,12 @@ fn compile_expr(fc: &mut FunctionCompiler, expr: &Expr, dst: Reg) -> Result<(), compile_expr(fc, arg, arg_reg)?; } + let saved_this_reg = fc.alloc_reg(); + fc.builder.emit_load_global(saved_this_reg, this_ni); fc.builder .emit_new_call(dst, func_reg, args_start, arg_count); + fc.builder.emit_store_global(this_ni, saved_this_reg); + fc.free_reg(saved_this_reg); for _ in 0..arg_count { fc.next_reg -= 1; @@ -4423,9 +4432,26 @@ fn compile_expr(fc: &mut FunctionCompiler, expr: &Expr, dst: Reg) -> Result<(), } ExprKind::Function(func_def) => { - let inner = compile_function_body_with_captures(fc, func_def)?; - let func_idx = fc.builder.add_function(inner); - fc.builder.emit_reg_u16(Op::CreateClosure, dst, func_idx); + if let Some(name) = func_def + .id + .as_deref() + .filter(|name| !name.is_empty()) + .filter(|name| collect_free_vars(&func_def.params, &func_def.body).contains(*name)) + { + let saved_locals = fc.locals.len(); + let self_reg = fc.define_local_ext(name, true, false); + fc.builder.emit_reg(Op::NewCell, self_reg); + let inner = compile_function_body_with_captures(fc, func_def)?; + let func_idx = fc.builder.add_function(inner); + fc.builder.emit_reg_u16(Op::CreateClosure, dst, func_idx); + fc.builder.emit_reg_reg(Op::CellStore, self_reg, dst); + fc.locals.truncate(saved_locals); + fc.free_reg(self_reg); + } else { + let inner = compile_function_body_with_captures(fc, func_def)?; + let func_idx = fc.builder.add_function(inner); + fc.builder.emit_reg_u16(Op::CreateClosure, dst, func_idx); + } } ExprKind::Arrow { diff --git a/crates/js/src/vm.rs b/crates/js/src/vm.rs index 2f617ec..34d7daa 100644 --- a/crates/js/src/vm.rs +++ b/crates/js/src/vm.rs @@ -14212,6 +14212,14 @@ mod tests { assert!(matches!(result, Value::Undefined)); } + #[test] + fn test_console_methods_are_enumerable() { + match eval("Object.keys(console).indexOf('log') >= 0").unwrap() { + Value::Boolean(found) => assert!(found, "console.log should be enumerable"), + v => panic!("expected boolean, got {v:?}"), + } + } + #[test] fn test_console_log_no_args() { let (_, console) = eval_with_console("console.log()"); diff --git a/crates/js/tests/class_members.rs b/crates/js/tests/class_members.rs index 883b244..775aa4a 100644 --- a/crates/js/tests/class_members.rs +++ b/crates/js/tests/class_members.rs @@ -34,6 +34,23 @@ fn instance_field_initializes_in_constructor() { assert_eq!(out, "15"); } +#[test] +fn nested_new_preserves_enclosing_constructor_this() { + let out = evaluate( + "class Inner { constructor() { this.kind = 'inner'; } } \ + class Outer { \ + constructor() { \ + this.config = { enabled: true }; \ + this.inner = new Inner(); \ + this.after = this.config.enabled; \ + } \ + } \ + new Outer().after;", + ) + .unwrap(); + assert_eq!(out, "true"); +} + #[test] fn instance_field_without_initializer_is_undefined_own_property() { let out = evaluate("class C { y; } var c = new C(); ('y' in c) + ',' + (c.y);").unwrap(); diff --git a/crates/js/tests/function_expressions.rs b/crates/js/tests/function_expressions.rs new file mode 100644 index 0000000..0d1c3be --- /dev/null +++ b/crates/js/tests/function_expressions.rs @@ -0,0 +1,45 @@ +//! Regression tests for function expression semantics. + +use we_js::evaluate; + +#[test] +fn named_function_expression_can_reference_itself() { + assert_eq!( + evaluate( + "var f = function self(n) { \ + return n <= 1 ? 1 : n * self(n - 1); \ + }; \ + f(4);" + ) + .unwrap(), + "24" + ); +} + +#[test] +fn named_function_expression_name_is_private() { + assert_eq!( + evaluate( + "var f = function self() { return typeof self; }; \ + typeof self + ':' + f();" + ) + .unwrap(), + "undefined:function" + ); +} + +#[test] +fn named_function_expression_preserves_assigned_properties() { + assert_eq!( + evaluate( + "var make = function () { \ + return function wrapped() { return wrapped.placeholder; }; \ + }; \ + var fn = make(); \ + fn.placeholder = 'ok'; \ + fn();" + ) + .unwrap(), + "ok" + ); +} diff --git a/crates/js/tests/opera_traffic.rs b/crates/js/tests/opera_traffic.rs new file mode 100644 index 0000000..614ceb8 --- /dev/null +++ b/crates/js/tests/opera_traffic.rs @@ -0,0 +1,48 @@ +//! Regression coverage for the Opera traffic bundle used by the real-web soak. + +use we_js::evaluate; + +#[test] +fn opera_traffic_bundle_accepts_missing_initial_options() { + let bundle = include_str!( + "../../e2e/real-web/snapshots/opera.com/cdn-production-opera-website.operacdn.com/staticfiles/traffic.0f43603a9af9.js" + ); + let source = format!( + "var window = {{ + location: {{ + pathname: '/', + hostname: 'www.opera.com', + host: 'www.opera.com', + origin: 'https://www.opera.com', + search: '' + }}, + innerWidth: 1365, + addEventListener: function () {{}}, + dispatchEvent: function () {{}} + }}; + var location = window.location; + var document = {{ + readyState: 'complete', + cookie: '', + referrer: '', + addEventListener: function () {{}}, + getElementsByTagName: function () {{ return []; }}, + querySelectorAll: function () {{ return []; }} + }}; + var localStorage = {{ + verbose: false, + getItem: function () {{ return null; }}, + setItem: function () {{}}, + removeItem: function () {{}} + }}; + function CustomEvent(type, init) {{ + this.type = type; + this.detail = init && init.detail; + }} + {bundle} + window.operaTrafficService.config.isGX + ':' + + window.operaTrafficService.config.enableABTestTracking" + ); + + assert_eq!(evaluate(&source).unwrap(), "false:true"); +}