From 7a246b63c7ba6695211985758d10df86988b0294 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Thu, 21 May 2026 12:53:54 +0200 Subject: [PATCH] LibJS: Infer computed property function names Use a runtime SetFunctionName bytecode operation when object literal property keys are not known until evaluation. This lets anonymous function and class expressions, methods, and accessors receive names from numeric, computed, and Symbol property keys. Store inferred ECMAScript function names on each function object instead of mutating shared function data. That keeps repeated evaluations with different computed keys from leaking names across closures, while still using the per-instance name for stack metadata. Add regression coverage for computed object property names, repeated computed-key evaluations, and preserving unnamed functions that are only referenced by a computed property value. --- .../AsmInterpreter/AsmInterpreter.cpp | 2 + Libraries/LibJS/Bytecode/Bytecode.def | 6 ++ Libraries/LibJS/Bytecode/Instruction.h | 6 ++ Libraries/LibJS/Bytecode/Interpreter.cpp | 25 ++++++++ Libraries/LibJS/Bytecode/PropertyAccess.h | 4 +- Libraries/LibJS/Bytecode/Validator.cpp | 3 + Libraries/LibJS/BytecodeDef/src/lib.rs | 1 + Libraries/LibJS/Runtime/ClassConstruction.cpp | 35 +++-------- .../Runtime/ECMAScriptFunctionObject.cpp | 10 +++- .../LibJS/Runtime/ECMAScriptFunctionObject.h | 9 ++- Libraries/LibJS/Rust/build.rs | 4 ++ Libraries/LibJS/Rust/src/bytecode/codegen.rs | 60 ++++++++++++++++++- .../LibJS/Rust/src/bytecode/validator.rs | 10 ++++ Libraries/LibJS/Rust/src/bytecode_cache.rs | 4 +- .../expected/numeric-getter-no-lhs-name.txt | 13 ++-- .../LibJS/Runtime/functions/function-name.js | 53 ++++++++++++++++ 16 files changed, 203 insertions(+), 42 deletions(-) diff --git a/Libraries/LibJS/Bytecode/AsmInterpreter/AsmInterpreter.cpp b/Libraries/LibJS/Bytecode/AsmInterpreter/AsmInterpreter.cpp index 0a608594cb..0e7b9e51ad 100644 --- a/Libraries/LibJS/Bytecode/AsmInterpreter/AsmInterpreter.cpp +++ b/Libraries/LibJS/Bytecode/AsmInterpreter/AsmInterpreter.cpp @@ -468,6 +468,8 @@ i64 asm_fallback_handler(VM* vm, u32 pc) return execute_throwing(*vm, pc); case Instruction::Type::PutByValueWithThis: return execute_throwing(*vm, pc); + case Instruction::Type::SetFunctionName: + return execute_throwing(*vm, pc); case Instruction::Type::ResolveSuperBase: return execute_throwing(*vm, pc); case Instruction::Type::DynamicSetLexicalBinding: diff --git a/Libraries/LibJS/Bytecode/Bytecode.def b/Libraries/LibJS/Bytecode/Bytecode.def index 8b36dcaf9c..04f101e901 100644 --- a/Libraries/LibJS/Bytecode/Bytecode.def +++ b/Libraries/LibJS/Bytecode/Bytecode.def @@ -1035,6 +1035,12 @@ op SetCompletionType < Instruction m_completion_type: Completion::Type endop +op SetFunctionName < Instruction + m_function: Operand + m_name: Operand + m_prefix: FunctionNamePrefix +endop + op SetGlobal < Instruction m_identifier: IdentifierTableIndex m_src: Operand diff --git a/Libraries/LibJS/Bytecode/Instruction.h b/Libraries/LibJS/Bytecode/Instruction.h index 50e0c7fcc6..fb7a641626 100644 --- a/Libraries/LibJS/Bytecode/Instruction.h +++ b/Libraries/LibJS/Bytecode/Instruction.h @@ -59,6 +59,12 @@ enum class ArgumentsKind { Unmapped, }; +enum class FunctionNamePrefix { + None, + Get, + Set, +}; + } namespace JS::Bytecode { diff --git a/Libraries/LibJS/Bytecode/Interpreter.cpp b/Libraries/LibJS/Bytecode/Interpreter.cpp index e97614270a..3831e86cf1 100644 --- a/Libraries/LibJS/Bytecode/Interpreter.cpp +++ b/Libraries/LibJS/Bytecode/Interpreter.cpp @@ -763,6 +763,7 @@ void VM::run_bytecode(size_t entry_point) HANDLE_INSTRUCTION(ResolveThisBinding); HANDLE_INSTRUCTION(RightShift); HANDLE_INSTRUCTION_WITHOUT_EXCEPTION_CHECK(SetCompletionType); + HANDLE_INSTRUCTION(SetFunctionName); HANDLE_INSTRUCTION(SetGlobal); HANDLE_INSTRUCTION_WITHOUT_EXCEPTION_CHECK(SetLexicalEnvironment); HANDLE_INSTRUCTION(SetLexicalBinding); @@ -3222,6 +3223,30 @@ void NewFunction::execute_impl(VM& vm) const vm.set(dst(), new_function(vm, m_shared_function_data_index, m_home_object)); } +static Optional function_name_prefix_to_string(Op::FunctionNamePrefix prefix) +{ + switch (prefix) { + case Op::FunctionNamePrefix::None: + return {}; + case Op::FunctionNamePrefix::Get: + return "get"sv; + case Op::FunctionNamePrefix::Set: + return "set"sv; + } + VERIFY_NOT_REACHED(); +} + +ThrowCompletionOr SetFunctionName::execute_impl(VM& vm) const +{ + auto function = vm.get(m_function).as_if(); + if (!function || !function->name().is_empty()) + return {}; + + auto property_key = TRY(vm.get(m_name).to_property_key(vm)); + function->set_inferred_name(Variant { move(property_key) }, function_name_prefix_to_string(m_prefix)); + return {}; +} + void Return::execute_impl(VM& vm) const { vm.do_return(vm.get(m_value)); diff --git a/Libraries/LibJS/Bytecode/PropertyAccess.h b/Libraries/LibJS/Bytecode/PropertyAccess.h index 28ce064723..45f3db2c2a 100644 --- a/Libraries/LibJS/Bytecode/PropertyAccess.h +++ b/Libraries/LibJS/Bytecode/PropertyAccess.h @@ -218,14 +218,14 @@ inline ThrowCompletionOr put_by_property_key(VM& vm, Value base, Value thi case PutKind::Getter: { auto& function = value.as_function(); if (is(function) && static_cast(function).name().is_empty()) - static_cast(&function)->set_name(Utf16String::formatted("get {}", name)); + static_cast(&function)->set_inferred_name(Variant { name }, "get"sv); object->define_direct_accessor(name, &function, nullptr, Attribute::Configurable | Attribute::Enumerable); break; } case PutKind::Setter: { auto& function = value.as_function(); if (is(function) && static_cast(function).name().is_empty()) - static_cast(&function)->set_name(Utf16String::formatted("set {}", name)); + static_cast(&function)->set_inferred_name(Variant { name }, "set"sv); object->define_direct_accessor(name, nullptr, &function, Attribute::Configurable | Attribute::Enumerable); break; } diff --git a/Libraries/LibJS/Bytecode/Validator.cpp b/Libraries/LibJS/Bytecode/Validator.cpp index d209dc4a98..16f4cb6eb7 100644 --- a/Libraries/LibJS/Bytecode/Validator.cpp +++ b/Libraries/LibJS/Bytecode/Validator.cpp @@ -95,6 +95,8 @@ static constexpr u32 put_kind_variant_count = to_underlying(PutKind::Own) + 1; static_assert(put_kind_variant_count == 5); static constexpr u32 arguments_kind_variant_count = to_underlying(Op::ArgumentsKind::Unmapped) + 1; static_assert(arguments_kind_variant_count == 2); +static constexpr u32 function_name_prefix_variant_count = to_underlying(Op::FunctionNamePrefix::Set) + 1; +static_assert(function_name_prefix_variant_count == 3); ErrorOr validate_bytecode(Executable const& executable, ReadonlySpan basic_block_offsets) { @@ -122,6 +124,7 @@ ErrorOr validate_bytecode(Executable const& executable, ReadonlySpan .environment_mode_variant_count = environment_mode_variant_count, .put_kind_variant_count = put_kind_variant_count, .arguments_kind_variant_count = arguments_kind_variant_count, + .function_name_prefix_variant_count = function_name_prefix_variant_count, }; // Project Executable's exception handlers down to plain offsets; the diff --git a/Libraries/LibJS/BytecodeDef/src/lib.rs b/Libraries/LibJS/BytecodeDef/src/lib.rs index 7df2ec1c18..0c74b49e2f 100644 --- a/Libraries/LibJS/BytecodeDef/src/lib.rs +++ b/Libraries/LibJS/BytecodeDef/src/lib.rs @@ -136,6 +136,7 @@ pub fn field_type_info(ty: &str) -> FieldType { "EnvironmentMode" => ("u32", 4, 4, "u32"), "PutKind" => ("u32", 4, 4, "u32"), "ArgumentsKind" => ("u32", 4, 4, "u32"), + "FunctionNamePrefix" => ("u32", 4, 4, "u32"), "Value" => ("u64", 8, 8, "u64"), "PropertyLookupCacheIndex" | "GlobalVariableCacheIndex" diff --git a/Libraries/LibJS/Runtime/ClassConstruction.cpp b/Libraries/LibJS/Runtime/ClassConstruction.cpp index 79a21e5705..6cbe3f9f39 100644 --- a/Libraries/LibJS/Runtime/ClassConstruction.cpp +++ b/Libraries/LibJS/Runtime/ClassConstruction.cpp @@ -19,10 +19,10 @@ namespace JS { -static void update_function_name(Value value, Utf16FlyString const& name) +static void update_function_name(Value value, ClassElementName const& name, Optional const& prefix = {}) { if (auto function = value.as_if(); function && function->name().is_empty()) - function->set_name(name); + function->set_inferred_name(name, prefix); } static ThrowCompletionOr resolve_element_key(VM& vm, Bytecode::ClassElementDescriptor const& descriptor, Value property_key) @@ -42,25 +42,6 @@ static ThrowCompletionOr resolve_element_key(VM& vm, Bytecode: return ClassElementName { key }; } -static Utf16String compute_element_name(ClassElementName const& element_name, StringView prefix = {}) -{ - auto name = element_name.visit( - [&](PropertyKey const& property_key) { - if (property_key.is_symbol()) { - auto description = property_key.as_symbol()->description(); - if (!description.has_value() || description->is_empty()) - return Utf16String {}; - return Utf16String::formatted("[{}]", *description); - } - return property_key.to_string(); - }, - [&](PrivateName const& private_name) { - return private_name.description.to_utf16_string(); - }); - - return Utf16String::formatted("{}{}{}", prefix, prefix.is_empty() ? "" : " ", name); -} - ThrowCompletionOr construct_class( VM& vm, Bytecode::ClassBlueprint const& blueprint, @@ -155,19 +136,19 @@ ThrowCompletionOr construct_class( auto& property_key = element_name.get(); switch (descriptor.kind) { case Bytecode::ClassElementDescriptor::Kind::Method: { - update_function_name(method_value, compute_element_name(element_name)); + update_function_name(method_value, element_name); PropertyDescriptor property_descriptor { .value = method_value, .writable = true, .enumerable = false, .configurable = true }; TRY(home_object.define_property_or_throw(property_key, property_descriptor)); break; } case Bytecode::ClassElementDescriptor::Kind::Getter: { - update_function_name(method_value, compute_element_name(element_name, "get"sv)); + update_function_name(method_value, element_name, "get"sv); PropertyDescriptor property_descriptor { .get = &method_function, .enumerable = false, .configurable = true }; TRY(home_object.define_property_or_throw(property_key, property_descriptor)); break; } case Bytecode::ClassElementDescriptor::Kind::Setter: { - update_function_name(method_value, compute_element_name(element_name, "set"sv)); + update_function_name(method_value, element_name, "set"sv); PropertyDescriptor property_descriptor { .set = &method_function, .enumerable = false, .configurable = true }; TRY(home_object.define_property_or_throw(property_key, property_descriptor)); break; @@ -182,13 +163,13 @@ ThrowCompletionOr construct_class( PrivateElement private_element = [&] { switch (descriptor.kind) { case Bytecode::ClassElementDescriptor::Kind::Method: - update_function_name(method_value, compute_element_name(element_name)); + update_function_name(method_value, element_name); return PrivateElement { private_name, PrivateElement::Kind::Method, method_value }; case Bytecode::ClassElementDescriptor::Kind::Getter: - update_function_name(method_value, compute_element_name(element_name, "get"sv)); + update_function_name(method_value, element_name, "get"sv); return PrivateElement { private_name, PrivateElement::Kind::Accessor, Value(Accessor::create(vm, &method_function, nullptr)) }; case Bytecode::ClassElementDescriptor::Kind::Setter: - update_function_name(method_value, compute_element_name(element_name, "set"sv)); + update_function_name(method_value, element_name, "set"sv); return PrivateElement { private_name, PrivateElement::Kind::Accessor, Value(Accessor::create(vm, nullptr, &method_function)) }; default: VERIFY_NOT_REACHED(); diff --git a/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp b/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp index 544f484a97..23f774f903 100644 --- a/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp +++ b/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp @@ -145,7 +145,7 @@ void ECMAScriptFunctionObject::get_stack_frame_info(size_t& registers_and_locals VERIFY(rust_executable); m_shared_data->set_executable(rust_executable); executable = rust_executable; - executable->name = m_shared_data->m_name; + executable->name = name(); if (Bytecode::g_dump_bytecode) executable->dump(); m_shared_data->clear_compile_inputs(); @@ -533,12 +533,18 @@ ThrowCompletionOr ECMAScriptFunctionObject::ordinary_call_evaluate_body(V void ECMAScriptFunctionObject::set_name(Utf16FlyString const& name) { auto& vm = this->vm(); - const_cast(shared_data()).m_name = name; + m_name = name; m_name_string = PrimitiveString::create(vm, name); PropertyDescriptor descriptor { .value = m_name_string, .writable = false, .enumerable = false, .configurable = true }; MUST(define_property_or_throw(vm.names.name, descriptor)); } +void ECMAScriptFunctionObject::set_inferred_name(Variant const& name, Optional const& prefix) +{ + auto function_name = make_function_name(name, prefix); + set_name(Utf16FlyString(function_name->utf16_string())); +} + ECMAScriptFunctionObject::ClassData& ECMAScriptFunctionObject::ensure_class_data() const { if (!m_class_data) diff --git a/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.h b/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.h index 23d0f3575b..0b9c9b9417 100644 --- a/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.h +++ b/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.h @@ -59,8 +59,14 @@ public: virtual Utf16String name_for_call_stack() const override; - Utf16FlyString const& name() const { return shared_data().m_name; } + Utf16FlyString const& name() const + { + if (m_name.has_value()) + return *m_name; + return shared_data().m_name; + } void set_name(Utf16FlyString const& name); + void set_inferred_name(Variant const& name, Optional const& prefix = {}); void set_is_class_constructor() { const_cast(shared_data()).set_is_class_constructor(); } @@ -155,6 +161,7 @@ private: GC::Ref m_shared_data; + Optional m_name; GC::Ptr m_name_string; // Internal Slots of ECMAScript Function Objects, https://tc39.es/ecma262/#table-internal-slots-of-ecmascript-function-objects diff --git a/Libraries/LibJS/Rust/build.rs b/Libraries/LibJS/Rust/build.rs index b8f75f59a1..98aa80a5ee 100644 --- a/Libraries/LibJS/Rust/build.rs +++ b/Libraries/LibJS/Rust/build.rs @@ -258,6 +258,10 @@ fn emit_scalar_field_check( w, " validate_arguments_kind(read_u32(bytes, at + {offset}), ctx)?;" )?, + "FunctionNamePrefix" => writeln!( + w, + " validate_function_name_prefix(read_u32(bytes, at + {offset}), ctx)?;" + )?, // bool, u64, Value, EnvironmentCoordinate, Builtin: no per-field // bound applied here. _ => {} diff --git a/Libraries/LibJS/Rust/src/bytecode/codegen.rs b/Libraries/LibJS/Rust/src/bytecode/codegen.rs index 575352a256..847c9448cd 100644 --- a/Libraries/LibJS/Rust/src/bytecode/codegen.rs +++ b/Libraries/LibJS/Rust/src/bytecode/codegen.rs @@ -1256,6 +1256,13 @@ enum ArgumentsKind { Unmapped = 1, } +#[repr(u32)] +enum FunctionNamePrefix { + None = 0, + Get = 1, + Set = 2, +} + /// Class element kind (ABI-compatible with ClassBlueprint::Element::Kind). #[repr(u8)] enum ClassElementKind { @@ -5110,6 +5117,27 @@ fn emit_switch_block_declaration_instantiation(generator: &mut Generator, data: // Object expression // ============================================================================= +fn is_anonymous_function_definition(generator: &Generator, expression: &Expression) -> bool { + match &expression.inner { + ExpressionKind::Function(function_id) => generator.function_table.get(*function_id).name.is_none(), + ExpressionKind::Class(data) => data.name.is_none(), + _ => false, + } +} + +fn emit_set_function_name( + generator: &mut Generator, + function: &ScopedOperand, + name: &ScopedOperand, + prefix: FunctionNamePrefix, +) { + generator.emit(Instruction::SetFunctionName { + function: function.operand(), + name: name.operand(), + prefix: prefix as u32, + }); +} + /// Generate bytecode for an object literal expression. /// /// Objects whose shape can be determined at compile time (only simple @@ -5188,6 +5216,25 @@ fn generate_object_expression( None }; + let is_method_like = property.is_method + || property.property_type == ObjectPropertyType::Getter + || property.property_type == ObjectPropertyType::Setter; + let function_name_prefix = match property.property_type { + ObjectPropertyType::Getter => FunctionNamePrefix::Get, + ObjectPropertyType::Setter => FunctionNamePrefix::Set, + _ => FunctionNamePrefix::None, + }; + // PropertyDefinitionEvaluation, https://tc39.es/ecma262/#sec-runtime-semantics-propertydefinitionevaluation + // If IsAnonymousFunctionDefinition(|AssignmentExpression|) is *true* and _isProtoSetter_ is *false*, then + // Let _propValue_ be ? NamedEvaluation of |AssignmentExpression| with argument _propertyKey_. + let should_set_runtime_function_name = computed_key.is_some() + && property.property_type != ObjectPropertyType::ProtoSetter + && (is_method_like + || property + .value + .as_ref() + .is_some_and(|value| is_anonymous_function_definition(generator, value))); + // Set pending LHS name for function name inference on non-computed properties. // ProtoSetter (__proto__) skips NamedEvaluation per spec. if !effectively_computed && property.property_type != ObjectPropertyType::ProtoSetter { @@ -5219,9 +5266,6 @@ fn generate_object_expression( } // Methods, getters, and setters need the object as their [[HomeObject]] // so that super property lookups work. - let is_method_like = property.is_method - || property.property_type == ObjectPropertyType::Getter - || property.property_type == ObjectPropertyType::Setter; if is_method_like { generator.home_objects.push(dst.clone()); } @@ -5234,6 +5278,16 @@ fn generate_object_expression( generator.home_objects.pop(); } generator.pending_lhs_name = None; + if should_set_runtime_function_name { + emit_set_function_name( + generator, + &value, + computed_key + .as_ref() + .expect("runtime function names require a runtime property key"), + function_name_prefix, + ); + } match property.property_type { ObjectPropertyType::Spread => { diff --git a/Libraries/LibJS/Rust/src/bytecode/validator.rs b/Libraries/LibJS/Rust/src/bytecode/validator.rs index 0607f4dde6..f9a6cd1251 100644 --- a/Libraries/LibJS/Rust/src/bytecode/validator.rs +++ b/Libraries/LibJS/Rust/src/bytecode/validator.rs @@ -56,6 +56,7 @@ pub struct FFIValidatorBounds { pub environment_mode_variant_count: u32, pub put_kind_variant_count: u32, pub arguments_kind_variant_count: u32, + pub function_name_prefix_variant_count: u32, } /// Categorization of validation failures, mirrored to C++ as an enum class. @@ -362,6 +363,14 @@ pub fn validate_arguments_kind(raw: u32, ctx: &ValidationContext) -> Result<(), Ok(()) } +#[inline] +pub fn validate_function_name_prefix(raw: u32, ctx: &ValidationContext) -> Result<(), ValidationErrorKind> { + if raw >= ctx.bounds.function_name_prefix_variant_count { + return Err(ValidationErrorKind::EnumOutOfRange); + } + Ok(()) +} + /// Walk `bytes` and verify the structural integrity of every instruction. /// /// Pass 1 verifies that the buffer is a tight sequence of well-formed @@ -519,6 +528,7 @@ mod tests { environment_mode_variant_count: 2, put_kind_variant_count: 5, arguments_kind_variant_count: 2, + function_name_prefix_variant_count: 3, } } diff --git a/Libraries/LibJS/Rust/src/bytecode_cache.rs b/Libraries/LibJS/Rust/src/bytecode_cache.rs index 258d9eb754..a94ef41b0e 100644 --- a/Libraries/LibJS/Rust/src/bytecode_cache.rs +++ b/Libraries/LibJS/Rust/src/bytecode_cache.rs @@ -29,7 +29,7 @@ use crate::bytecode::validator::{ use crate::{CompiledProgram, CompiledProgramBytecode, ModuleCallbacks, ast, u32_from_usize}; const MAGIC: &[u8; 8] = b"LBJSBC\0\0"; -const FORMAT_VERSION: u32 = 11; +const FORMAT_VERSION: u32 = 12; const SOURCE_HASH_SIZE: usize = 32; const BYTECODE_ALIGNMENT: usize = 8; const COMPLETION_TYPE_VARIANT_COUNT: u32 = 6; @@ -37,6 +37,7 @@ const ITERATOR_HINT_VARIANT_COUNT: u32 = 2; const ENVIRONMENT_MODE_VARIANT_COUNT: u32 = 2; const PUT_KIND_VARIANT_COUNT: u32 = 5; const ARGUMENTS_KIND_VARIANT_COUNT: u32 = 2; +const FUNCTION_NAME_PREFIX_VARIANT_COUNT: u32 = 3; fn source_span_is_valid(start: u32, end: u32, source_len: usize) -> bool { let start = start as usize; @@ -2403,6 +2404,7 @@ impl DecodedExecutableRecord { environment_mode_variant_count: ENVIRONMENT_MODE_VARIANT_COUNT, put_kind_variant_count: PUT_KIND_VARIANT_COUNT, arguments_kind_variant_count: ARGUMENTS_KIND_VARIANT_COUNT, + function_name_prefix_variant_count: FUNCTION_NAME_PREFIX_VARIANT_COUNT, }; let exception_handlers = self diff --git a/Tests/LibJS/Bytecode/expected/numeric-getter-no-lhs-name.txt b/Tests/LibJS/Bytecode/expected/numeric-getter-no-lhs-name.txt index 380622e487..ac4e9cf274 100644 --- a/Tests/LibJS/Bytecode/expected/numeric-getter-no-lhs-name.txt +++ b/Tests/LibJS/Bytecode/expected/numeric-getter-no-lhs-name.txt @@ -1,4 +1,4 @@ -$7f6ce915 numeric-getter-no-lhs-name.js:1:1 +$87b8bc45 numeric-getter-no-lhs-name.js:1:1 Registers: 7 Blocks: 1 Constants: @@ -8,11 +8,12 @@ block0: [ 0] NewObject dst:reg5 [ 10] ToPrimitiveWithStringHint dst:Int32(0), value:Int32(0) [ 20] NewFunction dst:reg6, shared_function_data_index:0, home_object:reg5 - [ 38] PutByValue base:reg5, property:Int32(0), src:reg6, kind:Getter - [ 50] SetGlobal `obj`, src:reg5 - [ 60] GetGlobal dst:reg5, `obj` - [ 70] GetByValue dst:reg6, base:reg5, property:Int32(0) (obj[Int32(0)]) - [ 88] End value:reg6 + [ 38] SetFunctionName function:reg6, name:Int32(0) + [ 48] PutByValue base:reg5, property:Int32(0), src:reg6, kind:Getter + [ 60] SetGlobal `obj`, src:reg5 + [ 70] GetGlobal dst:reg5, `obj` + [ 80] GetByValue dst:reg6, base:reg5, property:Int32(0) (obj[Int32(0)]) + [ 98] End value:reg6 get 0$4c2eeb71 numeric-getter-no-lhs-name.js:3:9 diff --git a/Tests/LibJS/Runtime/functions/function-name.js b/Tests/LibJS/Runtime/functions/function-name.js index 9b73f5f976..825723d250 100644 --- a/Tests/LibJS/Runtime/functions/function-name.js +++ b/Tests/LibJS/Runtime/functions/function-name.js @@ -44,6 +44,59 @@ test("functions in objects", () => { expect(o.c.name).toBe(""); }); +test("computed property names infer anonymous function names", () => { + const namedSymbol = Symbol("named"); + const emptySymbol = Symbol(""); + const unnamedSymbol = Symbol(); + const getterSymbol = Symbol("getter"); + const setterSymbol = Symbol(""); + + const object = { + [1]: function () {}, + [2]: class {}, + [namedSymbol]: () => {}, + [emptySymbol]: function* () {}, + [unnamedSymbol]: async function () {}, + [/a/]: function () {}, + get [getterSymbol]() {}, + set [setterSymbol](value) {}, + }; + + expect(object[1].name).toBe("1"); + expect(object[2].name).toBe("2"); + expect(object[namedSymbol].name).toBe("[named]"); + expect(object[emptySymbol].name).toBe("[]"); + expect(object[unnamedSymbol].name).toBe(""); + expect(object[/a/].name).toBe("/a/"); + expect(Object.getOwnPropertyDescriptor(object, getterSymbol).get.name).toBe("get [getter]"); + expect(Object.getOwnPropertyDescriptor(object, setterSymbol).set.name).toBe("set []"); +}); + +test("computed property name inference does not leak between evaluations", () => { + function objectName(key) { + return { [key]: function () {} }[key].name; + } + + function classMethodName(key) { + return new (class { + [key]() {} + })()[key].name; + } + + expect(objectName("first")).toBe("first"); + expect(objectName("second")).toBe("second"); + expect(classMethodName("first")).toBe("first"); + expect(classMethodName("second")).toBe("second"); +}); + +test("computed property name inference only applies to anonymous definitions", () => { + const key = "computed"; + const value = [function () {}][0]; + const object = { [key]: value }; + + expect(object[key].name).toBe(""); +}); + test("names of native functions", () => { expect(console.debug.name).toBe("debug"); expect((console.debug.name = "warn")).toBe("warn");