From 1c134e533dd5c1b03c8ac1237b48c704af982cd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 29 Jul 2026 06:12:57 +0200 Subject: [PATCH 1/3] =?UTF-8?q?size:=20intl-namespace=20feature=20gate=20?= =?UTF-8?q?=E2=80=94=20install=20+=20instanceof/subclass=20probes=20(compi?= =?UTF-8?q?ler-detected;=20~204K=20Intl=20web=20strips=20from=20programs?= =?UTF-8?q?=20with=20no=20locale=20API)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/perry-runtime/Cargo.toml | 16 ++++++++++++- crates/perry-runtime/src/intl.rs | 11 +++++++++ .../src/object/class_constructors.rs | 5 ++++ crates/perry-runtime/src/object/instanceof.rs | 5 ++++ .../compile/collect_modules/feature_detect.rs | 24 +++++++++++++++++++ .../compile/optimized_libs/freshness.rs | 9 ++++++- crates/perry/src/commands/compile/types.rs | 6 +++++ 7 files changed, 74 insertions(+), 2 deletions(-) diff --git a/crates/perry-runtime/Cargo.toml b/crates/perry-runtime/Cargo.toml index 987bd06542..a25677f0b0 100644 --- a/crates/perry-runtime/Cargo.toml +++ b/crates/perry-runtime/Cargo.toml @@ -22,7 +22,7 @@ crate-type = ["rlib"] # actually needs (see optimized_libs.rs), so the heavy subsystems below # (regex engine, Temporal, URL/IDNA, normalize, segmenter) are *opt-in per # app* and absent from binaries that never use them. -default = ["full", "regex-engine", "temporal", "url-engine", "string-normalize", "intl-segmenter", "intl-locale", "intl-datetime", "diagnostics", "mod-dgram", "mod-http2-constants", "dyn-eval", "keepalive-anchors", "alloc-mimalloc"] +default = ["full", "regex-engine", "temporal", "url-engine", "string-normalize", "intl-segmenter", "intl-namespace", "intl-locale", "intl-datetime", "diagnostics", "mod-dgram", "mod-http2-constants", "dyn-eval", "keepalive-anchors", "alloc-mimalloc"] # Compile the ~490 `#[used]` keep-alive anchor statics (`KEEP_*`, `keep!`, # `K*`/`G*` fn-pointer statics) that pin codegen-facing `#[no_mangle]` entry # points as dead-strip roots. They exist for the whole-program bitcode-LTO @@ -109,6 +109,20 @@ string-normalize = ["dep:unicode-normalization"] # wrap-ansi@9+ (and thus ink). Only `intl.rs` uses it; a program that never # constructs an `Intl.Segmenter` links none of it. intl-segmenter = ["dep:unicode-segmentation"] +# The `Intl.*` NAMESPACE surface (`Intl.NumberFormat`, `DateTimeFormat`, +# `Collator`, `RelativeTimeFormat`, `ListFormat`, `PluralRules`, +# `DisplayNames`, `DurationFormat`, `getCanonicalLocales`, +# `supportedValuesOf`) — ~219 KB of constructor/option/format machinery +# reached ONLY through `install_intl_namespace`, which `populate_global_this +# _builtins` calls unconditionally. Gating the install (a no-op stub when +# off) lets `-dead_strip` drop every part of `intl.rs` not also reachable +# from a locale-aware method entry point. Programs keep working either way: +# `Number/Date/String.prototype.toLocale*` and `localeCompare` live outside +# this gate (their `js_*` entries stay compiled and their helpers stay +# reachable), so only the `Intl.` namespace members depend on it. The +# compiler enables it on any `Intl`/locale-formatting token, erring toward +# enabling (same over-approximation contract as `temporal`). +intl-namespace = [] # `Intl.getCanonicalLocales` / `*.supportedLocalesOf` BCP-47 (UTS #35) language-tag # canonicalization via `icu_locale_core` (the data-free structural parser — case # normalization, variant ordering, extension well-formedness, UTS35 rejection of diff --git a/crates/perry-runtime/src/intl.rs b/crates/perry-runtime/src/intl.rs index 83d04b657a..fdbfa7d977 100644 --- a/crates/perry-runtime/src/intl.rs +++ b/crates/perry-runtime/src/intl.rs @@ -1733,6 +1733,17 @@ fn set_proto_to_string_tag(proto: *mut ObjectHeader, tag: &str) { ); } +/// Install the `Intl.*` namespace members. Behind `intl-namespace` (default-on; +/// the compiler enables it whenever the program mentions `Intl` or any +/// locale-formatting API): when the feature is off this is a no-op, the +/// `Intl` global is still a real (empty) namespace object, and `-dead_strip` +/// reclaims the constructor/option/format machinery that nothing else +/// reaches. `toLocale*` / `localeCompare` are unaffected — their entry points +/// and helpers live outside this gate. +#[cfg(not(feature = "intl-namespace"))] +pub fn install_intl_namespace(_ns_obj: *mut ObjectHeader) {} + +#[cfg(feature = "intl-namespace")] pub fn install_intl_namespace(ns_obj: *mut ObjectHeader) { if ns_obj.is_null() { return; diff --git a/crates/perry-runtime/src/object/class_constructors.rs b/crates/perry-runtime/src/object/class_constructors.rs index ba3deb35ed..0a1e075215 100644 --- a/crates/perry-runtime/src/object/class_constructors.rs +++ b/crates/perry-runtime/src/object/class_constructors.rs @@ -529,6 +529,11 @@ pub unsafe extern "C" fn js_super_construct_apply( // value is the Intl constructor closure; run it (new.target set) and re-home // the branded instance onto `this`, the spread counterpart of the // `js_fetch_or_value_super` Intl branch. + // `class X extends Intl.` — behind `intl-namespace` for the same + // reason as the instanceof probe: with the feature off no Intl + // constructor value exists, so the branch is unreachable, and skipping it + // keeps this always-live path from pinning the Intl constructor web. + #[cfg(feature = "intl-namespace")] { let parent_val = crate::object::class_registry::js_get_dynamic_parent_value(child_cid); if crate::intl::is_intl_constructor_value(parent_val) { diff --git a/crates/perry-runtime/src/object/instanceof.rs b/crates/perry-runtime/src/object/instanceof.rs index 1e689cb64c..19ae394d82 100644 --- a/crates/perry-runtime/src/object/instanceof.rs +++ b/crates/perry-runtime/src/object/instanceof.rs @@ -454,6 +454,11 @@ pub extern "C" fn js_instanceof_dynamic(value: f64, type_ref: f64) -> f64 { // `inst instanceof Intl.`: Intl instances are plain heap objects whose // `[[Prototype]]` is `Intl..prototype` but carry no class-id, so the // arms above can't match them. Walk their static-prototype chain. + // `Intl.*` brand checks. Behind `intl-namespace`: with the feature off no + // Intl constructor value can exist (the namespace install is a no-op), so + // the probe could never match — and skipping it keeps this always-live + // dispatcher from statically pinning every Intl constructor thunk (~204 KB). + #[cfg(feature = "intl-namespace")] if let Some(is_inst) = crate::intl::intl_instanceof(value, type_ref) { return if is_inst { f64::from_bits(crate::value::TAG_TRUE) diff --git a/crates/perry/src/commands/compile/collect_modules/feature_detect.rs b/crates/perry/src/commands/compile/collect_modules/feature_detect.rs index f6848f73b6..e4ea69c33b 100644 --- a/crates/perry/src/commands/compile/collect_modules/feature_detect.rs +++ b/crates/perry/src/commands/compile/collect_modules/feature_detect.rs @@ -291,6 +291,30 @@ pub(super) fn detect_optional_feature_usage( if hir_debug.contains("property: \"Segmenter\"") { ctx.uses_intl_segmenter = true; } + // `Intl.*` namespace surface (~219 KB). Every `Intl.X` access lowers + // with `Intl` as a property/identifier token, and the locale-aware + // prototype methods below can hand back Intl-formatted output, so any + // of them enables the namespace. Deliberately over-approximate — a + // missed detection leaves `Intl.NumberFormat` undefined at runtime, + // so err toward enabling (same contract as `temporal`). + if hir_debug.contains("\"Intl\"") + || hir_debug.contains("property: \"NumberFormat\"") + || hir_debug.contains("property: \"DateTimeFormat\"") + || hir_debug.contains("property: \"Collator\"") + || hir_debug.contains("property: \"RelativeTimeFormat\"") + || hir_debug.contains("property: \"ListFormat\"") + || hir_debug.contains("property: \"PluralRules\"") + || hir_debug.contains("property: \"DisplayNames\"") + || hir_debug.contains("property: \"DurationFormat\"") + || hir_debug.contains("property: \"Segmenter\"") + || hir_debug.contains("property: \"getCanonicalLocales\"") + || hir_debug.contains("property: \"supportedValuesOf\"") + || hir_debug.contains("property: \"supportedLocalesOf\"") + || hir_debug.contains("toLocale") + || hir_debug.contains("localeCompare") + { + ctx.uses_intl_namespace = true; + } // `Intl.getCanonicalLocales(...)` / `Intl.*.supportedLocalesOf(...)` gate // `perry-runtime/intl-locale` (`icu_locale_core` BCP-47 canonicalization). // Both lower with the method name as a `property` token. diff --git a/crates/perry/src/commands/compile/optimized_libs/freshness.rs b/crates/perry/src/commands/compile/optimized_libs/freshness.rs index 15662efb64..c2c4dd8f68 100644 --- a/crates/perry/src/commands/compile/optimized_libs/freshness.rs +++ b/crates/perry/src/commands/compile/optimized_libs/freshness.rs @@ -88,7 +88,7 @@ pub(crate) fn auto_optimized_cache_key( ) -> String { let target_str = target.unwrap_or("host"); format!( - "{}|{}|{}|wasm={}|regex={}|temporal={}|ee={}|url={}|norm={}|seg={}|loc={}|diag={}|dgram={}|http2={}|dyneval={}|sizeopt={}|anchors={}|v={}", + "{}|{}|{}|wasm={}|regex={}|temporal={}|ee={}|url={}|norm={}|seg={}|loc={}|intlns={}|diag={}|dgram={}|http2={}|dyneval={}|sizeopt={}|anchors={}|v={}", feature_arg, panic_abort_safe, target_str, @@ -100,6 +100,7 @@ pub(crate) fn auto_optimized_cache_key( ctx.uses_string_normalize, ctx.uses_intl_segmenter, ctx.uses_intl_locale, + ctx.uses_intl_namespace, ctx.uses_diagnostics, ctx.uses_dgram, // HTTP/2 imports and dynamic builtin resolution pull in @@ -166,6 +167,12 @@ pub(crate) fn auto_optimized_cross_features( if ctx.uses_intl_segmenter { cross_features.push("perry-runtime/intl-segmenter".to_string()); } + // `Intl.*` namespace surface — see perry-runtime's `intl-namespace`. + // A deferred dynamic-code site can construct `Intl.…` from a runtime + // string, so force it on there too (mirrors the dyn-eval regex rule). + if ctx.uses_intl_namespace || perry_hir::has_deferred_dynamic_code_sites() { + cross_features.push("perry-runtime/intl-namespace".to_string()); + } if ctx.uses_intl_locale { cross_features.push("perry-runtime/intl-locale".to_string()); } diff --git a/crates/perry/src/commands/compile/types.rs b/crates/perry/src/commands/compile/types.rs index 1d837aefd8..c71802ce1f 100644 --- a/crates/perry/src/commands/compile/types.rs +++ b/crates/perry/src/commands/compile/types.rs @@ -680,6 +680,11 @@ pub struct CompilationContext { /// `perry-runtime/intl-locale` (`icu_locale_core`'s data-free BCP-47 / UTS #35 /// structural parser). A program that never canonicalizes a locale links a /// lighter hand-rolled fallback instead. + /// The program can reach the `Intl.*` namespace surface (any `Intl` + /// token or locale-formatting API). Gates `perry-runtime/intl-namespace` + /// (~219 KB of constructor/option/format machinery). Over-approximates: + /// a dynamic-code site forces it on. + pub uses_intl_namespace: bool, pub uses_intl_locale: bool, /// Whether any TS module localizes a date/time — `Intl.DateTimeFormat`, or /// `Date.prototype.toLocale{,Date,Time}String`. Gates @@ -1028,6 +1033,7 @@ impl CompilationContext { uses_url: false, uses_string_normalize: false, uses_intl_segmenter: false, + uses_intl_namespace: false, uses_intl_locale: false, uses_intl_datetime: false, uses_diagnostics: false, From 12d5aabbaa68321d19811e7c43c2b330230cd4a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 29 Jul 2026 06:34:13 +0200 Subject: [PATCH 2/3] changelog: intl-namespace gate fragment --- changelog.d/intl-namespace-gate.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/intl-namespace-gate.md diff --git a/changelog.d/intl-namespace-gate.md b/changelog.d/intl-namespace-gate.md new file mode 100644 index 0000000000..d7e5e922e5 --- /dev/null +++ b/changelog.d/intl-namespace-gate.md @@ -0,0 +1 @@ +**Binary size:** the `Intl.*` namespace surface (~204 KB of constructor/option/format machinery — `NumberFormat`, `DateTimeFormat`, `Collator`, `PluralRules`, `ListFormat`, `RelativeTimeFormat`, `DisplayNames`, `DurationFormat`, locale canonicalization) is now behind a default-on `intl-namespace` feature that the compiler enables on any `Intl` or locale-formatting token. Three always-live paths referenced it unconditionally — `populate_global_this_builtins`' namespace install, `js_instanceof_dynamic`'s brand probe, and the `class X extends Intl.` super path — so every binary carried the whole web. With the feature off those become no-ops (unreachable by construction: no Intl constructor value can exist), and `-dead_strip` reclaims the machinery. `toLocaleString`/`toLocaleDateString`/`localeCompare` are unaffected — their entry points live outside the gate and their tokens also enable it. Hello world: −66 KB (4,527,336 → 4,461,208 bytes). From a4da80fb7d4727f877a41a237cdb160984453522 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 29 Jul 2026 06:34:44 +0200 Subject: [PATCH 3/3] changelog: name fragment after PR (#6959) --- .../{intl-namespace-gate.md => 6959-intl-namespace-gate.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{intl-namespace-gate.md => 6959-intl-namespace-gate.md} (100%) diff --git a/changelog.d/intl-namespace-gate.md b/changelog.d/6959-intl-namespace-gate.md similarity index 100% rename from changelog.d/intl-namespace-gate.md rename to changelog.d/6959-intl-namespace-gate.md