From 960c27ea8a7c9c7df73e3bd4e03b6ed5989ffcdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Tue, 22 Sep 2026 18:03:58 +0200 Subject: [PATCH 1/2] fix: preserve names of interpreted functions and classes --- crates/perry-runtime/src/dyn_eval/interp.rs | 6 ++ crates/perry-runtime/src/dyn_eval/tests.rs | 15 +++++ .../issue_10676_dynamic_function_names.rs | 57 +++++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 crates/perry/tests/issue_10676_dynamic_function_names.rs diff --git a/crates/perry-runtime/src/dyn_eval/interp.rs b/crates/perry-runtime/src/dyn_eval/interp.rs index e4f4788485..543560f221 100644 --- a/crates/perry-runtime/src/dyn_eval/interp.rs +++ b/crates/perry-runtime/src/dyn_eval/interp.rs @@ -470,6 +470,9 @@ pub(crate) fn make_function_value( ctx.wasm_allowed, ); let closure_idx = root_push(closure); + let closure_ptr = crate::value::js_nanbox_get_pointer(root_get(closure_idx)) + as *mut crate::closure::ClosureHeader; + crate::object::set_bound_native_closure_name(closure_ptr, &fn_name); env::define(root_get(name_env_idx), &fn_name, root_get(closure_idx)); let closure = root_get(closure_idx); roots_truncate(name_env_idx); @@ -1199,6 +1202,9 @@ pub(crate) fn eval_class_expr(ctx: &Ctx, class_expr: &ast::ClassExpr, env_idx: u let ctor_idx = root_push(ctor_closure); if let Some(name) = &name { + let ctor_ptr = crate::value::js_nanbox_get_pointer(root_get(ctor_idx)) + as *mut crate::closure::ClosureHeader; + crate::object::set_bound_native_closure_name(ctor_ptr, name); env::define(root_get(body_env_idx), name, root_get(ctor_idx)); } diff --git a/crates/perry-runtime/src/dyn_eval/tests.rs b/crates/perry-runtime/src/dyn_eval/tests.rs index 0fe12e05da..9f436ab22d 100644 --- a/crates/perry-runtime/src/dyn_eval/tests.rs +++ b/crates/perry-runtime/src/dyn_eval/tests.rs @@ -279,6 +279,21 @@ fn named_function_expression_recursion_and_expando() { assert_eq!(as_num(calls), 1.0); } +#[test] +fn interpreted_functions_and_classes_keep_their_declared_names() { + for (source, expected) in [ + ("return function Named() {};", "Named"), + ("function Declared() {} return Declared;", "Declared"), + ("return class Widget {};", "Widget"), + ] { + let factory = dyn_fn(&[source]); + let value_idx = root_push(call(factory, &[])); + let name = bridge::get_member(root_get(value_idx), "name"); + assert_eq!(as_str(name), expected, "source: {source}"); + roots_truncate(value_idx); + } +} + #[test] fn closures_capture_interpreter_scope() { let f = dyn_fn(&[r#" diff --git a/crates/perry/tests/issue_10676_dynamic_function_names.rs b/crates/perry/tests/issue_10676_dynamic_function_names.rs new file mode 100644 index 0000000000..6847a222ec --- /dev/null +++ b/crates/perry/tests/issue_10676_dynamic_function_names.rs @@ -0,0 +1,57 @@ +//! Runtime-built Function sources keep their parsed function and class names. + +use std::path::PathBuf; +use std::process::Command; + +#[test] +fn dynamic_function_and_class_names_are_visible() { + let dir = tempfile::tempdir().expect("tempdir"); + let entry = dir.path().join("main.ts"); + let binary = dir.path().join("main_bin"); + std::fs::write( + &entry, + r#" +const literal: any = new Function("return function Literal() {}")(); +const functionParts = ["return function ", "Named", "() {}"]; +const dynamic: any = new Function(functionParts.join(""))(); +const classParts = ["return class ", "Widget", " {}"]; +const klass: any = new Function(classParts.join(""))(); +console.log("FUNCTION", literal.name, dynamic.name); +console.log("CLASS", klass.name); +console.log("CONSTRUCTOR", new Function("return 1").name); +"#, + ) + .expect("write fixture"); + + let compiler = PathBuf::from(env!("CARGO_BIN_EXE_perry")); + let compile = Command::new(compiler) + .current_dir(dir.path()) + .arg("compile") + .arg(&entry) + .arg("-o") + .arg(&binary) + .arg("--no-cache") + .output() + .expect("compile fixture"); + assert!( + compile.status.success(), + "compile failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&compile.stdout), + String::from_utf8_lossy(&compile.stderr) + ); + + let run = Command::new(binary) + .current_dir(dir.path()) + .output() + .expect("run fixture"); + assert!( + run.status.success(), + "fixture failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&run.stdout), + String::from_utf8_lossy(&run.stderr) + ); + assert_eq!( + String::from_utf8_lossy(&run.stdout), + "FUNCTION Literal Named\nCLASS Widget\nCONSTRUCTOR anonymous\n" + ); +} From 83e6ffd77a60efddc1c310f1ad4989995cba1fad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Tue, 22 Sep 2026 18:04:17 +0200 Subject: [PATCH 2/2] docs: add changelog for dynamic function names --- changelog.d/11008-dynamic-function-names.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/11008-dynamic-function-names.md diff --git a/changelog.d/11008-dynamic-function-names.md b/changelog.d/11008-dynamic-function-names.md new file mode 100644 index 0000000000..a93aa1b3d6 --- /dev/null +++ b/changelog.d/11008-dynamic-function-names.md @@ -0,0 +1 @@ +Fixed `.name` on named functions and classes returned from runtime-generated `new Function` sources, including sources assembled from strings.