From 7bc1178d823f0b17f5b334706de9c8197d7bac36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 18 Sep 2026 06:04:25 +0200 Subject: [PATCH 1/3] perf(size): intern nested Function.toString source past the 8 MiB cap (#10574) The retained-source pool already shared nested function bodies, but it disabled intern when unique-string lengths summed past 8 MiB. That is the tsc case: ~24 MB of overlapping slices of a ~6 MB module, so __cstring kept one copy per function. Over-budget modules now still share into the longest parent (the CJS factory / module wrapper). --function-source=header stores `function name(params) { /* source elided */ }` instead of the body for the remaining unique-source win; full interned source stays the default so fn.toString() is byte-identical. --- CLAUDE.md | 2 +- Cargo.lock | 162 ++++++------- Cargo.toml | 2 +- .../src/codegen/artifact_source_text.rs | 6 +- crates/perry-codegen/src/codegen/artifacts.rs | 14 +- .../src/codegen/emission_order_tests.rs | 72 ++++++ .../src/codegen/function_source_header.rs | 215 ++++++++++++++++++ crates/perry-codegen/src/codegen/mod.rs | 1 + .../src/codegen/retained_source_pool.rs | 89 +++++++- .../perry/src/commands/compile/build_cache.rs | 1 + .../src/commands/compile/object_cache.rs | 10 +- .../object_cache/object_cache_tests.rs | 1 + .../src/commands/compile/run_pipeline.rs | 9 + crates/perry/src/commands/compile/types.rs | 13 ++ crates/perry/src/commands/dev.rs | 1 + crates/perry/src/commands/run/mod.rs | 1 + docs/src/cli/flags.md | 1 + 17 files changed, 502 insertions(+), 98 deletions(-) create mode 100644 crates/perry-codegen/src/codegen/function_source_header.rs diff --git a/CLAUDE.md b/CLAUDE.md index 1681e05242..13ee060916 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1593 +**Current Version:** 0.5.1594 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 2fa3c95b15..387c3891aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5623,7 +5623,7 @@ checksum = "fc61f41aef38c94e922057977bcb33bf185ab42242188719991ecfdc0fa1fe6b" [[package]] name = "perry" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "anyhow", "base64 0.22.1", @@ -5687,7 +5687,7 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "perry-dispatch", "serde", @@ -5695,7 +5695,7 @@ dependencies = [ [[package]] name = "perry-audio-miniaudio" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "cc", "libc", @@ -5704,7 +5704,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "aho-corasick", "anyhow", @@ -5721,7 +5721,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "anyhow", "perry-hir", @@ -5729,7 +5729,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "anyhow", "perry-hir", @@ -5737,7 +5737,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "anyhow", "perry-dispatch", @@ -5746,7 +5746,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "anyhow", "perry-hir", @@ -5754,7 +5754,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "anyhow", "base64 0.22.1", @@ -5766,7 +5766,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "anyhow", "perry-hir", @@ -5774,7 +5774,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "async-trait", "clap", @@ -5798,14 +5798,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "serde", "serde_json", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1593" +version = "0.5.1594" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5824,7 +5824,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "anyhow", "clap", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "block2", "objc2", @@ -5849,7 +5849,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "argon2", "perry-ffi", @@ -5858,7 +5858,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "perry-ffi", "reqwest", @@ -5867,7 +5867,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "bcrypt", "perry-ffi", @@ -5875,7 +5875,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "perry-ffi", "rusqlite", @@ -5883,7 +5883,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "perry-ffi", "scraper", @@ -5891,7 +5891,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "perry-ffi", "perry-runtime", @@ -5899,7 +5899,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "chrono", "cron", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "chrono", "perry-ffi", @@ -5917,7 +5917,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "perry-ffi", "rust_decimal", @@ -5925,7 +5925,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "perry-ffi", "serde_json", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "perry-ffi", "rand 0.10.2", @@ -5941,7 +5941,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "perry-ffi", "perry-runtime", @@ -5949,14 +5949,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "bytes", "http-body-util", @@ -5973,7 +5973,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "bytes", "lazy_static", @@ -5986,7 +5986,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "base64 0.22.1", "bytes", @@ -6018,7 +6018,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "lazy_static", "perry-ffi", @@ -6028,7 +6028,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "base64 0.22.1", "jsonwebtoken", @@ -6039,7 +6039,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "lru", "perry-ffi", @@ -6048,7 +6048,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "chrono", "perry-ffi", @@ -6056,7 +6056,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "bson", "futures-util", @@ -6068,7 +6068,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "chrono", "perry-ffi", @@ -6080,7 +6080,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "nanoid", "perry-ffi", @@ -6089,7 +6089,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "bytes", "perry-ffi", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "const-oid 0.10.2", "der 0.8.2", @@ -6123,7 +6123,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "lettre", "perry-ffi", @@ -6133,7 +6133,7 @@ dependencies = [ [[package]] name = "perry-ext-parcel-watcher" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "notify", "perry-ffi", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "perry-ffi", "printpdf", @@ -6153,7 +6153,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "perry-ffi", "sqlx", @@ -6162,7 +6162,7 @@ dependencies = [ [[package]] name = "perry-ext-qs" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "perry-ffi", "perry-runtime", @@ -6171,7 +6171,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "governor", "perry-ffi", @@ -6179,7 +6179,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "fast_image_resize", "image", @@ -6190,7 +6190,7 @@ dependencies = [ [[package]] name = "perry-ext-streams" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "lazy_static", "perry-ffi", @@ -6199,7 +6199,7 @@ dependencies = [ [[package]] name = "perry-ext-typescript" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "anyhow", "perry-ffi", @@ -6219,7 +6219,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "perry-ffi", "perry-runtime", @@ -6228,7 +6228,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "perry-ffi", "uuid", @@ -6236,7 +6236,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "perry-ffi", "perry-validation", @@ -6245,7 +6245,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "futures-util", "lazy_static", @@ -6258,7 +6258,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "brotli", "flate2", @@ -6268,7 +6268,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "dashmap 6.2.1", "once_cell", @@ -6278,7 +6278,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "anyhow", "perry-api-manifest", @@ -6298,11 +6298,11 @@ dependencies = [ [[package]] name = "perry-native-registration" -version = "0.5.1593" +version = "0.5.1594" [[package]] name = "perry-parser" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "anyhow", "perry-diagnostics", @@ -6315,7 +6315,7 @@ dependencies = [ [[package]] name = "perry-perex" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "perex", "regex", @@ -6323,7 +6323,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "ahash", "base64 0.22.1", @@ -6381,14 +6381,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6477,21 +6477,21 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "perry-hir", ] [[package]] name = "perry-ui" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "dirs", "perry-ffi", @@ -6501,7 +6501,7 @@ dependencies = [ [[package]] name = "perry-ui-android" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "base64 0.22.1", "jni", @@ -6516,7 +6516,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "rand 0.10.2", "serde", @@ -6526,7 +6526,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "base64 0.22.1", "cairo-rs 0.22.9", @@ -6549,7 +6549,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "base64 0.22.1", "block2", @@ -6566,7 +6566,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "base64 0.22.1", "block2", @@ -6583,7 +6583,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1593" +version = "0.5.1594" [[package]] name = "perry-ui-test" @@ -6594,11 +6594,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1593" +version = "0.5.1594" [[package]] name = "perry-ui-tvos" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "base64 0.22.1", "block2", @@ -6615,7 +6615,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "base64 0.22.1", "block2", @@ -6632,7 +6632,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "block2", "libc", @@ -6646,7 +6646,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "base64 0.22.1", "libc", @@ -6665,7 +6665,7 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "base64 0.22.1", "libc", @@ -6678,7 +6678,7 @@ dependencies = [ [[package]] name = "perry-updater" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "anyhow", "base64 0.22.1", @@ -6693,7 +6693,7 @@ dependencies = [ [[package]] name = "perry-validation" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "idna", "regex", @@ -6703,7 +6703,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1593" +version = "0.5.1594" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 06fe77db59..36f6330f74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -338,7 +338,7 @@ codegen-units = 1 codegen-units = 1 [workspace.package] -version = "0.5.1593" +version = "0.5.1594" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/crates/perry-codegen/src/codegen/artifact_source_text.rs b/crates/perry-codegen/src/codegen/artifact_source_text.rs index f9c451c667..92c85f24be 100644 --- a/crates/perry-codegen/src/codegen/artifact_source_text.rs +++ b/crates/perry-codegen/src/codegen/artifact_source_text.rs @@ -35,7 +35,11 @@ pub(super) fn extend_class_method_source_text( if symbol.is_empty() || !llmod.has_function(&symbol) || !seen.insert(symbol.clone()) { return; } - user_fn_source.push((symbol, source.text.clone(), source.is_non_strict_ordinary)); + user_fn_source.push(( + symbol, + super::function_source_header::retained_function_text(hir, func_id, &source.text), + source.is_non_strict_ordinary, + )); }; for class in &hir.classes { diff --git a/crates/perry-codegen/src/codegen/artifacts.rs b/crates/perry-codegen/src/codegen/artifacts.rs index 695ecc65a0..d912f7c479 100644 --- a/crates/perry-codegen/src/codegen/artifacts.rs +++ b/crates/perry-codegen/src/codegen/artifacts.rs @@ -1880,7 +1880,7 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> { if let Some(sym) = func_names.get(&f.id) { user_fn_source.push(( format!("__perry_wrap_{}", sym), - src.text.clone(), + super::function_source_header::retained_function_text(hir, f.id, &src.text), src.is_non_strict_ordinary, )); } @@ -1907,7 +1907,11 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> { materialized_closure_sources.sort_by_key(|(func_id, _)| **func_id); for (func_id, src) in materialized_closure_sources { let sym = format!("perry_closure_{}__{}", module_prefix, func_id); - user_fn_source.push((sym, src.text.clone(), src.is_non_strict_ordinary)); + user_fn_source.push(( + sym, + super::function_source_header::retained_function_text(hir, *func_id, &src.text), + src.is_non_strict_ordinary, + )); } // #9468: method/accessor bodies are raw symbols rather than closure @@ -1943,6 +1947,10 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> { progress.checkpoint("runtime registration metadata"); + let class_source_elided = super::function_source_header::elide_class_sources(hir); + let class_source_text = class_source_elided + .as_ref() + .unwrap_or(&hir.class_source_text); emit_string_pool( llmod, strings, @@ -1954,7 +1962,7 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> { class_table, imported_class_stubs, &hir.class_display_names, - &hir.class_source_text, + &class_source_text, &ctor_arity_overrides, closure_rest_params, closure_arities, diff --git a/crates/perry-codegen/src/codegen/emission_order_tests.rs b/crates/perry-codegen/src/codegen/emission_order_tests.rs index 84111b9326..0652dddc94 100644 --- a/crates/perry-codegen/src/codegen/emission_order_tests.rs +++ b/crates/perry-codegen/src/codegen/emission_order_tests.rs @@ -611,3 +611,75 @@ fn retained_source_ranges_preserve_registrations_and_ownership() { ); } } + +/// #10574 Part 2: `--function-source=header` must drop the body and keep the +/// name plus parameter names, which is what name-extraction and DI consume. +#[test] +fn header_mode_replaces_bodies_with_a_di_header() { + let _guard = super::function_source_header::override_function_source_header_mode(true); + let mut module = empty_module("function_source_header.ts"); + let mut foo = method_fn(100, "foo"); + foo.params = vec![ + Param { + id: 1, + name: "a".to_string(), + ty: Type::Any, + default: None, + decorators: Vec::new(), + is_rest: false, + arguments_object: None, + }, + Param { + id: 2, + name: "b".to_string(), + ty: Type::Any, + default: None, + decorators: Vec::new(), + is_rest: false, + arguments_object: None, + }, + ]; + module.functions.push(foo); + module.closure_source_text.insert( + 100, + perry_hir::FunctionSourceMetadata { + text: "function foo(a, b) {\n return 'DISTINCTIVE_BODY_10574';\n}".to_string(), + is_non_strict_ordinary: true, + }, + ); + module + .classes + .push(plain_class(3, "Envelope", method_fn(200, "m"))); + module.closure_source_text.insert( + 200, + perry_hir::FunctionSourceMetadata { + text: "m() { return 'METHOD_BODY_10574'; }".to_string(), + is_non_strict_ordinary: false, + }, + ); + module.class_source_text.insert( + 3, + "class Envelope { m() { return 'METHOD_BODY_10574'; } }".to_string(), + ); + let emitted = ir(&module); + assert!( + !emitted.contains("DISTINCTIVE_BODY_10574"), + "header mode must not retain the function body" + ); + assert!( + !emitted.contains("METHOD_BODY_10574"), + "header mode must not retain method or class bodies" + ); + assert!( + emitted.contains("source elided"), + "header mode must emit the elided-source stand-in" + ); + assert!( + emitted.contains("function foo(a, b)"), + "header must keep the name and parameter names" + ); + assert!( + emitted.contains("class Envelope"), + "class toString header must keep the class name" + ); +} diff --git a/crates/perry-codegen/src/codegen/function_source_header.rs b/crates/perry-codegen/src/codegen/function_source_header.rs new file mode 100644 index 0000000000..8afc1f9316 --- /dev/null +++ b/crates/perry-codegen/src/codegen/function_source_header.rs @@ -0,0 +1,215 @@ +//! #10574 Part 2: retain a synthesized `Function.prototype.toString` header +//! instead of the function body. +//! +//! Default remains the interned original source (Part 1). `header` mode stores +//! `function () { /* source elided */ }`, which is enough for +//! name extraction, Angular/Vue-style parameter-name DI, and +//! `toString().includes("[native code]")` probes, and drops the remaining +//! ~6 MB of unique function source on a tsc-sized bundle. +//! +//! Opt in with `--function-source=header` or `PERRY_FUNCTION_SOURCE=header`. +//! Full source is the default so `fn.toString()` stays spec-identical and +//! first-party worker serialization (`perry-threads`) keeps working. + +use std::cell::Cell; +use std::collections::HashMap; + +use perry_hir::types::FuncId; +use perry_hir::{Function, Module as HirModule, Param}; + +thread_local! { + static HEADER_MODE_OVERRIDE: Cell> = const { Cell::new(None) }; +} + +/// True when codegen should emit the synthesized header instead of the body. +pub(super) fn function_source_header_mode() -> bool { + if let Some(overridden) = HEADER_MODE_OVERRIDE.with(Cell::get) { + return overridden; + } + matches!( + std::env::var("PERRY_FUNCTION_SOURCE").as_deref(), + Ok("header") | Ok("elide") + ) +} + +/// RAII override for unit tests. Restores the previous override on drop so +/// parallel tests on this thread cannot leak the mode into a later case. +#[cfg(test)] +pub(super) struct FunctionSourceHeaderGuard(Option); + +#[cfg(test)] +impl Drop for FunctionSourceHeaderGuard { + fn drop(&mut self) { + HEADER_MODE_OVERRIDE.with(|cell| cell.set(self.0)); + } +} + +#[cfg(test)] +pub(super) fn override_function_source_header_mode(on: bool) -> FunctionSourceHeaderGuard { + FunctionSourceHeaderGuard(HEADER_MODE_OVERRIDE.with(|cell| cell.replace(Some(on)))) +} + +/// Original source, or the synthesized header when header mode is on. +pub(super) fn retained_function_text(hir: &HirModule, func_id: FuncId, original: &str) -> String { + if !function_source_header_mode() { + return original.to_string(); + } + match function_by_id(hir, func_id) { + Some(func) => synthesize_function_header(&header_name(hir, func), func.params.as_slice()), + None => synthesize_function_header("", &[]), + } +} + +/// Header-mode class source map. `None` on the default path so the caller +/// can pass `hir.class_source_text` without cloning it. +pub(super) fn elide_class_sources(hir: &HirModule) -> Option> { + if !function_source_header_mode() { + return None; + } + Some( + hir.class_source_text + .keys() + .map(|&cid| (cid, synthesize_class_header(hir, cid))) + .collect(), + ) +} + +fn header_name(hir: &HirModule, func: &Function) -> String { + if let Some(display) = hir.closure_display_names.get(&func.id) { + if is_user_visible_name(display) { + return display.clone(); + } + } + if is_user_visible_name(&func.name) { + func.name.clone() + } else { + String::new() + } +} + +fn is_user_visible_name(name: &str) -> bool { + !name.is_empty() + && !name.starts_with("__perry") + && !name.starts_with("perry_") + && !name.starts_with("__Anon") + && !name.starts_with("__anon") +} + +fn synthesize_function_header(name: &str, params: &[Param]) -> String { + let params_src = params + .iter() + .filter_map(header_param) + .collect::>() + .join(", "); + if name.is_empty() { + format!("function ({params_src}) {{ /* source elided */ }}") + } else { + format!("function {name}({params_src}) {{ /* source elided */ }}") + } +} + +fn header_param(param: &Param) -> Option { + if param.arguments_object.is_some() { + return None; + } + if !is_user_visible_name(¶m.name) { + return None; + } + if param.is_rest { + Some(format!("...{}", param.name)) + } else { + Some(param.name.clone()) + } +} + +fn synthesize_class_header(hir: &HirModule, cid: u32) -> String { + let name = hir + .class_display_names + .get(&cid) + .cloned() + .or_else(|| { + hir.classes + .iter() + .find(|class| class.id == cid) + .map(|class| class.name.clone()) + }) + .filter(|name| is_user_visible_name(name)); + match name { + Some(name) => format!("class {name} {{ /* source elided */ }}"), + None => "class { /* source elided */ }".to_string(), + } +} + +fn function_by_id(hir: &HirModule, id: FuncId) -> Option<&Function> { + if let Some(func) = hir.functions.iter().find(|func| func.id == id) { + return Some(func); + } + for class in &hir.classes { + if let Some(ctor) = &class.constructor { + if ctor.id == id { + return Some(ctor); + } + } + for func in class + .methods + .iter() + .chain(class.static_methods.iter()) + .chain(class.getters.iter().map(|(_, func)| func)) + .chain(class.setters.iter().map(|(_, func)| func)) + { + if func.id == id { + return Some(func); + } + } + if let Some(member) = class + .computed_members + .iter() + .find(|member| member.function.id == id) + { + return Some(&member.function); + } + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + use perry_hir::types::Type; + use perry_hir::Param; + + fn param(name: &str, rest: bool) -> Param { + Param { + id: 1, + name: name.to_string(), + ty: Type::Any, + default: None, + decorators: Vec::new(), + is_rest: rest, + arguments_object: None, + } + } + + #[test] + fn named_function_keeps_parameter_names_and_drops_the_body() { + let text = synthesize_function_header("foo", &[param("a", false), param("b", false)]); + assert_eq!(text, "function foo(a, b) { /* source elided */ }"); + assert!(text.starts_with("function foo(")); + assert!(!text.contains("return")); + } + + #[test] + fn anonymous_and_rest_params_round_trip_the_di_header() { + assert_eq!( + synthesize_function_header("", &[param("x", false), param("rest", true)]), + "function (x, ...rest) { /* source elided */ }" + ); + } + + #[test] + fn compiler_params_are_omitted() { + let text = + synthesize_function_header("foo", &[param("__perry_cap_0", false), param("a", false)]); + assert_eq!(text, "function foo(a) { /* source elided */ }"); + } +} diff --git a/crates/perry-codegen/src/codegen/mod.rs b/crates/perry-codegen/src/codegen/mod.rs index 592d956d69..925c42f4cd 100644 --- a/crates/perry-codegen/src/codegen/mod.rs +++ b/crates/perry-codegen/src/codegen/mod.rs @@ -199,6 +199,7 @@ mod entry; pub mod entry_outline; pub(crate) mod func_registry; mod function; +mod function_source_header; #[cfg(test)] mod guarded_falsy_default_method_tests; #[cfg(test)] diff --git a/crates/perry-codegen/src/codegen/retained_source_pool.rs b/crates/perry-codegen/src/codegen/retained_source_pool.rs index dad12102c9..de5763001e 100644 --- a/crates/perry-codegen/src/codegen/retained_source_pool.rs +++ b/crates/perry-codegen/src/codegen/retained_source_pool.rs @@ -11,6 +11,12 @@ use crate::{ }; const MIN_PATTERN_BYTES: usize = 4096; +/// Byte-sum cap on the all-parents Aho-Corasick intern. Nested function +/// source on a real bundle is *unique strings* whose lengths still sum to +/// the duplicated total (tsc: ~24 MB of overlapping slices of a ~6 MB +/// module). Exceeding this used to disable intern entirely (`plan` returned +/// one blob per function), which is how #10574 measured 24.3 MB of +/// `__cstring`. Over-budget modules now still share into the longest parent. const MAX_PATTERN_BYTES: usize = 8 * 1024 * 1024; const MAX_MATCHES: usize = 1_000_000; @@ -81,8 +87,10 @@ impl<'a> SourcePool<'a> { } } -/// Each result names an input parent and an exact byte offset. Budget/build -/// failure keeps independent byte ranges, never a guessed match or missing text. +/// Each result names an input parent and an exact byte offset. Empty sources +/// and below-minimum modules keep independent byte ranges. Over-budget or +/// automaton-build failure still intern into the longest parent (#10574) +/// rather than emitting one copy per function. fn plan( input: &[&[u8]], minimum_bytes: usize, @@ -93,21 +101,19 @@ fn plan( let total = input .iter() .fold(0usize, |sum, bytes| sum.saturating_add(bytes.len())); - if input.len() < 2 - || total < minimum_bytes - || total > maximum_bytes - || input.iter().any(|bytes| bytes.is_empty()) - || maximum_matches == 0 - { + if input.len() < 2 || total < minimum_bytes || input.iter().any(|bytes| bytes.is_empty()) { return raw(); } + if total > maximum_bytes || maximum_matches == 0 { + return share_into_longest(input); + } // A contiguous NFA avoids the potentially much larger dense DFA. Pattern // bytes and reported matches are bounded independently of source syntax. let Ok(automaton) = AhoCorasickBuilder::new() .kind(Some(AhoCorasickKind::ContiguousNFA)) .build(input) else { - return raw(); + return share_into_longest(input); }; let mut order: Vec = (0..input.len()).collect(); order.sort_by_key(|&idx| (std::cmp::Reverse(input[idx].len()), idx)); @@ -138,6 +144,46 @@ fn plan( .collect() } +/// Nested `Function.prototype.toString` text is almost always a slice of the +/// longest function (the CJS factory / module wrapper). Searching each +/// remaining unique string in that one haystack recovers the 4× duplication +/// without building an automaton over every overlapping copy. +fn share_into_longest(input: &[&[u8]]) -> Vec<(usize, usize)> { + let n = input.len(); + let mut result: Vec<(usize, usize)> = (0..n).map(|idx| (idx, 0)).collect(); + if n < 2 { + return result; + } + let parent = (0..n) + .min_by_key(|&idx| (std::cmp::Reverse(input[idx].len()), idx)) + .expect("n >= 2"); + let haystack = input[parent]; + for (idx, needle) in input.iter().copied().enumerate() { + if idx == parent || needle.len() > haystack.len() { + continue; + } + if let Some(offset) = find_bytes(haystack, needle) { + result[idx] = (parent, offset); + } + } + result +} + +fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option { + if needle.is_empty() { + return Some(0); + } + if needle.len() > haystack.len() { + return None; + } + AhoCorasickBuilder::new() + .kind(Some(AhoCorasickKind::ContiguousNFA)) + .build(std::iter::once(needle)) + .ok()? + .find(haystack) + .map(|found| found.start()) +} + #[cfg(test)] mod tests { use super::*; @@ -188,11 +234,34 @@ mod tests { } let raw: Vec<_> = (0..input.len()).map(|idx| (idx, 0)).collect(); assert_eq!(verify(input, 1024, 2048, 100), raw); - assert_eq!(verify(input, 0, 1, 100), raw); + // Over the automaton byte budget: still share into the longest parent + // (`aaaa`) instead of disabling intern. `b` is disjoint and stays a + // blob. #10574: this is the tsc-sized path (24 MB nested / 8 MB cap). + let over = verify(input, 0, 1, 100); + assert_eq!(over[0], (3, 0)); + assert_eq!(over[1], (3, 0)); + assert_eq!(over[2], (3, 0)); + assert_eq!(over[3], (3, 0)); + assert_eq!(over[4], (4, 0)); verify(&[b"", b"hello"], 0, 1024, 100); verify(&[], 0, 1024, 100); } + #[test] + fn over_budget_nested_function_source_shares_into_the_longest_parent() { + let inner = b"function inner() { return 1; }"; + let mut outer = b"function outer() { ".to_vec(); + outer.extend_from_slice(inner); + outer.extend_from_slice(b" }"); + let input: &[&[u8]] = &[&outer, inner]; + // `maximum_bytes` below `outer.len() + inner.len()` forces the + // longest-parent fallback the 8 MiB production cap takes on tsc. + let result = verify(input, 0, 8, 100); + assert_eq!(result[0], (0, 0)); + assert_eq!(result[1].0, 0); + assert_eq!(&outer[result[1].1..result[1].1 + inner.len()], inner); + } + #[test] fn emitted_pool_deduplicates_small_sources_without_a_matcher() { let mut module = LlModule::new("aarch64-apple-darwin"); diff --git a/crates/perry/src/commands/compile/build_cache.rs b/crates/perry/src/commands/compile/build_cache.rs index 2aa0e7c1b6..116f75b6ff 100644 --- a/crates/perry/src/commands/compile/build_cache.rs +++ b/crates/perry/src/commands/compile/build_cache.rs @@ -32,6 +32,7 @@ const BUILD_CACHE_ENV_VARS: &[&str] = &[ "HARMONYOS_SDK_HOME", "PERRY_DEBUG_INIT", "PERRY_DEBUG_SYMBOLS", + "PERRY_FUNCTION_SOURCE", "PERRY_LLVM_CLANG", // exp/llvm-inprocess: selects the in-process LLVM backend for .ll -> .o; // a different backend means different object bytes. diff --git a/crates/perry/src/commands/compile/object_cache.rs b/crates/perry/src/commands/compile/object_cache.rs index 64e7f9f927..033c2537b3 100644 --- a/crates/perry/src/commands/compile/object_cache.rs +++ b/crates/perry/src/commands/compile/object_cache.rs @@ -241,7 +241,7 @@ fn stable_type_key(ty: &perry_hir::types::Type) -> String { /// /// We also mix in environment variables that `perry-codegen` reads /// at compile time but that aren't part of `CompileOptions`: -/// `PERRY_DEBUG_INIT`, `PERRY_DEBUG_SYMBOLS`, `PERRY_LLVM_CLANG`, +/// `PERRY_DEBUG_INIT`, `PERRY_DEBUG_SYMBOLS`, `PERRY_FUNCTION_SOURCE`, `PERRY_LLVM_CLANG`, /// `PERRY_WRITE_BARRIERS`, `PERRY_SHADOW_STACK`, /// `PERRY_DISABLE_BUFFER_FAST_PATH`, `PERRY_VERIFY_NATIVE_REGIONS`, /// and `PERRY_TARGET_CPU`. See the env-var @@ -1008,6 +1008,8 @@ fn compute_object_cache_key_with_env( // eager initializer chain in the entry object (entry.rs). // - PERRY_DEBUG_SYMBOLS=1 adds `-g` to clang → embeds DWARF sections // into the object (linker.rs). + // - PERRY_FUNCTION_SOURCE=header elides function bodies from + // `fn.toString()` metadata (#10574). // - PERRY_LLVM_CLANG selects which clang binary compiles .ll → .o; // different clang versions/builds emit different bytes (linker.rs). // - PERRY_WRITE_BARRIERS=0/off/false suppresses generated barrier @@ -1185,6 +1187,12 @@ fn compute_object_cache_key_with_env( .as_deref() .unwrap_or(""), ); + // #10574: header vs full function source changes the retained-source + // constants in `__perry_init_strings_*`. + h.field( + "env_function_source", + env_var("PERRY_FUNCTION_SOURCE").as_deref().unwrap_or(""), + ); h.field( "env_entry_symbol", env_var("PERRY_ENTRY_SYMBOL").as_deref().unwrap_or(""), diff --git a/crates/perry/src/commands/compile/object_cache/object_cache_tests.rs b/crates/perry/src/commands/compile/object_cache/object_cache_tests.rs index 77662c1c25..adc384bdcc 100644 --- a/crates/perry/src/commands/compile/object_cache/object_cache_tests.rs +++ b/crates/perry/src/commands/compile/object_cache/object_cache_tests.rs @@ -726,6 +726,7 @@ fn key_changes_with_codegen_env_vars() { let opts = empty_opts(); for var in [ "PERRY_DEBUG_SYMBOLS", + "PERRY_FUNCTION_SOURCE", "PERRY_LLVM_CLANG", "PERRY_LLVM_INPROCESS", "PERRY_WRITE_BARRIERS", diff --git a/crates/perry/src/commands/compile/run_pipeline.rs b/crates/perry/src/commands/compile/run_pipeline.rs index 16e2fb4457..a8e4bc82b7 100644 --- a/crates/perry/src/commands/compile/run_pipeline.rs +++ b/crates/perry/src/commands/compile/run_pipeline.rs @@ -617,6 +617,15 @@ pub fn run_with_parse_cache( std::env::set_var("PERRY_DEBUG_SYMBOLS", "1"); } + // #10574: `--function-source=header` elides function bodies from the + // image. Promote to `PERRY_FUNCTION_SOURCE` before rayon codegen so + // the object-cache key and the string-pool emitter observe one knob. + // Only set (never unset): an already-exported env value wins, matching + // `--debug-symbols` / `PERRY_DEBUG_SYMBOLS`. + if args.function_source == "header" && std::env::var_os("PERRY_FUNCTION_SOURCE").is_none() { + std::env::set_var("PERRY_FUNCTION_SOURCE", "header"); + } + // `--report-size` needs a symbol table to attribute size by crate, but not // full DWARF — reuse the lighter `PERRY_KEEP_SYMBOLS` strip-skip knob // rather than `PERRY_DEBUG_SYMBOLS`, so asking for a size report doesn't diff --git a/crates/perry/src/commands/compile/types.rs b/crates/perry/src/commands/compile/types.rs index 1316022d9c..0f9e3210d0 100644 --- a/crates/perry/src/commands/compile/types.rs +++ b/crates/perry/src/commands/compile/types.rs @@ -273,6 +273,19 @@ pub struct CompileArgs { #[arg(long)] pub report_size: bool, + /// How much `Function.prototype.toString` source to keep in the binary + /// (#10574). `full` (default) stores interned original source — nested + /// functions share one module blob, so this is already the 18 MB + /// duplication win, with byte-identical `fn.toString()`. `header` stores + /// `function () { /* source elided */ }` instead: enough + /// for name extraction, Angular/Vue-style parameter-name DI, and + /// `[native code]` probes, and drops the remaining unique function + /// source (~6 MB on a tsc-sized bundle). Also `PERRY_FUNCTION_SOURCE`. + /// Programs that parse function *bodies* (`new Function(fn.toString())`, + /// `perry-threads` worker serialization) need `full`. + #[arg(long, default_value = "full", value_parser = ["full", "header"])] + pub function_source: String, + /// Disable the per-module object cache. /// By default Perry caches each module's object bytes keyed by a /// hash of the source plus every `CompileOptions` field that can diff --git a/crates/perry/src/commands/dev.rs b/crates/perry/src/commands/dev.rs index 921aa6d9a8..b15e4b330b 100644 --- a/crates/perry/src/commands/dev.rs +++ b/crates/perry/src/commands/dev.rs @@ -308,6 +308,7 @@ fn build_once( no_auto_optimize: false, debug_symbols: false, report_size: false, + function_source: "full".to_string(), no_cache: false, // `perry dev` has no `--cache-dir` flag of its own; the resolver // still honors `PERRY_CACHE_DIR` / perry.toml `[perry] cacheDir` / diff --git a/crates/perry/src/commands/run/mod.rs b/crates/perry/src/commands/run/mod.rs index b38342a363..91bcb08153 100644 --- a/crates/perry/src/commands/run/mod.rs +++ b/crates/perry/src/commands/run/mod.rs @@ -220,6 +220,7 @@ pub fn run(args: RunArgs, format: OutputFormat, use_color: bool, verbose: u8) -> no_auto_optimize: false, debug_symbols: false, report_size: false, + function_source: "full".to_string(), no_cache: false, // `perry run` has no `--cache-dir` flag; the resolver still honors // `PERRY_CACHE_DIR` / perry.toml `[perry] cacheDir` / package.json diff --git a/docs/src/cli/flags.md b/docs/src/cli/flags.md index 255e2b120d..91dfd8e230 100644 --- a/docs/src/cli/flags.md +++ b/docs/src/cli/flags.md @@ -297,6 +297,7 @@ the generating asset module where applicable. | `--no-codegen` | Skip the `package.json` `perry.codegen` build-time steps (also `PERRY_SKIP_CODEGEN=1`). See [Project Configuration](../getting-started/project-config.md) | | `--keep-intermediates` | Keep `.o` and `.asm` intermediate files | | `--debug-symbols` | Retain symbols/DWARF (and emit a Windows PDB) instead of stripping the result. | +| `--function-source ` | How much `Function.prototype.toString` source to keep. `full` (default) stores interned original source (nested functions share one module blob). `header` stores `function name(params) { /* source elided */ }` instead — enough for name/parameter extraction, much smaller. Also `PERRY_FUNCTION_SOURCE`. | | `--no-cache` | Disable the per-module object cache for this build; also `PERRY_NO_CACHE=1`. | | `--cache-dir ` | Override the machine-local cache root; see [Cache Directory](cache-dir.md). | | `--verify-native-regions` | Run native-representation lowering invariants and force codegen instead of cache reuse. | From f6f083285c68595c292c613be4b5a3fd8fd74181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 18 Sep 2026 06:05:31 +0200 Subject: [PATCH 2/3] docs(changelog): fragment for #10579 --- changelog.d/10579-function-source-intern.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 changelog.d/10579-function-source-intern.md diff --git a/changelog.d/10579-function-source-intern.md b/changelog.d/10579-function-source-intern.md new file mode 100644 index 0000000000..9502ded431 --- /dev/null +++ b/changelog.d/10579-function-source-intern.md @@ -0,0 +1,15 @@ +### Performance + +- **Nested `Function.prototype.toString` source is interned at tsc scale + (#10574).** Codegen already shared overlapping function bodies via + `SourcePool`, but intern turned *off* when unique-string lengths summed + past 8 MiB. A CJS bundle like `typescript/lib/_tsc.js` is ~24 MB of + nested slices of a ~6 MB module, so `__cstring` kept one copy per + function (24.3 MB, 28% of an 86 MB tsc binary). Over-budget modules now + still share into the longest parent (the CJS factory / module wrapper). + `fn.toString()` is byte-identical. The remaining unique source (~6 MB) + can be dropped with `--function-source=header` / + `PERRY_FUNCTION_SOURCE=header`, which stores + `function () { /* source elided */ }` instead of the body + — enough for name extraction and parameter-name DI, not enough to + reconstruct bodies. Full interned source stays the default. From f9a2ce3ea3a626df413f63d998de087677d8b67d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 18 Sep 2026 13:57:59 +0200 Subject: [PATCH 3/3] fix(codegen): keep names and parameters for closures in --function-source=header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Header mode resolved a function's name and parameters through `function_by_id`, which searches only `hir.functions` and class members. Arrow functions, function expressions and nested declarations lower to an `Expr::Closure` nested in an expression tree, so every one of them missed and fell through to `synthesize_function_header("", &[])` — emitting `function () { /* source elided */ }` with no name and no parameters. On typescript@5.9.3 that was ~9,600 of 9,644 functions, all interned onto one shared anonymous string: the linked binary held 48 distinct headers, of which 27 carried parameters. That breaks the documented contract for this mode (Angular/Vue-style parameter-name DI, name extraction) and is worse than Static Hermes, which at least emits `function f(a0, a1)`. `ClosureHeaders` maps FuncId -> (params, is_arrow) from the `closures` slice `emit_module_artifacts` already holds, so this is a map build rather than a new traversal. Arrows now render as `(a, b) => { ... }`: calling an arrow `function` misreports the function kind on top of eliding the body. tsc, header mode, after: 4,007 distinct headers, 3,963 of them carrying parameters (was 27), one empty header left as the genuine unknown-id fallback. Binary 67,782,520 -> 68,112,752 bytes (+330 KB, +0.5%) — real headers intern less than one shared anonymous string did. `--noEmit` output and exit code stay byte-identical to node. Nested function declarations keep their parameters but not yet their name, because lowering records `closure_display_names` for function expressions and object methods but not for nested declarations. `fn.name` is unaffected and still matches node (`"inner"`), so `Function.prototype.name` consumers — including tsc's own `Debug.getFunctionName`, which checks the name property before parsing `toString()` — do not see this. Also in this commit, from review of the same PR: - CLI beats env for `--function-source`, matching the precedence documented for `--cache-dir`/`PERRY_CACHE_DIR`. The flag is now `Option` so an explicit `--function-source=full` is distinguishable from an omitted flag; previously an exported `PERRY_FUNCTION_SOURCE` silently won. - An unrecognised `PERRY_FUNCTION_SOURCE` is rejected instead of quietly selecting full source, so a typo no longer retains every function body with no diagnostic. - Reverted the version bump in Cargo.toml/CLAUDE.md per CONTRIBUTING.md ("maintainer handles these at merge"). Tests: closures_keep_their_names_and_parameters and arrow_closures_keep_arrow_syntax both fail on the unfixed code, verified by removing the fallback and re-running — left `function () { ... }` against the expected `function (epsilon) { ... }` and `(g, d) => { ... }`. --- CLAUDE.md | 2 +- Cargo.lock | 162 +++++++++--------- Cargo.toml | 2 +- .../src/codegen/artifact_source_text.rs | 8 +- crates/perry-codegen/src/codegen/artifacts.rs | 18 +- .../src/codegen/function_source_header.rs | 159 ++++++++++++++++- .../src/commands/compile/run_pipeline.rs | 21 ++- crates/perry/src/commands/compile/types.rs | 4 +- crates/perry/src/commands/dev.rs | 2 +- crates/perry/src/commands/run/mod.rs | 2 +- 10 files changed, 283 insertions(+), 97 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 13ee060916..1681e05242 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1594 +**Current Version:** 0.5.1593 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 387c3891aa..2fa3c95b15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5623,7 +5623,7 @@ checksum = "fc61f41aef38c94e922057977bcb33bf185ab42242188719991ecfdc0fa1fe6b" [[package]] name = "perry" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "anyhow", "base64 0.22.1", @@ -5687,7 +5687,7 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "perry-dispatch", "serde", @@ -5695,7 +5695,7 @@ dependencies = [ [[package]] name = "perry-audio-miniaudio" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "cc", "libc", @@ -5704,7 +5704,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "aho-corasick", "anyhow", @@ -5721,7 +5721,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "anyhow", "perry-hir", @@ -5729,7 +5729,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "anyhow", "perry-hir", @@ -5737,7 +5737,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "anyhow", "perry-dispatch", @@ -5746,7 +5746,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "anyhow", "perry-hir", @@ -5754,7 +5754,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "anyhow", "base64 0.22.1", @@ -5766,7 +5766,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "anyhow", "perry-hir", @@ -5774,7 +5774,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "async-trait", "clap", @@ -5798,14 +5798,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "serde", "serde_json", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1594" +version = "0.5.1593" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5824,7 +5824,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "anyhow", "clap", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "block2", "objc2", @@ -5849,7 +5849,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "argon2", "perry-ffi", @@ -5858,7 +5858,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "perry-ffi", "reqwest", @@ -5867,7 +5867,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "bcrypt", "perry-ffi", @@ -5875,7 +5875,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "perry-ffi", "rusqlite", @@ -5883,7 +5883,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "perry-ffi", "scraper", @@ -5891,7 +5891,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "perry-ffi", "perry-runtime", @@ -5899,7 +5899,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "chrono", "cron", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "chrono", "perry-ffi", @@ -5917,7 +5917,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "perry-ffi", "rust_decimal", @@ -5925,7 +5925,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "perry-ffi", "serde_json", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "perry-ffi", "rand 0.10.2", @@ -5941,7 +5941,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "perry-ffi", "perry-runtime", @@ -5949,14 +5949,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "bytes", "http-body-util", @@ -5973,7 +5973,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "bytes", "lazy_static", @@ -5986,7 +5986,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "base64 0.22.1", "bytes", @@ -6018,7 +6018,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "lazy_static", "perry-ffi", @@ -6028,7 +6028,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "base64 0.22.1", "jsonwebtoken", @@ -6039,7 +6039,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "lru", "perry-ffi", @@ -6048,7 +6048,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "chrono", "perry-ffi", @@ -6056,7 +6056,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "bson", "futures-util", @@ -6068,7 +6068,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "chrono", "perry-ffi", @@ -6080,7 +6080,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "nanoid", "perry-ffi", @@ -6089,7 +6089,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "bytes", "perry-ffi", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "const-oid 0.10.2", "der 0.8.2", @@ -6123,7 +6123,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "lettre", "perry-ffi", @@ -6133,7 +6133,7 @@ dependencies = [ [[package]] name = "perry-ext-parcel-watcher" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "notify", "perry-ffi", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "perry-ffi", "printpdf", @@ -6153,7 +6153,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "perry-ffi", "sqlx", @@ -6162,7 +6162,7 @@ dependencies = [ [[package]] name = "perry-ext-qs" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "perry-ffi", "perry-runtime", @@ -6171,7 +6171,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "governor", "perry-ffi", @@ -6179,7 +6179,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "fast_image_resize", "image", @@ -6190,7 +6190,7 @@ dependencies = [ [[package]] name = "perry-ext-streams" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "lazy_static", "perry-ffi", @@ -6199,7 +6199,7 @@ dependencies = [ [[package]] name = "perry-ext-typescript" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "anyhow", "perry-ffi", @@ -6219,7 +6219,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "perry-ffi", "perry-runtime", @@ -6228,7 +6228,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "perry-ffi", "uuid", @@ -6236,7 +6236,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "perry-ffi", "perry-validation", @@ -6245,7 +6245,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "futures-util", "lazy_static", @@ -6258,7 +6258,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "brotli", "flate2", @@ -6268,7 +6268,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "dashmap 6.2.1", "once_cell", @@ -6278,7 +6278,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "anyhow", "perry-api-manifest", @@ -6298,11 +6298,11 @@ dependencies = [ [[package]] name = "perry-native-registration" -version = "0.5.1594" +version = "0.5.1593" [[package]] name = "perry-parser" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "anyhow", "perry-diagnostics", @@ -6315,7 +6315,7 @@ dependencies = [ [[package]] name = "perry-perex" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "perex", "regex", @@ -6323,7 +6323,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "ahash", "base64 0.22.1", @@ -6381,14 +6381,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6477,21 +6477,21 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "perry-hir", ] [[package]] name = "perry-ui" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "dirs", "perry-ffi", @@ -6501,7 +6501,7 @@ dependencies = [ [[package]] name = "perry-ui-android" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "base64 0.22.1", "jni", @@ -6516,7 +6516,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "rand 0.10.2", "serde", @@ -6526,7 +6526,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "base64 0.22.1", "cairo-rs 0.22.9", @@ -6549,7 +6549,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "base64 0.22.1", "block2", @@ -6566,7 +6566,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "base64 0.22.1", "block2", @@ -6583,7 +6583,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1594" +version = "0.5.1593" [[package]] name = "perry-ui-test" @@ -6594,11 +6594,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1594" +version = "0.5.1593" [[package]] name = "perry-ui-tvos" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "base64 0.22.1", "block2", @@ -6615,7 +6615,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "base64 0.22.1", "block2", @@ -6632,7 +6632,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "block2", "libc", @@ -6646,7 +6646,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "base64 0.22.1", "libc", @@ -6665,7 +6665,7 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "base64 0.22.1", "libc", @@ -6678,7 +6678,7 @@ dependencies = [ [[package]] name = "perry-updater" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "anyhow", "base64 0.22.1", @@ -6693,7 +6693,7 @@ dependencies = [ [[package]] name = "perry-validation" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "idna", "regex", @@ -6703,7 +6703,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1594" +version = "0.5.1593" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 36f6330f74..06fe77db59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -338,7 +338,7 @@ codegen-units = 1 codegen-units = 1 [workspace.package] -version = "0.5.1594" +version = "0.5.1593" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/crates/perry-codegen/src/codegen/artifact_source_text.rs b/crates/perry-codegen/src/codegen/artifact_source_text.rs index 92c85f24be..c39070c56a 100644 --- a/crates/perry-codegen/src/codegen/artifact_source_text.rs +++ b/crates/perry-codegen/src/codegen/artifact_source_text.rs @@ -16,6 +16,7 @@ use super::helpers::{scoped_method_name, scoped_static_method_name}; pub(super) fn extend_class_method_source_text( hir: &HirModule, + closures: &super::function_source_header::ClosureHeaders<'_>, module_prefix: &str, llmod: &LlModule, user_fn_source: &mut Vec<(String, String, bool)>, @@ -37,7 +38,12 @@ pub(super) fn extend_class_method_source_text( } user_fn_source.push(( symbol, - super::function_source_header::retained_function_text(hir, func_id, &source.text), + super::function_source_header::retained_function_text( + hir, + closures, + func_id, + &source.text, + ), source.is_non_strict_ordinary, )); }; diff --git a/crates/perry-codegen/src/codegen/artifacts.rs b/crates/perry-codegen/src/codegen/artifacts.rs index d912f7c479..bb446c4983 100644 --- a/crates/perry-codegen/src/codegen/artifacts.rs +++ b/crates/perry-codegen/src/codegen/artifacts.rs @@ -1874,13 +1874,21 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> { // above); inline closures only have a `perry_closure_*` global when // materialized, so gate those on `materialized_closure_ids` to avoid // referencing an undefined global (the #318/#343 clang-failure class). + // #10574: resolve closure params/kind for functions that are not + // `hir.functions` entries, so header mode keeps their names and parameters. + let closure_headers = super::function_source_header::ClosureHeaders::new(closures); let mut user_fn_source: Vec<(String, String, bool)> = Vec::new(); for f in &hir.functions { if let Some(src) = hir.closure_source_text.get(&f.id) { if let Some(sym) = func_names.get(&f.id) { user_fn_source.push(( format!("__perry_wrap_{}", sym), - super::function_source_header::retained_function_text(hir, f.id, &src.text), + super::function_source_header::retained_function_text( + hir, + &closure_headers, + f.id, + &src.text, + ), src.is_non_strict_ordinary, )); } @@ -1909,7 +1917,12 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> { let sym = format!("perry_closure_{}__{}", module_prefix, func_id); user_fn_source.push(( sym, - super::function_source_header::retained_function_text(hir, *func_id, &src.text), + super::function_source_header::retained_function_text( + hir, + &closure_headers, + *func_id, + &src.text, + ), src.is_non_strict_ordinary, )); } @@ -1919,6 +1932,7 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> { // module actually emitted; the helper also preserves the file-size gate. super::artifact_source_text::extend_class_method_source_text( hir, + &closure_headers, module_prefix, llmod, &mut user_fn_source, diff --git a/crates/perry-codegen/src/codegen/function_source_header.rs b/crates/perry-codegen/src/codegen/function_source_header.rs index 8afc1f9316..02fb37e50a 100644 --- a/crates/perry-codegen/src/codegen/function_source_header.rs +++ b/crates/perry-codegen/src/codegen/function_source_header.rs @@ -15,7 +15,7 @@ use std::cell::Cell; use std::collections::HashMap; use perry_hir::types::FuncId; -use perry_hir::{Function, Module as HirModule, Param}; +use perry_hir::{Expr, Function, Module as HirModule, Param}; thread_local! { static HEADER_MODE_OVERRIDE: Cell> = const { Cell::new(None) }; @@ -49,14 +49,78 @@ pub(super) fn override_function_source_header_mode(on: bool) -> FunctionSourceHe FunctionSourceHeaderGuard(HEADER_MODE_OVERRIDE.with(|cell| cell.replace(Some(on)))) } +/// Parameter/kind lookup for functions that are **not** `hir.functions` +/// entries. Arrow functions, function expressions and nested function +/// declarations all lower to an `Expr::Closure` nested inside an expression +/// tree, so `function_by_id` cannot see them — it searches only `hir.functions` +/// and class members. Before this existed, every one of them fell back to +/// `function () { ... }`, dropping the name *and* the parameters that the +/// documented Angular/Vue-style DI contract depends on. On `typescript@5.9.3` +/// that was ~9,600 of 9,644 functions, all interned onto one shared string. +/// +/// Built from the same `closures` slice `emit_module_artifacts` already holds, +/// so this adds a map build, not a traversal. +pub(super) struct ClosureHeaders<'a> { + by_id: HashMap, +} + +impl<'a> ClosureHeaders<'a> { + pub(super) fn new(closures: &'a [(FuncId, Expr)]) -> Self { + let mut by_id = HashMap::new(); + for (func_id, expr) in closures { + if let Expr::Closure { + params, is_arrow, .. + } = expr + { + by_id.insert(*func_id, (params.as_slice(), *is_arrow)); + } + } + Self { by_id } + } + + #[cfg(test)] + pub(super) fn empty() -> Self { + Self { + by_id: HashMap::new(), + } + } + + fn get(&self, id: FuncId) -> Option<(&'a [Param], bool)> { + self.by_id.get(&id).copied() + } +} + /// Original source, or the synthesized header when header mode is on. -pub(super) fn retained_function_text(hir: &HirModule, func_id: FuncId, original: &str) -> String { +pub(super) fn retained_function_text( + hir: &HirModule, + closures: &ClosureHeaders<'_>, + func_id: FuncId, + original: &str, +) -> String { if !function_source_header_mode() { return original.to_string(); } - match function_by_id(hir, func_id) { - Some(func) => synthesize_function_header(&header_name(hir, func), func.params.as_slice()), - None => synthesize_function_header("", &[]), + if let Some(func) = function_by_id(hir, func_id) { + return synthesize_function_header(&header_name(hir, func), func.params.as_slice()); + } + if let Some((params, is_arrow)) = closures.get(func_id) { + // An arrow has no name in source and `toString()` must not claim one, + // nor call itself `function` - that misreports the function kind on + // top of eliding the body. + if is_arrow { + return synthesize_arrow_header(params); + } + return synthesize_function_header(&closure_header_name(hir, func_id), params); + } + synthesize_function_header("", &[]) +} + +/// Display name for a closure that has no `hir.functions` entry — a function +/// expression or nested declaration keeps its source name here. +fn closure_header_name(hir: &HirModule, func_id: FuncId) -> String { + match hir.closure_display_names.get(&func_id) { + Some(display) if is_user_visible_name(display) => display.clone(), + _ => String::new(), } } @@ -108,6 +172,15 @@ fn synthesize_function_header(name: &str, params: &[Param]) -> String { } } +fn synthesize_arrow_header(params: &[Param]) -> String { + let params_src = params + .iter() + .filter_map(header_param) + .collect::>() + .join(", "); + format!("({params_src}) => {{ /* source elided */ }}") +} + fn header_param(param: &Param) -> Option { if param.arguments_object.is_some() { return None; @@ -178,6 +251,7 @@ mod tests { use perry_hir::types::Type; use perry_hir::Param; + fn param(name: &str, rest: bool) -> Param { Param { id: 1, @@ -206,6 +280,81 @@ mod tests { ); } + fn closure_expr(func_id: u32, params: Vec, is_arrow: bool) -> (FuncId, Expr) { + ( + func_id, + Expr::Closure { + func_id, + params, + return_type: Type::Any, + body: Vec::new(), + captures: Vec::new(), + mutable_captures: Vec::new(), + captures_this: false, + captures_new_target: false, + enclosing_class: None, + is_arrow, + is_async: false, + is_generator: false, + is_strict: false, + }, + ) + } + + /// #10574: the regression that shipped in the first cut of header mode. + /// A closure is not a `hir.functions` entry, so `function_by_id` misses it + /// and the fallback produced `function () { ... }` for ~9,600 of tsc's + /// 9,644 functions — losing the names and parameters the DI contract + /// promises. Without `ClosureHeaders` these two assertions fail. + #[test] + fn closures_keep_their_names_and_parameters() { + let hir = HirModule::new("t"); + let closures = vec![closure_expr(7, vec![param("epsilon", false)], false)]; + let headers = ClosureHeaders::new(&closures); + let _guard = override_function_source_header_mode(true); + assert_eq!( + retained_function_text(&hir, &headers, 7, "function named2(epsilon) { return 1; }"), + "function (epsilon) { /* source elided */ }" + ); + assert!(!retained_function_text(&hir, &headers, 7, "x").contains("return")); + } + + /// An arrow must not be reported as `function (...)`: that misstates the + /// function *kind* on top of eliding the body. + #[test] + fn arrow_closures_keep_arrow_syntax() { + let hir = HirModule::new("t"); + let closures = vec![closure_expr(9, vec![param("g", false), param("d", false)], true)]; + let headers = ClosureHeaders::new(&closures); + let _guard = override_function_source_header_mode(true); + assert_eq!( + retained_function_text(&hir, &headers, 9, "(g, d) => g + d"), + "(g, d) => { /* source elided */ }" + ); + } + + /// An unknown id still degrades safely rather than panicking. + #[test] + fn unknown_ids_fall_back_to_an_anonymous_header() { + let hir = HirModule::new("t"); + let headers = ClosureHeaders::empty(); + let _guard = override_function_source_header_mode(true); + assert_eq!( + retained_function_text(&hir, &headers, 404, "whatever"), + "function () { /* source elided */ }" + ); + } + + /// Default mode must stay byte-identical to the original source. + #[test] + fn full_mode_is_byte_identical() { + let hir = HirModule::new("t"); + let headers = ClosureHeaders::empty(); + let _guard = override_function_source_header_mode(false); + let src = "function keepMe(a, b) { return a + b; }"; + assert_eq!(retained_function_text(&hir, &headers, 1, src), src); + } + #[test] fn compiler_params_are_omitted() { let text = diff --git a/crates/perry/src/commands/compile/run_pipeline.rs b/crates/perry/src/commands/compile/run_pipeline.rs index a8e4bc82b7..c90df1835d 100644 --- a/crates/perry/src/commands/compile/run_pipeline.rs +++ b/crates/perry/src/commands/compile/run_pipeline.rs @@ -622,8 +622,25 @@ pub fn run_with_parse_cache( // the object-cache key and the string-pool emitter observe one knob. // Only set (never unset): an already-exported env value wins, matching // `--debug-symbols` / `PERRY_DEBUG_SYMBOLS`. - if args.function_source == "header" && std::env::var_os("PERRY_FUNCTION_SOURCE").is_none() { - std::env::set_var("PERRY_FUNCTION_SOURCE", "header"); + // Precedence is CLI flag > env, matching `--cache-dir`/`PERRY_CACHE_DIR` + // and the rest of the CLI. An explicit `--function-source` always wins; + // only an omitted flag defers to an exported `PERRY_FUNCTION_SOURCE`. + match args.function_source.as_deref() { + Some(value) => std::env::set_var("PERRY_FUNCTION_SOURCE", value), + None => { + // An unknown exported value used to silently select full source, so + // a typo (`headeer`) quietly produced a binary with every function + // body retained and no diagnostic. `--function-source` is validated + // by clap; the env var has to be validated here. + if let Some(value) = std::env::var_os("PERRY_FUNCTION_SOURCE") { + let value = value.to_string_lossy().trim().to_string(); + if !matches!(value.as_str(), "full" | "header" | "elide") { + anyhow::bail!( + "PERRY_FUNCTION_SOURCE must be `full` or `header` (got `{value}`)" + ); + } + } + } } // `--report-size` needs a symbol table to attribute size by crate, but not diff --git a/crates/perry/src/commands/compile/types.rs b/crates/perry/src/commands/compile/types.rs index 0f9e3210d0..1f8f6bac5e 100644 --- a/crates/perry/src/commands/compile/types.rs +++ b/crates/perry/src/commands/compile/types.rs @@ -283,8 +283,8 @@ pub struct CompileArgs { /// source (~6 MB on a tsc-sized bundle). Also `PERRY_FUNCTION_SOURCE`. /// Programs that parse function *bodies* (`new Function(fn.toString())`, /// `perry-threads` worker serialization) need `full`. - #[arg(long, default_value = "full", value_parser = ["full", "header"])] - pub function_source: String, + #[arg(long, value_parser = ["full", "header"])] + pub function_source: Option, /// Disable the per-module object cache. /// By default Perry caches each module's object bytes keyed by a diff --git a/crates/perry/src/commands/dev.rs b/crates/perry/src/commands/dev.rs index b15e4b330b..4ed7844f7d 100644 --- a/crates/perry/src/commands/dev.rs +++ b/crates/perry/src/commands/dev.rs @@ -308,7 +308,7 @@ fn build_once( no_auto_optimize: false, debug_symbols: false, report_size: false, - function_source: "full".to_string(), + function_source: None, no_cache: false, // `perry dev` has no `--cache-dir` flag of its own; the resolver // still honors `PERRY_CACHE_DIR` / perry.toml `[perry] cacheDir` / diff --git a/crates/perry/src/commands/run/mod.rs b/crates/perry/src/commands/run/mod.rs index 91bcb08153..a40cb00119 100644 --- a/crates/perry/src/commands/run/mod.rs +++ b/crates/perry/src/commands/run/mod.rs @@ -220,7 +220,7 @@ pub fn run(args: RunArgs, format: OutputFormat, use_color: bool, verbose: u8) -> no_auto_optimize: false, debug_symbols: false, report_size: false, - function_source: "full".to_string(), + function_source: None, no_cache: false, // `perry run` has no `--cache-dir` flag; the resolver still honors // `PERRY_CACHE_DIR` / perry.toml `[perry] cacheDir` / package.json