diff --git a/changelog.d/7435-gate-url-shape-probes.md b/changelog.d/7435-gate-url-shape-probes.md new file mode 100644 index 0000000000..e852327fdb --- /dev/null +++ b/changelog.d/7435-gate-url-shape-probes.md @@ -0,0 +1 @@ +size: the URL-shape probes on generic value paths (`ToString`, `+` coercion, `JSON.stringify`, `Object.fromEntries`, by-name field set) are now compiled only under `url-engine` — they were pinning the URL parser into every binary. Hello world drops 33,064 bytes (4,708,736 → 4,675,672). diff --git a/crates/perry-runtime/src/json/replacer.rs b/crates/perry-runtime/src/json/replacer.rs index 0d2c319fc4..1dfd9bbcd6 100644 --- a/crates/perry-runtime/src/json/replacer.rs +++ b/crates/perry-runtime/src/json/replacer.rs @@ -274,6 +274,7 @@ unsafe fn dispatch_pointer_with_replacer( // the URL, so the generic object walk below would trip the circular- // structure detector. The href is a plain string, so the emit is identical // for compact and pretty walks. See `write_url_href_json`. + #[cfg(feature = "url-engine")] if crate::url::is_url_object_shape(ptr as *mut crate::ObjectHeader) { super::stringify::write_url_href_json(ptr as *mut crate::ObjectHeader, buf); return; @@ -823,6 +824,7 @@ pub(crate) unsafe fn stringify_value_pretty( // be walked as a plain object (its `searchParams` back-reference trips // the circular-structure detector). Mirrors the compact-path branch in // `stringify_object_inner`; see `write_url_href_json`. + #[cfg(feature = "url-engine")] if crate::url::is_url_object_shape(ptr as *mut crate::ObjectHeader) { super::stringify::write_url_href_json(ptr as *mut crate::ObjectHeader, buf); return; diff --git a/crates/perry-runtime/src/object/field_set_by_name/tail.rs b/crates/perry-runtime/src/object/field_set_by_name/tail.rs index 03260f8bd7..e028699e7a 100644 --- a/crates/perry-runtime/src/object/field_set_by_name/tail.rs +++ b/crates/perry-runtime/src/object/field_set_by_name/tail.rs @@ -314,6 +314,11 @@ pub(super) fn set_field_by_name_object_tail( } } + // Binary size: this whole arm statically references eight + // `js_url_set_*` entry points, which is what kept the URL parser in + // every binary. Gated with the other URL shape probes; see + // `value/to_string.rs`. + #[cfg(feature = "url-engine")] if !plan_fast && !key.is_null() && (key as usize) > 0x10000 diff --git a/crates/perry-runtime/src/object/object_ops/from_entries.rs b/crates/perry-runtime/src/object/object_ops/from_entries.rs index 50863a0e1c..4e242c0deb 100644 --- a/crates/perry-runtime/src/object/object_ops/from_entries.rs +++ b/crates/perry-runtime/src/object/object_ops/from_entries.rs @@ -59,6 +59,7 @@ unsafe fn object_from_entries_has_iterator(value: f64, raw: i64, gc_type: Option | Some(crate::gc::GC_TYPE_SET) => return true, Some(crate::gc::GC_TYPE_OBJECT) => { let obj = raw as *mut ObjectHeader; + #[cfg(feature = "url-engine")] if crate::url::try_read_as_search_params(obj).is_some() { return true; } @@ -107,6 +108,9 @@ unsafe fn object_from_entries_materialize_entries(entries_value: f64) -> *mut Ar return crate::map::js_map_entries(raw as *const crate::map::MapHeader); } + // The whole arm exists to serve `URLSearchParams`, so it goes with the + // feature rather than leaving `obj` bound for a body that isn't compiled. + #[cfg(feature = "url-engine")] if gc_type == Some(crate::gc::GC_TYPE_OBJECT) { let obj = raw as *mut ObjectHeader; if crate::url::try_read_as_search_params(obj).is_some() { diff --git a/crates/perry-runtime/src/value/dynamic_arith.rs b/crates/perry-runtime/src/value/dynamic_arith.rs index 061d39f8d2..f023a465da 100644 --- a/crates/perry-runtime/src/value/dynamic_arith.rs +++ b/crates/perry-runtime/src/value/dynamic_arith.rs @@ -276,17 +276,24 @@ unsafe fn to_primitive_default_for_add(value: f64) -> f64 { // registry ids, not heap `ObjectHeader`s, so the shape probe would // dereference unmapped memory. if !crate::value::addr_class::is_handle_band(ptr) { - let boxed = - f64::from_bits(crate::value::POINTER_TAG | ((ptr as u64) & crate::value::POINTER_MASK)); - let href = crate::url::url_class::js_url_href_if_url(boxed); - if href.to_bits() != crate::value::TAG_UNDEFINED { - let s = js_jsvalue_to_string(href); - return crate::value::js_nanbox_string(s as i64); - } - let obj = ptr as *mut crate::object::ObjectHeader; - if crate::url::try_read_as_search_params(obj).is_some() { - let s = crate::url::search_params::js_url_search_params_to_string(obj); - return crate::value::js_nanbox_string(s as i64); + // See the matching note in `value/to_string.rs`: shape probes on a + // generic path, kept out of non-URL binaries so the parser can strip. + // `boxed` is bound inside the gate: it feeds only these probes. + #[cfg(feature = "url-engine")] + { + let boxed = f64::from_bits( + crate::value::POINTER_TAG | ((ptr as u64) & crate::value::POINTER_MASK), + ); + let href = crate::url::url_class::js_url_href_if_url(boxed); + if href.to_bits() != crate::value::TAG_UNDEFINED { + let s = js_jsvalue_to_string(href); + return crate::value::js_nanbox_string(s as i64); + } + let obj = ptr as *mut crate::object::ObjectHeader; + if crate::url::try_read_as_search_params(obj).is_some() { + let s = crate::url::search_params::js_url_search_params_to_string(obj); + return crate::value::js_nanbox_string(s as i64); + } } } diff --git a/crates/perry-runtime/src/value/to_string.rs b/crates/perry-runtime/src/value/to_string.rs index 0bfc25b712..85f6f2fe4d 100644 --- a/crates/perry-runtime/src/value/to_string.rs +++ b/crates/perry-runtime/src/value/to_string.rs @@ -1163,17 +1163,29 @@ pub extern "C" fn js_jsvalue_to_string(value: f64) -> *mut crate::string::String // heap `ObjectHeader`s, so the shape check would dereference // unmapped memory. if !crate::value::addr_class::is_handle_band(ptr as usize) { - let boxed = f64::from_bits(POINTER_TAG | ((ptr as u64) & POINTER_MASK)); - let url_href = crate::url::url_class::js_url_href_if_url(boxed); - if url_href.to_bits() != crate::value::TAG_UNDEFINED { - return js_jsvalue_to_string(url_href); - } - if crate::url::try_read_as_search_params(ptr as *mut crate::object::ObjectHeader) - .is_some() + // Binary size: these are SHAPE probes on a generic path, so the + // static reference keeps the whole URL class + parser alive in + // every binary even though the runtime check can never pass + // without `url-engine`. `uses_url` (zero-false-negative by + // construction) is what turns that feature on, so a program + // with no URL API cannot own a URL or URLSearchParams here. + // `boxed` is bound inside the gate: it feeds only these probes. + #[cfg(feature = "url-engine")] { - return crate::url::search_params::js_url_search_params_to_string( + let boxed = f64::from_bits(POINTER_TAG | ((ptr as u64) & POINTER_MASK)); + let url_href = crate::url::url_class::js_url_href_if_url(boxed); + if url_href.to_bits() != crate::value::TAG_UNDEFINED { + return js_jsvalue_to_string(url_href); + } + if crate::url::try_read_as_search_params( ptr as *mut crate::object::ObjectHeader, - ); + ) + .is_some() + { + return crate::url::search_params::js_url_search_params_to_string( + ptr as *mut crate::object::ObjectHeader, + ); + } } } // OrdinaryToPrimitive(obj, "string"): the object has no diff --git a/test-files/test_gap_url_shape_probe_gate.ts b/test-files/test_gap_url_shape_probe_gate.ts new file mode 100644 index 0000000000..178455773c --- /dev/null +++ b/test-files/test_gap_url_shape_probe_gate.ts @@ -0,0 +1,19 @@ +// The generic value paths (`ToString`, `+` coercion, `JSON.stringify`, +// `Object.fromEntries`, and the by-name field setter) each carry a runtime +// "is this object URL-shaped?" probe. Those probes are compiled only under +// `url-engine`, because their static references otherwise pin the whole URL +// parser into every binary. This file uses the URL API, so `uses_url` turns +// the feature on and every probe below must behave exactly as before. + +const u = new URL("https://user:pw@example.com:8443/a/b?x=1&y=2#frag"); +console.log(String(u)); +console.log("" + u); +console.log(u.hostname, u.port, u.pathname, u.search, u.hash); +u.pathname = "/changed"; +u.search = "?z=9"; +u.href = "https://other.example/zzz?q=1"; +console.log(u.href, u.hostname); +const sp = new URLSearchParams("a=1&b=2"); +console.log(String(sp), "" + sp); +console.log(JSON.stringify({ u, sp: String(sp) })); +console.log(JSON.stringify(Object.fromEntries(sp)));