Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/6959-intl-namespace-gate.md
Original file line number Diff line number Diff line change
@@ -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.<Ctor>` 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).
16 changes: 15 additions & 1 deletion crates/perry-runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions crates/perry-runtime/src/intl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions crates/perry-runtime/src/object/class_constructors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.<Ctor>` — 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) {
Expand Down
5 changes: 5 additions & 0 deletions crates/perry-runtime/src/object/instanceof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,11 @@ pub extern "C" fn js_instanceof_dynamic(value: f64, type_ref: f64) -> f64 {
// `inst instanceof Intl.<Ctor>`: Intl instances are plain heap objects whose
// `[[Prototype]]` is `Intl.<Ctor>.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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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());
}
Expand Down
6 changes: 6 additions & 0 deletions crates/perry/src/commands/compile/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Loading