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.
This commit is contained in:
Andreas Kling 2026-05-21 12:53:54 +02:00 committed by Andreas Kling
parent 5c881d1553
commit 7a246b63c7
16 changed files with 203 additions and 42 deletions

View file

@ -468,6 +468,8 @@ i64 asm_fallback_handler(VM* vm, u32 pc)
return execute_throwing<Op::PutBySpread>(*vm, pc);
case Instruction::Type::PutByValueWithThis:
return execute_throwing<Op::PutByValueWithThis>(*vm, pc);
case Instruction::Type::SetFunctionName:
return execute_throwing<Op::SetFunctionName>(*vm, pc);
case Instruction::Type::ResolveSuperBase:
return execute_throwing<Op::ResolveSuperBase>(*vm, pc);
case Instruction::Type::DynamicSetLexicalBinding:

View file

@ -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

View file

@ -59,6 +59,12 @@ enum class ArgumentsKind {
Unmapped,
};
enum class FunctionNamePrefix {
None,
Get,
Set,
};
}
namespace JS::Bytecode {

View file

@ -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<StringView> 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<void> SetFunctionName::execute_impl(VM& vm) const
{
auto function = vm.get(m_function).as_if<ECMAScriptFunctionObject>();
if (!function || !function->name().is_empty())
return {};
auto property_key = TRY(vm.get(m_name).to_property_key(vm));
function->set_inferred_name(Variant<PropertyKey, PrivateName> { 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));

View file

@ -218,14 +218,14 @@ inline ThrowCompletionOr<void> put_by_property_key(VM& vm, Value base, Value thi
case PutKind::Getter: {
auto& function = value.as_function();
if (is<ECMAScriptFunctionObject>(function) && static_cast<ECMAScriptFunctionObject const&>(function).name().is_empty())
static_cast<ECMAScriptFunctionObject*>(&function)->set_name(Utf16String::formatted("get {}", name));
static_cast<ECMAScriptFunctionObject*>(&function)->set_inferred_name(Variant<PropertyKey, PrivateName> { 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<ECMAScriptFunctionObject>(function) && static_cast<ECMAScriptFunctionObject const&>(function).name().is_empty())
static_cast<ECMAScriptFunctionObject*>(&function)->set_name(Utf16String::formatted("set {}", name));
static_cast<ECMAScriptFunctionObject*>(&function)->set_inferred_name(Variant<PropertyKey, PrivateName> { name }, "set"sv);
object->define_direct_accessor(name, nullptr, &function, Attribute::Configurable | Attribute::Enumerable);
break;
}

View file

@ -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<void> validate_bytecode(Executable const& executable, ReadonlySpan<u32> basic_block_offsets)
{
@ -122,6 +124,7 @@ ErrorOr<void> validate_bytecode(Executable const& executable, ReadonlySpan<u32>
.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

View file

@ -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"

View file

@ -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<StringView> const& prefix = {})
{
if (auto function = value.as_if<ECMAScriptFunctionObject>(); function && function->name().is_empty())
function->set_name(name);
function->set_inferred_name(name, prefix);
}
static ThrowCompletionOr<ClassElementName> resolve_element_key(VM& vm, Bytecode::ClassElementDescriptor const& descriptor, Value property_key)
@ -42,25 +42,6 @@ static ThrowCompletionOr<ClassElementName> 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<ECMAScriptFunctionObject*> construct_class(
VM& vm,
Bytecode::ClassBlueprint const& blueprint,
@ -155,19 +136,19 @@ ThrowCompletionOr<ECMAScriptFunctionObject*> construct_class(
auto& property_key = element_name.get<PropertyKey>();
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<ECMAScriptFunctionObject*> 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();

View file

@ -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<Value> ECMAScriptFunctionObject::ordinary_call_evaluate_body(V
void ECMAScriptFunctionObject::set_name(Utf16FlyString const& name)
{
auto& vm = this->vm();
const_cast<SharedFunctionInstanceData&>(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<PropertyKey, PrivateName> const& name, Optional<StringView> 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)

View file

@ -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<PropertyKey, PrivateName> const& name, Optional<StringView> const& prefix = {});
void set_is_class_constructor() { const_cast<SharedFunctionInstanceData&>(shared_data()).set_is_class_constructor(); }
@ -155,6 +161,7 @@ private:
GC::Ref<SharedFunctionInstanceData> m_shared_data;
Optional<Utf16FlyString> m_name;
GC::Ptr<PrimitiveString> m_name_string;
// Internal Slots of ECMAScript Function Objects, https://tc39.es/ecma262/#table-internal-slots-of-ecmascript-function-objects

View file

@ -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.
_ => {}

View file

@ -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 => {

View file

@ -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,
}
}

View file

@ -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

View file

@ -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

View file

@ -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");