diff --git a/Cargo.lock b/Cargo.lock index d6c4beedb8..f02b6d6e1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6364,6 +6364,7 @@ dependencies = [ "unicode-segmentation", "url", "windows-sys 0.61.2", + "writeable", "x509-cert", ] diff --git a/changelog.d/9451-9468-intl-method-source.md b/changelog.d/9451-9468-intl-method-source.md new file mode 100644 index 0000000000..033cf88709 --- /dev/null +++ b/changelog.d/9451-9468-intl-method-source.md @@ -0,0 +1,15 @@ +### Fixed + +- **Class methods and accessors now retain their original source text.** + `String(C.prototype.method)`, direct `.toString()`, template coercion, and + reflected getter/setter functions return the source MethodDefinition rather + than a synthesized native-function body. Object-literal accessors also + receive their specified `get name` / `set name` function names. CommonJS + class expressions keep assignment-inferred names without exposing Perry's + internal anonymous-default registration key. Fixes #9468. + +- **Default `Intl.DateTimeFormat` dates now use the locale's CLDR numeric + pattern.** The implicit numeric year/month/day field set, its + `formatToParts()` output, and `Date.prototype.toLocaleDateString()` now agree + on locale order, separators, and padding instead of falling back to the + hard-coded US layout. Fixes #9451. diff --git a/crates/perry-codegen/src/codegen/artifact_source_text.rs b/crates/perry-codegen/src/codegen/artifact_source_text.rs new file mode 100644 index 0000000000..794eed3ba7 --- /dev/null +++ b/crates/perry-codegen/src/codegen/artifact_source_text.rs @@ -0,0 +1,117 @@ +//! Source-text registration for raw class method and accessor symbols. +//! +//! Class members are not ordinary closure wrappers, so their retained source +//! must be paired with the LLVM body symbol codegen actually emitted. Kept out +//! of `artifacts.rs` so that file remains below the repository's 2,000-line +//! limit. + +use std::collections::HashSet; + +use perry_hir::types::FuncId; +use perry_hir::Module as HirModule; + +use crate::module::LlModule; + +use super::helpers::{scoped_method_name, scoped_static_method_name}; + +pub(super) fn extend_class_method_source_text( + hir: &HirModule, + module_prefix: &str, + llmod: &LlModule, + user_fn_source: &mut Vec<(String, String)>, +) { + // An HIR registry entry is not proof that this module emitted the body: a + // cross-module or typed-only accessor can remain present without a local + // definition. Referencing such a symbol from module initialization makes + // LLVM reject the module, so `has_function` is the final authority. + let mut seen: HashSet = user_fn_source + .iter() + .map(|(symbol, _)| symbol.clone()) + .collect(); + let mut push_defined = |func_id: FuncId, symbol: String| { + let Some(source) = hir.closure_source_text.get(&func_id) else { + return; + }; + if symbol.is_empty() || !llmod.has_function(&symbol) || !seen.insert(symbol.clone()) { + return; + } + user_fn_source.push((symbol, source.clone())); + }; + + for class in &hir.classes { + if class.id == 0 { + continue; + } + for method in &class.methods { + push_defined( + method.id, + scoped_method_name(module_prefix, &class.name, &method.name), + ); + } + for member in class + .computed_members + .iter() + .filter(|member| !member.is_static) + { + push_defined( + member.function.id, + scoped_method_name(module_prefix, &class.name, &member.function.name), + ); + } + for (prop, getter) in &class.getters { + let symbol = if class.static_accessor_fn_ids.contains(&getter.id) { + scoped_static_method_name( + module_prefix, + class.id, + &class.name, + &format!("__get_{prop}"), + ) + } else { + scoped_method_name( + module_prefix, + &class.name, + &format!("__get_{}", getter.name), + ) + }; + push_defined(getter.id, symbol); + } + for (prop, setter) in &class.setters { + let symbol = if class.static_accessor_fn_ids.contains(&setter.id) { + scoped_static_method_name( + module_prefix, + class.id, + &class.name, + &format!("__set_{prop}"), + ) + } else { + scoped_method_name( + module_prefix, + &class.name, + &format!("__set_{}", setter.name), + ) + }; + push_defined(setter.id, symbol); + } + for method in &class.static_methods { + push_defined( + method.id, + scoped_static_method_name(module_prefix, class.id, &class.name, &method.name), + ); + } + for member in class + .computed_members + .iter() + .filter(|member| member.is_static) + { + push_defined( + member.function.id, + scoped_static_method_name( + module_prefix, + class.id, + &class.name, + &member.function.name, + ), + ); + } + } +} diff --git a/crates/perry-codegen/src/codegen/artifacts.rs b/crates/perry-codegen/src/codegen/artifacts.rs index 4e69575e47..6f845e6276 100644 --- a/crates/perry-codegen/src/codegen/artifacts.rs +++ b/crates/perry-codegen/src/codegen/artifacts.rs @@ -1854,6 +1854,16 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> { user_fn_source.push((sym, src.clone())); } + // #9468: method/accessor bodies are raw symbols rather than closure + // wrappers. Pair retained MethodDefinition text only with symbols this + // module actually emitted; the helper also preserves the file-size gate. + super::artifact_source_text::extend_class_method_source_text( + hir, + module_prefix, + llmod, + &mut user_fn_source, + ); + // Wall 51: the standalone-ctor arity registered into CLASS_CONSTRUCTORS must // match the arity of the ctor function actually emitted above (which, for a // no-own-ctor class with heritage, is the synthesized `super(...args)` diff --git a/crates/perry-codegen/src/codegen/mod.rs b/crates/perry-codegen/src/codegen/mod.rs index 0c6d7b52d9..ba4899a9ef 100644 --- a/crates/perry-codegen/src/codegen/mod.rs +++ b/crates/perry-codegen/src/codegen/mod.rs @@ -179,6 +179,7 @@ mod argument_shape_clone_tests; pub(crate) mod arguments; mod artifact_context; mod artifact_display_names; +mod artifact_source_text; mod artifacts; mod boxed_locals; #[cfg(test)] diff --git a/crates/perry-hir/src/destructuring/var_decl.rs b/crates/perry-hir/src/destructuring/var_decl.rs index 072b1b5e5e..81e30a2d9b 100644 --- a/crates/perry-hir/src/destructuring/var_decl.rs +++ b/crates/perry-hir/src/destructuring/var_decl.rs @@ -105,7 +105,25 @@ pub(crate) fn lower_var_decl_with_destructuring( if let Some(init_ast) = decl.init.as_ref() { result.extend(predeclare_implicit_assignment_targets(ctx, init_ast)); } - let init = decl.init.as_ref().map(|e| lower_expr(ctx, e)).transpose()?; + // A simple binding performs NamedEvaluation for an anonymous + // function/class initializer (`const C = class {}`). Most class + // expressions take stmt.rs's direct class fast path, but a + // pre-existing class-registry entry can deliberately divert one + // through this generic path (notably the CommonJS factory's + // function-scope pre-registration). Preserve the binding name in + // that path too; the helper filters out named definitions and + // non-NamedEvaluation expressions before installing the context. + let init = decl + .init + .as_ref() + .map(|e| { + crate::lower::expr_assign::lower_rhs_with_assignment_name( + ctx, + e, + Some(name.clone()), + ) + }) + .transpose()?; if matches!(ty, Type::Any) { match &init { Some(Expr::NativeMethodCall { module, method, .. }) => { diff --git a/crates/perry-hir/src/lower/expr_assign.rs b/crates/perry-hir/src/lower/expr_assign.rs index a34f3055f1..d593a91d69 100644 --- a/crates/perry-hir/src/lower/expr_assign.rs +++ b/crates/perry-hir/src/lower/expr_assign.rs @@ -56,7 +56,7 @@ pub(crate) fn rhs_accepts_assignment_name(expr: &ast::Expr) -> bool { } } -fn lower_rhs_with_assignment_name( +pub(crate) fn lower_rhs_with_assignment_name( ctx: &mut LoweringContext, rhs: &ast::Expr, name: Option, diff --git a/crates/perry-hir/src/lower/expr_object.rs b/crates/perry-hir/src/lower/expr_object.rs index 4bf17bb602..dedfdd6c15 100644 --- a/crates/perry-hir/src/lower/expr_object.rs +++ b/crates/perry-hir/src/lower/expr_object.rs @@ -501,6 +501,19 @@ fn lower_accessor_prop( }; let func_id = ctx.fresh_func(); + // #9468: accessor definitions run SetFunctionName with the `get`/`set` + // prefix. Class accessors already acquire this name when reflected from a + // descriptor; object-literal accessors are ordinary closure values and + // therefore need the same metadata recorded at lowering. + if let MethodKeyKind::Static(key) = &accessor_key { + let prefix = if setter_param.is_some() { + "set " + } else { + "get " + }; + ctx.closure_display_names + .insert(func_id, format!("{prefix}{key}")); + } let outer_locals: Vec<(String, LocalId)> = ctx .locals .iter() diff --git a/crates/perry-hir/src/lower_decl/class_decl.rs b/crates/perry-hir/src/lower_decl/class_decl.rs index 6daacccc90..d7d0080aa8 100644 --- a/crates/perry-hir/src/lower_decl/class_decl.rs +++ b/crates/perry-hir/src/lower_decl/class_decl.rs @@ -226,6 +226,28 @@ pub fn lower_class_decl( .insert(class_id, class_decl.ident.sym.to_string()); } capture_class_source(ctx, class_id, &class_decl.class); + // cjs_wrap rewrites a sole `module.exports = class { ... }` into a + // declaration under this reserved key so Perry can hoist and register the + // class. The key is compiler-only: the original class expression had no + // NamedEvaluation context, therefore its observable `.name` is empty and + // its retained source must not expose the injected identifier. + const CJS_ANONYMOUS_DEFAULT: &str = "__perry_cjs_default__"; + if class_decl.ident.sym.as_ref() == CJS_ANONYMOUS_DEFAULT { + ctx.class_display_names.insert(class_id, String::new()); + if let Some(source) = ctx.class_source_text.get_mut(&class_id) { + if let Some(after_class) = source.strip_prefix("class") { + let trimmed = after_class.trim_start(); + if let Some(after_name) = trimmed.strip_prefix(CJS_ANONYMOUS_DEFAULT) { + let name_is_complete = after_name.as_bytes().first().is_none_or(|byte| { + !byte.is_ascii_alphanumeric() && *byte != b'_' && *byte != b'$' + }); + if name_is_complete { + *source = format!("class{after_name}"); + } + } + } + } + } if let Some(ast::Expr::Ident(parent)) = class_decl.class.super_class.as_deref() { if let Some(crate::lower::fn_ctor_env::FnCtorShape::DynCtor(kind)) = ctx.fn_ctor_env.entries.get(parent.sym.as_ref()).cloned() diff --git a/crates/perry-hir/src/lower_decl/class_members.rs b/crates/perry-hir/src/lower_decl/class_members.rs index d830af083a..2e9f0b13fb 100644 --- a/crates/perry-hir/src/lower_decl/class_members.rs +++ b/crates/perry-hir/src/lower_decl/class_members.rs @@ -9,6 +9,40 @@ use crate::lower_types::*; use super::*; +/// #9468: retain a class member's MethodDefinition source under the FuncId of +/// its compiled method/accessor body. Unlike ordinary functions, class members +/// are emitted as raw `perry_method_*` / `perry_static_*` symbols, but the +/// source registry can still use the same FuncId-to-source handoff. +/// +/// SWC's ClassMethod span includes the class-only `static` modifier. That +/// modifier is not part of the function object's [[SourceText]] (`String(C.m)` +/// starts at the method definition itself), so remove it while preserving the +/// rest byte-for-byte, including `get`/`set`, `async`, `*`, and comments. +fn capture_class_method_source( + ctx: &mut LoweringContext, + func_id: crate::types::FuncId, + method: &ast::ClassMethod, +) { + let Some(mut src) = crate::ir::current_module_source_slice(method.span.lo.0, method.span.hi.0) + else { + return; + }; + if method.is_static { + let leading_ws = src.len() - src.trim_start().len(); + let candidate = &src[leading_ws..]; + if let Some(after_static) = candidate.strip_prefix("static") { + if after_static + .as_bytes() + .first() + .is_some_and(u8::is_ascii_whitespace) + { + src = after_static.trim_start().to_string(); + } + } + } + ctx.closure_source_text.insert(func_id, src); +} + pub fn lower_constructor( ctx: &mut LoweringContext, class_name: &str, @@ -670,6 +704,7 @@ pub fn lower_class_method_with_name( ctx.exit_type_param_scope(); let func_id = ctx.fresh_func(); + capture_class_method_source(ctx, func_id, method); // Record the param-prologue length for generator methods so the generator // transform runs param binding (default guards + destructuring) synchronously // at call time per spec FunctionDeclarationInstantiation order. Without this, @@ -799,8 +834,11 @@ pub fn lower_getter_method_with_name( ctx.exit_scope(scope_mark); ctx.in_nonarrow_fn = saved_in_nonarrow_fn; + let func_id = ctx.fresh_func(); + capture_class_method_source(ctx, func_id, method); + Ok(Function { - id: ctx.fresh_func(), + id: func_id, name, type_params: Vec::new(), params: Vec::new(), @@ -967,8 +1005,11 @@ pub fn lower_setter_method_with_name( ctx.exit_scope(scope_mark); ctx.in_nonarrow_fn = saved_in_nonarrow_fn; + let func_id = ctx.fresh_func(); + capture_class_method_source(ctx, func_id, method); + Ok(Function { - id: ctx.fresh_func(), + id: func_id, name, type_params: Vec::new(), params, diff --git a/crates/perry-runtime/Cargo.toml b/crates/perry-runtime/Cargo.toml index ec1f9b4340..16d61a0fa1 100644 --- a/crates/perry-runtime/Cargo.toml +++ b/crates/perry-runtime/Cargo.toml @@ -212,7 +212,7 @@ proc-ipc = [] intl-locale = ["dep:icu_locale", "dep:icu_locale_core"] # CLDR-accurate Intl.DateTimeFormat / toLocaleString date-time patterns and a # compiled IANA database for explicit named `timeZone` options. -intl-datetime = ["dep:icu_datetime", "dep:icu_time", "dep:icu_calendar", "dep:icu_locale_core", "dep:timezone_provider"] +intl-datetime = ["dep:icu_datetime", "dep:icu_time", "dep:icu_calendar", "dep:icu_locale_core", "dep:timezone_provider", "dep:writeable"] # `full` only opt-ins the small Node-API helpers (os.hostname / os.homedir). # `postgres`, `redis`, `whoami` were previously listed here but were either # unimported (postgres, whoami) or only used by a now-deleted `redis_client.rs` @@ -347,6 +347,8 @@ icu_locale_core = { version = "2", optional = true } icu_datetime = { version = "2", default-features = false, features = ["compiled_data"], optional = true } icu_time = { version = "2", default-features = false, features = ["compiled_data"], optional = true } icu_calendar = { version = "2", default-features = false, features = ["compiled_data"], optional = true } +# ICU's semantic field annotations drive Intl.DateTimeFormat#formatToParts. +writeable = { version = "0.6", optional = true } idna = { version = "1", optional = true } url = { version = "2", optional = true } # #4911: real node:dns resolve*/reverse. hickory-proto provides DNS wire-format diff --git a/crates/perry-runtime/src/closure/dispatch.rs b/crates/perry-runtime/src/closure/dispatch.rs index c19fca9c77..bdfb309264 100644 --- a/crates/perry-runtime/src/closure/dispatch.rs +++ b/crates/perry-runtime/src/closure/dispatch.rs @@ -20,7 +20,10 @@ mod errors; mod validate; mod value_call; -pub(crate) use bound::{coerce_call_this, rebind_explicit_this, reify_function_method_value}; +pub(crate) use bound::{ + bound_method_source_func_ptr, coerce_call_this, rebind_explicit_this, + reify_function_method_value, +}; pub use bound::{dispatch_bound_function, dispatch_bound_method, js_function_bind}; pub(crate) use errors::reset_throw_not_callable_counter; diff --git a/crates/perry-runtime/src/closure/dispatch/bound.rs b/crates/perry-runtime/src/closure/dispatch/bound.rs index 74d753cc2b..fffb633467 100644 --- a/crates/perry-runtime/src/closure/dispatch/bound.rs +++ b/crates/perry-runtime/src/closure/dispatch/bound.rs @@ -3,6 +3,57 @@ use super::*; +/// Resolve a bound class-method value to the raw method body whose source was +/// registered by codegen. Ordinary instance methods carry an owner prototype +/// ref plus their string name; statics carry a constructor ref; symbol-keyed +/// methods carry the already-resolved function pointer directly. +/// +/// Returns `None` for the many non-class users of the BOUND_METHOD sentinel +/// (native module methods, `Function.prototype.call` reifications, and so on), +/// which correctly retain synthesized native source text. +pub(crate) unsafe fn bound_method_source_func_ptr(closure: *const ClosureHeader) -> Option { + if closure.is_null() + || (*closure).func_ptr != BOUND_METHOD_FUNC_PTR + || crate::closure::real_capture_count((*closure).capture_count) < 3 + { + return None; + } + + let method_name_ptr = js_closure_get_capture_ptr(closure, 1) as *const u8; + if method_name_ptr == crate::object::SYMBOL_BOUND_METHOD_NAME.as_ptr() { + if crate::closure::real_capture_count((*closure).capture_count) < 5 { + return None; + } + return (js_closure_get_capture_ptr(closure, 3) as usize != 0) + .then(|| js_closure_get_capture_ptr(closure, 3) as usize); + } + + let method_name_len = js_closure_get_capture_ptr(closure, 2) as usize; + if method_name_ptr.is_null() || method_name_len == 0 { + return None; + } + let name = + std::str::from_utf8(std::slice::from_raw_parts(method_name_ptr, method_name_len)).ok()?; + let receiver = js_closure_get_capture_f64(closure, 0); + + if let Some(class_id) = crate::object::class_prototype_ref_id(receiver) { + return crate::object::lookup_class_method_in_chain(class_id, name) + .map(|(func_ptr, ..)| func_ptr); + } + + let static_class_id = crate::object::class_ref_id(receiver).or_else(|| { + crate::object::is_class_object_value(receiver).then(|| { + let obj = crate::value::JSValue::from_bits(receiver.to_bits()) + .as_pointer::(); + crate::object::js_object_get_class_id(obj) + }) + }); + static_class_id + .filter(|class_id| *class_id != 0) + .and_then(|class_id| crate::object::lookup_static_method_in_chain(class_id, name)) + .map(|(func_ptr, ..)| func_ptr) +} + /// Dispatch a bound method call with the given arguments. /// Extracts the namespace object and method name from the closure captures, /// then calls js_native_call_method with the packed arguments. diff --git a/crates/perry-runtime/src/closure/mod.rs b/crates/perry-runtime/src/closure/mod.rs index e41cb4d81c..0708298460 100644 --- a/crates/perry-runtime/src/closure/mod.rs +++ b/crates/perry-runtime/src/closure/mod.rs @@ -44,6 +44,10 @@ pub use registry::{ CLOSURE_MAGIC, NO_THIS_REBIND_FLAG, }; +pub(crate) use dispatch::{ + bound_method_source_func_ptr, coerce_call_this, rebind_explicit_this, + reify_function_method_value, reset_throw_not_callable_counter, +}; pub use dispatch::{ clean_closure_ptr, dispatch_bound_function, dispatch_bound_method, get_valid_func_ptr, js_closure_call0, js_closure_call1, js_closure_call10, js_closure_call11, js_closure_call12, @@ -53,10 +57,6 @@ pub use dispatch::{ js_closure_call_apply_with_spread, js_closure_call_array, js_function_bind, js_native_call_value, throw_not_callable, DirectCall1, DirectCall2, DirectCall3, DirectCall4, }; -pub(crate) use dispatch::{ - coerce_call_this, rebind_explicit_this, reify_function_method_value, - reset_throw_not_callable_counter, -}; pub use unbox::{js_closure_unbox_callee_checked, js_closure_unbox_callee_checked_rebind}; #[cfg(test)] diff --git a/crates/perry-runtime/src/intl/date_collator.rs b/crates/perry-runtime/src/intl/date_collator.rs index 948e0e6c3b..396c1fc7ad 100644 --- a/crates/perry-runtime/src/intl/date_collator.rs +++ b/crates/perry-runtime/src/intl/date_collator.rs @@ -7,6 +7,8 @@ use crate::value::{js_nanbox_pointer, JSValue}; mod compare; use compare::{collator_compare_order, CollatorCompareOptions}; +mod icu; +use icu::{icu_component_parts, icu_components, icu_style}; /// ECMA-402 FormatDateTime / HandleDateTimeValue step 1: coerce the /// `format`/`formatToParts` argument to a TimeClip'd integer-millisecond value. @@ -139,6 +141,7 @@ fn format_parts_with_dtf_obj( let secs = dtf_zone_local_secs(obj, (ms as i64).div_euclid(1000), temporal_kind); let (year, month, day, hour, minute, second) = crate::date::timestamp_to_components(secs); let mi = month.saturating_sub(1).min(11) as usize; + let locale = get_string_field(obj, KEY_LOCALE).unwrap_or_else(|| "en-US".to_string()); let date_style = get_string_field(obj, KEY_DATE_STYLE); let time_style = get_string_field(obj, KEY_TIME_STYLE); @@ -415,6 +418,26 @@ fn format_parts_with_dtf_obj( let day_period_opt = get_string_field(obj, KEY_DAY_PERIOD); let fractional_digits = get_number_field(obj, KEY_FRACTIONAL).map(|n| (n as u8).clamp(1, 3)); + if hour_opt.is_none() + && minute_opt.is_none() + && second_opt.is_none() + && day_period_opt.is_none() + && era_opt.is_none() + && fractional_digits.is_none() + { + if let Some(parts) = icu_component_parts( + &locale, + year, + month, + day, + year_opt.as_deref(), + month_opt.as_deref(), + day_opt.as_deref(), + weekday_opt.as_deref(), + ) { + return parts; + } + } build_parts_from_components( year, month, @@ -845,117 +868,6 @@ fn era_string(year: i32, style: &str) -> &'static str { } } -/// Format a `dateStyle`/`timeStyle` combination via icu4x (CLDR patterns). -/// Returns `None` when the icu feature is off, the caller opted out (`enabled` -/// = false, e.g. a Temporal partial), or the option combination is unmapped -/// (notably a `long`/`full` timeStyle, which carries a localized time-zone -/// name) — the caller then falls back to the bespoke formatters below. -#[cfg(feature = "intl-datetime")] -#[allow(clippy::too_many_arguments)] -fn icu_style( - enabled: bool, - locale: &str, - year: i32, - month: u32, - day: u32, - hour: u32, - minute: u32, - second: u32, - date_style: Option<&str>, - time_style: Option<&str>, - hour_cycle: Option<&str>, - hour12: Option, -) -> Option { - use super::icu_dtf::{self, Len, Req}; - if !enabled { - return None; - } - icu_dtf::format(&Req { - locale, - year, - month: month as u8, - day: day as u8, - hour: hour as u8, - minute: minute as u8, - second: second as u8, - date_style: date_style.and_then(Len::parse), - time_style: time_style.and_then(Len::parse), - hour_cycle, - hour12, - }) -} - -#[cfg(not(feature = "intl-datetime"))] -#[allow(clippy::too_many_arguments)] -fn icu_style( - _enabled: bool, - _locale: &str, - _year: i32, - _month: u32, - _day: u32, - _hour: u32, - _minute: u32, - _second: u32, - _date_style: Option<&str>, - _time_style: Option<&str>, - _hour_cycle: Option<&str>, - _hour12: Option, -) -> Option { - None -} - -/// Format a date-only, name-bearing component set via icu4x. `None` when the -/// feature is off or icu can't reproduce the combo (numeric-only, narrow, -/// structurally inexpressible), so the caller falls back. -#[cfg(feature = "intl-datetime")] -#[allow(clippy::too_many_arguments)] -fn icu_components( - locale: &str, - year: i32, - month: u32, - day: u32, - year_opt: Option<&str>, - month_opt: Option<&str>, - day_opt: Option<&str>, - weekday_opt: Option<&str>, -) -> Option { - use super::icu_dtf::{self, CompReq}; - icu_dtf::format_components(&CompReq { - locale, - year, - month: month as u8, - day: day as u8, - hour: 0, - minute: 0, - second: 0, - has_year: year_opt.is_some(), - has_month: month_opt.is_some(), - has_day: day_opt.is_some(), - month_style: month_opt, - weekday_style: weekday_opt, - has_hour: false, - has_minute: false, - has_second: false, - hour_cycle: None, - hour12: None, - }) -} - -#[cfg(not(feature = "intl-datetime"))] -#[allow(clippy::too_many_arguments)] -fn icu_components( - _locale: &str, - _year: i32, - _month: u32, - _day: u32, - _year_opt: Option<&str>, - _month_opt: Option<&str>, - _day_opt: Option<&str>, - _weekday_opt: Option<&str>, -) -> Option { - None -} - fn format_date_style(year: i32, month: u32, day: u32, secs: i64, style: &str) -> String { let mi = month.saturating_sub(1).min(11) as usize; match style { diff --git a/crates/perry-runtime/src/intl/date_collator/icu.rs b/crates/perry-runtime/src/intl/date_collator/icu.rs new file mode 100644 index 0000000000..3781c87b2e --- /dev/null +++ b/crates/perry-runtime/src/intl/date_collator/icu.rs @@ -0,0 +1,167 @@ +//! Thin feature-gated adapters from DateTimeFormat state to `icu_dtf`. +//! +//! Kept separate from `date_collator.rs` so the primary implementation stays +//! below the repository's 2,000-line limit. + +/// Format a `dateStyle`/`timeStyle` combination via icu4x (CLDR patterns). +/// Returns `None` when the icu feature is off, the caller opted out (`enabled` +/// = false, e.g. a Temporal partial), or the option combination is unmapped. +#[cfg(feature = "intl-datetime")] +#[allow(clippy::too_many_arguments)] +pub(super) fn icu_style( + enabled: bool, + locale: &str, + year: i32, + month: u32, + day: u32, + hour: u32, + minute: u32, + second: u32, + date_style: Option<&str>, + time_style: Option<&str>, + hour_cycle: Option<&str>, + hour12: Option, +) -> Option { + use super::super::icu_dtf::{self, Len, Req}; + if !enabled { + return None; + } + icu_dtf::format(&Req { + locale, + year, + month: month as u8, + day: day as u8, + hour: hour as u8, + minute: minute as u8, + second: second as u8, + date_style: date_style.and_then(Len::parse), + time_style: time_style.and_then(Len::parse), + hour_cycle, + hour12, + }) +} + +#[cfg(not(feature = "intl-datetime"))] +#[allow(clippy::too_many_arguments)] +pub(super) fn icu_style( + _enabled: bool, + _locale: &str, + _year: i32, + _month: u32, + _day: u32, + _hour: u32, + _minute: u32, + _second: u32, + _date_style: Option<&str>, + _time_style: Option<&str>, + _hour_cycle: Option<&str>, + _hour12: Option, +) -> Option { + None +} + +/// Format a date-only component set via icu4x. `None` when the feature is off +/// or icu cannot reproduce the combination. +#[cfg(feature = "intl-datetime")] +#[allow(clippy::too_many_arguments)] +pub(super) fn icu_components( + locale: &str, + year: i32, + month: u32, + day: u32, + year_opt: Option<&str>, + month_opt: Option<&str>, + day_opt: Option<&str>, + weekday_opt: Option<&str>, +) -> Option { + use super::super::icu_dtf::{self, CompReq}; + icu_dtf::format_components(&CompReq { + locale, + year, + month: month as u8, + day: day as u8, + hour: 0, + minute: 0, + second: 0, + has_year: year_opt.is_some(), + has_month: month_opt.is_some(), + has_day: day_opt.is_some(), + year_style: year_opt, + month_style: month_opt, + day_style: day_opt, + weekday_style: weekday_opt, + has_hour: false, + has_minute: false, + has_second: false, + hour_cycle: None, + hour12: None, + }) +} + +#[cfg(not(feature = "intl-datetime"))] +#[allow(clippy::too_many_arguments)] +pub(super) fn icu_components( + _locale: &str, + _year: i32, + _month: u32, + _day: u32, + _year_opt: Option<&str>, + _month_opt: Option<&str>, + _day_opt: Option<&str>, + _weekday_opt: Option<&str>, +) -> Option { + None +} + +/// Semantic counterpart of `icu_components`, used by `formatToParts` for the +/// default numeric Y/M/D field set so its order and punctuation match CLDR. +#[cfg(feature = "intl-datetime")] +#[allow(clippy::too_many_arguments)] +pub(super) fn icu_component_parts( + locale: &str, + year: i32, + month: u32, + day: u32, + year_opt: Option<&str>, + month_opt: Option<&str>, + day_opt: Option<&str>, + weekday_opt: Option<&str>, +) -> Option> { + use super::super::icu_dtf::{self, CompReq}; + icu_dtf::format_components_parts(&CompReq { + locale, + year, + month: month as u8, + day: day as u8, + hour: 0, + minute: 0, + second: 0, + has_year: year_opt.is_some(), + has_month: month_opt.is_some(), + has_day: day_opt.is_some(), + year_style: year_opt, + month_style: month_opt, + day_style: day_opt, + weekday_style: weekday_opt, + has_hour: false, + has_minute: false, + has_second: false, + hour_cycle: None, + hour12: None, + }) +} + +#[cfg(not(feature = "intl-datetime"))] +#[allow(clippy::too_many_arguments)] +pub(super) fn icu_component_parts( + _locale: &str, + _year: i32, + _month: u32, + _day: u32, + _year_opt: Option<&str>, + _month_opt: Option<&str>, + _day_opt: Option<&str>, + _weekday_opt: Option<&str>, +) -> Option> { + None +} diff --git a/crates/perry-runtime/src/intl/icu_dtf.rs b/crates/perry-runtime/src/intl/icu_dtf.rs index 4b0627cd8b..b9780265b7 100644 --- a/crates/perry-runtime/src/intl/icu_dtf.rs +++ b/crates/perry-runtime/src/intl/icu_dtf.rs @@ -10,11 +10,15 @@ use icu_datetime::fieldsets; use icu_datetime::fieldsets::builder::{DateFields, FieldSetBuilder}; use icu_datetime::input::{Date, DateTime, Time}; -use icu_datetime::options::{Length, TimePrecision}; +use icu_datetime::options::{Alignment, Length, TimePrecision, YearStyle}; use icu_datetime::preferences::HourCycle; use icu_datetime::DateTimeFormatter; use icu_datetime::DateTimeFormatterPreferences; use icu_locale_core::Locale; +use writeable::{Part, PartsWrite, Writeable}; + +mod default_numeric_patterns; +use default_numeric_patterns::YMD_PATTERNS; /// dateStyle / timeStyle length. #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -177,9 +181,13 @@ pub(crate) struct CompReq<'a> { pub has_year: bool, pub has_month: bool, pub has_day: bool, + /// `year` option value (`numeric`/`2-digit`), or `None` when absent. + pub year_style: Option<&'a str>, /// `month` option value (`numeric`/`2-digit`/`short`/`long`/`narrow`), or /// `None` when month is absent. pub month_style: Option<&'a str>, + /// `day` option value (`numeric`/`2-digit`), or `None` when absent. + pub day_style: Option<&'a str>, /// `weekday` option value (`short`/`long`/`narrow`), or `None` when absent. pub weekday_style: Option<&'a str>, pub has_hour: bool, @@ -191,17 +199,21 @@ pub(crate) struct CompReq<'a> { /// Format an explicit-component request via icu4x's dynamic `FieldSetBuilder`. /// -/// Only combos icu's *semantic* field sets reproduce faithfully are handled: -/// a date part must carry a spelled month (`short`/`long`) or a weekday (icu -/// gets the localized name + field order right). A **purely numeric** date is -/// deliberately rejected (returns `None`): its minimal-digit CLDR pattern -/// (`5.1.2026` for de) can't be expressed by icu's `Short` length, which pads -/// and truncates the year (`05.01.26`) — the caller's numeric assembly owns -/// that. `narrow` and structurally-inexpressible field combos also return -/// `None` for the fallback. +/// Only combos icu's semantic field sets reproduce faithfully are handled: a +/// date part must carry a spelled month (`short`/`long`), a weekday, or the +/// ECMA-402 default numeric Y/M/D set. The latter uses CLDR's classical `yMd` +/// pattern (see `format_default_numeric_parts`); semantic `Short` is not +/// equivalent because it pads German/Japanese fields. `narrow` and +/// structurally-inexpressible field combos still return `None` for fallback. pub(crate) fn format_components(req: &CompReq) -> Option { let has_weekday = req.weekday_style.is_some(); let has_date = req.has_year || req.has_month || req.has_day || has_weekday; + let has_time = req.has_hour || req.has_minute || req.has_second; + let numeric_ymd = req.year_style == Some("numeric") + && req.month_style == Some("numeric") + && req.day_style == Some("numeric") + && !has_weekday + && !has_time; // Name-bearing = a spelled month or a weekday; only these route to icu. let month_len = match req.month_style { @@ -216,10 +228,19 @@ pub(crate) fn format_components(req: &CompReq) -> Option { }; let name_bearing = month_len.is_some() || weekday_len.is_some(); - // Reject narrow (no semantic-fieldset equivalent) and purely numeric dates. + // Reject narrow (no semantic-fieldset equivalent) and numeric component + // combinations other than the default Y/M/D set. if matches!(req.month_style, Some("narrow")) || matches!(req.weekday_style, Some("narrow")) { return None; } + if numeric_ymd { + return format_default_numeric_parts(req).map(|parts| { + parts + .into_iter() + .map(|(_, value)| value) + .collect::() + }); + } if has_date && !name_bearing { return None; } @@ -289,6 +310,204 @@ pub(crate) fn format_components(req: &CompReq) -> Option { Some(normalize(&formatted)) } +#[derive(Default)] +struct DatePartsSink { + active: Vec, + parts: Vec<(&'static str, String)>, +} + +impl std::fmt::Write for DatePartsSink { + fn write_str(&mut self, value: &str) -> std::fmt::Result { + let kind = self + .active + .iter() + .rev() + .find(|part| part.category == "datetime") + .map(|part| part.value) + .unwrap_or("literal"); + if let Some((last_kind, last_value)) = self.parts.last_mut() { + if *last_kind == kind { + last_value.push_str(value); + return Ok(()); + } + } + self.parts.push((kind, value.to_string())); + Ok(()) + } +} + +impl PartsWrite for DatePartsSink { + type SubPartsWrite = Self; + + fn with_part( + &mut self, + part: Part, + mut write: impl FnMut(&mut Self::SubPartsWrite) -> std::fmt::Result, + ) -> std::fmt::Result { + self.active.push(part); + let result = write(self); + self.active.pop(); + result + } +} + +fn ymd_pattern(locale: &Locale) -> Option<&'static str> { + // The table contains canonical CLDR locale identifiers without Unicode or + // private-use extensions. Fall back one subtag at a time for structurally + // valid locales not materialized as their own CLDR data locale. + let id = locale.id.to_string(); + let mut candidate = id.as_str(); + loop { + if let Ok(index) = YMD_PATTERNS.binary_search_by(|entry| entry.0.cmp(candidate)) { + return Some(YMD_PATTERNS[index].1); + } + candidate = candidate.rsplit_once('-').map(|(parent, _)| parent)?; + } +} + +fn push_part(parts: &mut Vec<(&'static str, String)>, kind: &'static str, value: impl AsRef) { + let value = value.as_ref(); + if value.is_empty() { + return; + } + if let Some((last_kind, last_value)) = parts.last_mut() { + if *last_kind == kind { + last_value.push_str(value); + return; + } + } + parts.push((kind, value.to_string())); +} + +fn without_numeric_padding(value: &str, number: u8) -> &str { + if number >= 10 { + return value; + } + // `Alignment::Column` makes a one-digit month/day two localized digits. + // Decimal digits are single Unicode scalars; discard that leading zero. + value + .char_indices() + .nth(1) + .map_or(value, |(index, _)| &value[index..]) +} + +/// Render the ECMA-402 default numeric fields with CLDR's classical `yMd` +/// skeleton. ICU4X 2.x compiles semantic field sets but no longer exposes its +/// classical skeleton matcher at runtime, and semantic `YMD::short()` is not +/// the same pattern: for example, it yields `05.01.2026` in German and +/// `2026/01/05` in Japanese. The small generated table carries just `yMd`'s +/// localized pattern; ICU still supplies calendar conversion and digits. +fn format_default_numeric_parts(req: &CompReq) -> Option> { + let locale: Locale = req.locale.parse().ok()?; + let pattern = ymd_pattern(&locale)?; + let mut builder = FieldSetBuilder::default(); + builder.date_fields = Some(DateFields::YMD); + builder.length = Some(Length::Short); + builder.alignment = Some(Alignment::Column); + builder.year_style = Some(YearStyle::Full); + + let date = Date::try_new_iso(req.year, req.month.into(), req.day.into()).ok()?; + let dtf = DateTimeFormatter::try_new((&locale).into(), builder.build_date().ok()?).ok()?; + let mut sink = DatePartsSink::default(); + dtf.format(&date).write_to_parts(&mut sink).ok()?; + + let year = sink + .parts + .iter() + .find(|(kind, _)| *kind == "year" || *kind == "relatedYear")? + .1 + .as_str(); + let month = sink + .parts + .iter() + .find(|(kind, _)| *kind == "month")? + .1 + .as_str(); + let day = sink + .parts + .iter() + .find(|(kind, _)| *kind == "day")? + .1 + .as_str(); + + let mut parts = Vec::with_capacity(5); + let mut chars = pattern.chars().peekable(); + let mut literal = String::new(); + let mut quoted = false; + while let Some(ch) = chars.next() { + if ch == '\'' { + if chars.peek() == Some(&'\'') { + chars.next(); + literal.push('\''); + } else { + quoted = !quoted; + } + continue; + } + if !quoted && matches!(ch, 'y' | 'M' | 'd') { + push_part(&mut parts, "literal", &literal); + literal.clear(); + let mut width = 1; + while chars.peek() == Some(&ch) { + chars.next(); + width += 1; + } + match ch { + 'y' => push_part(&mut parts, "year", year), + 'M' => push_part( + &mut parts, + "month", + if width == 1 { + without_numeric_padding(month, req.month) + } else { + month + }, + ), + 'd' => push_part( + &mut parts, + "day", + if width == 1 { + without_numeric_padding(day, req.day) + } else { + day + }, + ), + _ => unreachable!(), + } + } else { + literal.push(ch); + } + } + if quoted { + return None; + } + push_part(&mut parts, "literal", literal); + Some( + parts + .into_iter() + .map(|(kind, value)| (kind, normalize(&value))) + .collect(), + ) +} + +/// CLDR-ordered semantic parts for the default numeric Y/M/D component set. +/// ICU annotates fields before localized punctuation is written, so this stays +/// correct even when a locale changes both order and separators. +pub(crate) fn format_components_parts(req: &CompReq) -> Option> { + let numeric_ymd = req.year_style == Some("numeric") + && req.month_style == Some("numeric") + && req.day_style == Some("numeric") + && req.weekday_style.is_none() + && !req.has_hour + && !req.has_minute + && !req.has_second; + if !numeric_ymd { + return None; + } + + format_default_numeric_parts(req) +} + #[cfg(test)] mod tests { use super::*; @@ -419,7 +638,9 @@ mod tests { has_year: year.is_some(), has_month: month.is_some(), has_day: day.is_some(), + year_style: year, month_style: month, + day_style: day, weekday_style: weekday, has_hour: false, has_minute: false, @@ -468,16 +689,39 @@ mod tests { } #[test] - fn numeric_and_narrow_components_defer() { - // Pure-numeric date → None (numeric locale pattern owns it). - assert_eq!( - comp( - "de", + fn default_numeric_components_match_node() { + let cases = [ + ("de-DE", "5.1.2026"), + ("fr-FR", "05/01/2026"), + ("ja-JP", "2026/1/5"), + ("en-GB", "05/01/2026"), + ("en-US", "1/5/2026"), + ]; + for (locale, want) in cases { + let got = comp( + locale, Some("numeric"), Some("numeric"), Some("numeric"), - None - ), + None, + ); + assert_eq!(got.as_deref(), Some(want), "locale {locale}"); + } + } + + #[test] + fn default_numeric_pattern_table_is_strictly_sorted() { + assert!( + YMD_PATTERNS.windows(2).all(|pair| pair[0].0 < pair[1].0), + "YMD_PATTERNS must stay sorted and unique for binary search" + ); + } + + #[test] + fn unsupported_numeric_and_narrow_components_defer() { + // Numeric subsets retain the bespoke fallback. + assert_eq!( + comp("de", None, Some("numeric"), Some("numeric"), None), None ); // Narrow → None. diff --git a/crates/perry-runtime/src/intl/icu_dtf/default_numeric_patterns.rs b/crates/perry-runtime/src/intl/icu_dtf/default_numeric_patterns.rs new file mode 100644 index 0000000000..ee75d93cd4 --- /dev/null +++ b/crates/perry-runtime/src/intl/icu_dtf/default_numeric_patterns.rs @@ -0,0 +1,769 @@ +// @generated from Unicode CLDR 48.2.1 cldr-dates-full. +// The default ECMA-402 date request is the classical `yMd` skeleton. +pub(super) static YMD_PATTERNS: &[(&str, &str)] = &[ + ("aa", "y-MM-dd"), + ("aa-DJ", "y-MM-dd"), + ("aa-ER", "y-MM-dd"), + ("ab", "y-MM-dd"), + ("af", "y-MM-dd"), + ("af-NA", "y-MM-dd"), + ("agq", "d/M/y"), + ("ak", "y/M/d"), + ("am", "d/M/y"), + ("an", "y-MM-dd"), + ("ann", "y-MM-dd"), + ("apc", "y-MM-dd"), + ("ar", "d‏/M‏/y"), + ("ar-AE", "d‏/M‏/y"), + ("ar-BH", "d‏/M‏/y"), + ("ar-DJ", "d‏/M‏/y"), + ("ar-DZ", "d‏/M‏/y"), + ("ar-EG", "d‏/M‏/y"), + ("ar-EH", "d‏/M‏/y"), + ("ar-ER", "d‏/M‏/y"), + ("ar-IL", "d‏/M‏/y"), + ("ar-IQ", "d‏/M‏/y"), + ("ar-JO", "d‏/M‏/y"), + ("ar-KM", "d‏/M‏/y"), + ("ar-KW", "d‏/M‏/y"), + ("ar-LB", "d‏/M‏/y"), + ("ar-LY", "d‏/M‏/y"), + ("ar-MA", "d‏/M‏/y"), + ("ar-MR", "d‏/M‏/y"), + ("ar-OM", "d‏/M‏/y"), + ("ar-PS", "d‏/M‏/y"), + ("ar-QA", "d‏/M‏/y"), + ("ar-SA", "d‏/M‏/y"), + ("ar-SD", "d‏/M‏/y"), + ("ar-SO", "d‏/M‏/y"), + ("ar-SS", "d‏/M‏/y"), + ("ar-SY", "d‏/M‏/y"), + ("ar-TD", "d‏/M‏/y"), + ("ar-TN", "d‏/M‏/y"), + ("ar-YE", "d‏/M‏/y"), + ("arn", "y-MM-dd"), + ("as", "dd-MM-y"), + ("asa", "d/M/y"), + ("ast", "d/M/y"), + ("az", "dd.MM.y"), + ("az-Arab", "y-MM-dd"), + ("az-Arab-IQ", "y-MM-dd"), + ("az-Arab-TR", "y-MM-dd"), + ("az-Cyrl", "dd.MM.y"), + ("az-Latn", "dd.MM.y"), + ("ba", "dd.MM.y"), + ("bal", "dd-MM-y"), + ("bal-Arab", "dd-MM-y"), + ("bal-Latn", "d/M/y"), + ("bas", "d/M/y"), + ("be", "d.M.y"), + ("be-tarask", "d.M.y"), + ("bem", "d/M/y"), + ("bew", "y-MM-dd"), + ("bez", "d/M/y"), + ("bg", "d.MM.y 'г'."), + ("bgc", "y-MM-dd"), + ("bgn", "y-MM-dd"), + ("bgn-AE", "y-MM-dd"), + ("bgn-AF", "y-MM-dd"), + ("bgn-IR", "y-MM-dd"), + ("bgn-OM", "y-MM-dd"), + ("bho", "y-MM-dd"), + ("blo", "M/d/y"), + ("blt", "y-MM-dd"), + ("bm", "d/M/y"), + ("bm-Nkoo", "y-MM-dd"), + ("bn", "d/M/y"), + ("bn-IN", "d/M/y"), + ("bo", "y-MM-dd"), + ("bo-IN", "y-MM-dd"), + ("bqi", "y-MM-dd"), + ("br", "dd/MM/y"), + ("brx", "dd-MM-y"), + ("bs", "d. M. y."), + ("bs-Cyrl", "dd.MM.y."), + ("bs-Latn", "d. M. y."), + ("bss", "y-MM-dd"), + ("bua", "dd.MM.y"), + ("byn", "y-MM-dd"), + ("ca", "d/M/y"), + ("ca-AD", "d/M/y"), + ("ca-ES-valencia", "d/M/y"), + ("ca-FR", "d/M/y"), + ("ca-IT", "d/M/y"), + ("cad", "y-MM-dd"), + ("cch", "y-MM-dd"), + ("ccp", "d/M/y"), + ("ccp-IN", "d/M/y"), + ("ce", "y-MM-dd"), + ("ceb", "M/d/y"), + ("cgg", "d/M/y"), + ("cho", "y-MM-dd"), + ("chr", "M/d/y"), + ("cic", "y-MM-dd"), + ("ckb", "d/M/y"), + ("ckb-IR", "d/M/y"), + ("co", "y-MM-dd"), + ("cop", "y-MM-dd"), + ("cs", "d. M. y"), + ("csw", "y-MM-dd"), + ("cu", "y-MM-dd"), + ("cv", "y.MM.dd"), + ("cy", "d/M/y"), + ("da", "d.M.y"), + ("da-GL", "d.M.y"), + ("dav", "d/M/y"), + ("de", "d.M.y"), + ("de-AT", "d.M.y"), + ("de-BE", "d.M.y"), + ("de-CH", "d.M.y"), + ("de-IT", "d.M.y"), + ("de-LI", "d.M.y"), + ("de-LU", "d.M.y"), + ("dje", "d/M/y"), + ("doi", "d/M/y"), + ("dsb", "d.M.y"), + ("dua", "d/M/y"), + ("dv", "y-MM-dd"), + ("dyo", "d/M/y"), + ("dz", "y-M-d"), + ("ebu", "d/M/y"), + ("ee", "M/d/y"), + ("ee-TG", "M/d/y"), + ("el", "d/M/y"), + ("el-CY", "d/M/y"), + ("el-polyton", "d/M/y"), + ("en", "M/d/y"), + ("en-001", "dd/MM/y"), + ("en-150", "dd/MM/y"), + ("en-AE", "dd/MM/y"), + ("en-AG", "dd/MM/y"), + ("en-AI", "dd/MM/y"), + ("en-AS", "M/d/y"), + ("en-AT", "dd/MM/y"), + ("en-AU", "dd/MM/y"), + ("en-BB", "dd/MM/y"), + ("en-BE", "d/M/y"), + ("en-BI", "M/d/y"), + ("en-BM", "dd/MM/y"), + ("en-BS", "dd/MM/y"), + ("en-BW", "dd/MM/y"), + ("en-BZ", "dd/MM/y"), + ("en-CA", "y-MM-dd"), + ("en-CC", "dd/MM/y"), + ("en-CH", "dd.MM.y"), + ("en-CK", "dd/MM/y"), + ("en-CM", "dd/MM/y"), + ("en-CX", "dd/MM/y"), + ("en-CY", "dd/MM/y"), + ("en-CZ", "dd/MM/y"), + ("en-DE", "dd/MM/y"), + ("en-DG", "dd/MM/y"), + ("en-DK", "dd/MM/y"), + ("en-DM", "dd/MM/y"), + ("en-Dsrt", "y-MM-dd"), + ("en-EE", "dd/MM/y"), + ("en-ER", "dd/MM/y"), + ("en-ES", "dd/MM/y"), + ("en-FI", "dd/MM/y"), + ("en-FJ", "dd/MM/y"), + ("en-FK", "dd/MM/y"), + ("en-FM", "dd/MM/y"), + ("en-FR", "dd/MM/y"), + ("en-GB", "dd/MM/y"), + ("en-GD", "dd/MM/y"), + ("en-GE", "dd/MM/y"), + ("en-GG", "dd/MM/y"), + ("en-GH", "dd/MM/y"), + ("en-GI", "dd/MM/y"), + ("en-GM", "dd/MM/y"), + ("en-GS", "dd/MM/y"), + ("en-GU", "M/d/y"), + ("en-GY", "dd/MM/y"), + ("en-HK", "d/M/y"), + ("en-HU", "dd/MM/y"), + ("en-ID", "dd/MM/y"), + ("en-IE", "d/M/y"), + ("en-IL", "dd/MM/y"), + ("en-IM", "dd/MM/y"), + ("en-IN", "d/M/y"), + ("en-IO", "dd/MM/y"), + ("en-IT", "dd/MM/y"), + ("en-JE", "dd/MM/y"), + ("en-JM", "dd/MM/y"), + ("en-JP", "y/MM/dd"), + ("en-KE", "dd/MM/y"), + ("en-KI", "dd/MM/y"), + ("en-KN", "dd/MM/y"), + ("en-KY", "dd/MM/y"), + ("en-LC", "dd/MM/y"), + ("en-LR", "dd/MM/y"), + ("en-LS", "dd/MM/y"), + ("en-LT", "dd/MM/y"), + ("en-LV", "dd/MM/y"), + ("en-MG", "dd/MM/y"), + ("en-MH", "M/d/y"), + ("en-MO", "dd/MM/y"), + ("en-MP", "M/d/y"), + ("en-MS", "dd/MM/y"), + ("en-MT", "dd/MM/y"), + ("en-MU", "dd/MM/y"), + ("en-MV", "dd/MM/y"), + ("en-MW", "dd/MM/y"), + ("en-MY", "dd/MM/y"), + ("en-NA", "dd/MM/y"), + ("en-NF", "dd/MM/y"), + ("en-NG", "dd/MM/y"), + ("en-NL", "dd/MM/y"), + ("en-NO", "dd/MM/y"), + ("en-NR", "dd/MM/y"), + ("en-NU", "dd/MM/y"), + ("en-NZ", "d/MM/y"), + ("en-PG", "dd/MM/y"), + ("en-PH", "M/d/y"), + ("en-PK", "dd/MM/y"), + ("en-PL", "dd/MM/y"), + ("en-PN", "dd/MM/y"), + ("en-PR", "M/d/y"), + ("en-PT", "dd/MM/y"), + ("en-PW", "dd/MM/y"), + ("en-RO", "dd/MM/y"), + ("en-RW", "dd/MM/y"), + ("en-SB", "dd/MM/y"), + ("en-SC", "dd/MM/y"), + ("en-SD", "dd/MM/y"), + ("en-SE", "y-MM-dd"), + ("en-SG", "dd/MM/y"), + ("en-SH", "dd/MM/y"), + ("en-SI", "dd/MM/y"), + ("en-SK", "dd/MM/y"), + ("en-SL", "dd/MM/y"), + ("en-SS", "dd/MM/y"), + ("en-SX", "dd/MM/y"), + ("en-SZ", "dd/MM/y"), + ("en-Shaw", "y-MM-dd"), + ("en-TC", "dd/MM/y"), + ("en-TK", "dd/MM/y"), + ("en-TO", "dd/MM/y"), + ("en-TT", "dd/MM/y"), + ("en-TV", "dd/MM/y"), + ("en-TZ", "dd/MM/y"), + ("en-UA", "dd/MM/y"), + ("en-UG", "dd/MM/y"), + ("en-UM", "M/d/y"), + ("en-VC", "dd/MM/y"), + ("en-VG", "dd/MM/y"), + ("en-VI", "M/d/y"), + ("en-VU", "dd/MM/y"), + ("en-WS", "dd/MM/y"), + ("en-ZA", "y/MM/dd"), + ("en-ZM", "dd/MM/y"), + ("en-ZW", "d/M/y"), + ("eo", "y-MM-dd"), + ("es", "d/M/y"), + ("es-419", "d/M/y"), + ("es-AR", "d/M/y"), + ("es-BO", "d/M/y"), + ("es-BR", "d/M/y"), + ("es-BZ", "d/M/y"), + ("es-CL", "dd-MM-y"), + ("es-CO", "d/M/y"), + ("es-CR", "d/M/y"), + ("es-CU", "d/M/y"), + ("es-DO", "d/M/y"), + ("es-EA", "d/M/y"), + ("es-EC", "d/M/y"), + ("es-GQ", "d/M/y"), + ("es-GT", "d/M/y"), + ("es-HN", "d/M/y"), + ("es-IC", "d/M/y"), + ("es-MX", "d/M/y"), + ("es-NI", "d/M/y"), + ("es-PA", "MM/dd/y"), + ("es-PE", "d/M/y"), + ("es-PH", "d/M/y"), + ("es-PR", "MM/dd/y"), + ("es-PY", "d/M/y"), + ("es-SV", "d/M/y"), + ("es-US", "d/M/y"), + ("es-UY", "d/M/y"), + ("es-VE", "d/M/y"), + ("et", "d.M.y"), + ("eu", "y/M/d"), + ("ewo", "d/M/y"), + ("fa", "y/M/d"), + ("fa-AF", "M/d/y"), + ("ff", "y-MM-dd"), + ("ff-Adlm", "d-M-y"), + ("ff-Adlm-BF", "d-M-y"), + ("ff-Adlm-CM", "d-M-y"), + ("ff-Adlm-GH", "d-M-y"), + ("ff-Adlm-GM", "d-M-y"), + ("ff-Adlm-GW", "d-M-y"), + ("ff-Adlm-LR", "d-M-y"), + ("ff-Adlm-MR", "d-M-y"), + ("ff-Adlm-NE", "d-M-y"), + ("ff-Adlm-NG", "d-M-y"), + ("ff-Adlm-SL", "d-M-y"), + ("ff-Adlm-SN", "d-M-y"), + ("ff-Latn", "y-MM-dd"), + ("ff-Latn-BF", "y-MM-dd"), + ("ff-Latn-CM", "y-MM-dd"), + ("ff-Latn-GH", "y-MM-dd"), + ("ff-Latn-GM", "y-MM-dd"), + ("ff-Latn-GN", "y-MM-dd"), + ("ff-Latn-GW", "y-MM-dd"), + ("ff-Latn-LR", "y-MM-dd"), + ("ff-Latn-MR", "y-MM-dd"), + ("ff-Latn-NE", "y-MM-dd"), + ("ff-Latn-NG", "y-MM-dd"), + ("ff-Latn-SL", "y-MM-dd"), + ("fi", "d.M.y"), + ("fil", "M/d/y"), + ("fo", "dd.MM.y"), + ("fo-DK", "dd.MM.y"), + ("fr", "dd/MM/y"), + ("fr-BE", "dd/MM/y"), + ("fr-BF", "dd/MM/y"), + ("fr-BI", "dd/MM/y"), + ("fr-BJ", "dd/MM/y"), + ("fr-BL", "dd/MM/y"), + ("fr-CA", "y-MM-dd"), + ("fr-CD", "dd/MM/y"), + ("fr-CF", "dd/MM/y"), + ("fr-CG", "dd/MM/y"), + ("fr-CH", "dd.MM.y"), + ("fr-CI", "dd/MM/y"), + ("fr-CM", "dd/MM/y"), + ("fr-DJ", "dd/MM/y"), + ("fr-DZ", "dd/MM/y"), + ("fr-GA", "dd/MM/y"), + ("fr-GF", "dd/MM/y"), + ("fr-GN", "dd/MM/y"), + ("fr-GP", "dd/MM/y"), + ("fr-GQ", "dd/MM/y"), + ("fr-HT", "dd/MM/y"), + ("fr-KM", "dd/MM/y"), + ("fr-LU", "dd/MM/y"), + ("fr-MA", "dd/MM/y"), + ("fr-MC", "dd/MM/y"), + ("fr-MF", "dd/MM/y"), + ("fr-MG", "dd/MM/y"), + ("fr-ML", "dd/MM/y"), + ("fr-MQ", "dd/MM/y"), + ("fr-MR", "dd/MM/y"), + ("fr-MU", "dd/MM/y"), + ("fr-NC", "dd/MM/y"), + ("fr-NE", "dd/MM/y"), + ("fr-PF", "dd/MM/y"), + ("fr-PM", "dd/MM/y"), + ("fr-RE", "dd/MM/y"), + ("fr-RW", "dd/MM/y"), + ("fr-SC", "dd/MM/y"), + ("fr-SN", "dd/MM/y"), + ("fr-SY", "dd/MM/y"), + ("fr-TD", "dd/MM/y"), + ("fr-TG", "dd/MM/y"), + ("fr-TN", "dd/MM/y"), + ("fr-VU", "dd/MM/y"), + ("fr-WF", "dd/MM/y"), + ("fr-YT", "dd/MM/y"), + ("frr", "y-MM-dd"), + ("fur", "y-MM-dd"), + ("fy", "d-M-y"), + ("ga", "dd/MM/y"), + ("ga-GB", "dd/MM/y"), + ("gaa", "M/d/y"), + ("gd", "d/M/y"), + ("gez", "y-MM-dd"), + ("gez-ER", "y-MM-dd"), + ("gl", "d/M/y"), + ("gn", "y-MM-dd"), + ("gsw", "y-MM-dd"), + ("gsw-FR", "y-MM-dd"), + ("gsw-LI", "y-MM-dd"), + ("gu", "d/M/y"), + ("guz", "y-MM-dd"), + ("gv", "y-MM-dd"), + ("ha", "y-MM-dd"), + ("ha-Arab", "y-MM-dd"), + ("ha-Arab-SD", "y-MM-dd"), + ("ha-GH", "y-MM-dd"), + ("ha-NE", "y-MM-dd"), + ("he", "d.M.y"), + ("hi", "d/M/y"), + ("hi-Latn", "d/M/y"), + ("hnj", "y-MM-dd"), + ("hnj-Hmnp", "y-MM-dd"), + ("hr", "dd. MM. y."), + ("hr-BA", "dd. MM. y."), + ("hsb", "d.M.y"), + ("ht", "dd/MM/y"), + ("hu", "y. MM. dd."), + ("hy", "dd.MM.y"), + ("ia", "dd-MM-y"), + ("id", "d/M/y"), + ("ie", "d.M.y"), + ("ig", "d/M/y"), + ("ii", "y-MM-dd"), + ("io", "y-MM-dd"), + ("is", "d.M.y"), + ("it", "dd/MM/y"), + ("it-CH", "dd/MM/y"), + ("it-SM", "dd/MM/y"), + ("it-VA", "dd/MM/y"), + ("iu", "MM/dd/y"), + ("iu-Latn", "y-MM-dd"), + ("ja", "y/M/d"), + ("jbo", "y-MM-dd"), + ("jgo", "M.d.y"), + ("jmc", "y-MM-dd"), + ("jv", "dd-MM-y"), + ("ka", "d.M.y"), + ("kaa", "y-MM-dd"), + ("kaa-Cyrl", "y-MM-dd"), + ("kaa-Latn", "y-MM-dd"), + ("kab", "y-MM-dd"), + ("kaj", "y-MM-dd"), + ("kam", "y-MM-dd"), + ("kcg", "y-MM-dd"), + ("kde", "y-MM-dd"), + ("kea", "dd/MM/y"), + ("kek", "dd-MM-y"), + ("ken", "y-MM-dd"), + ("kgp", "dd/MM/y"), + ("khq", "y-MM-dd"), + ("ki", "y-MM-dd"), + ("kk", "dd.MM.y"), + ("kk-Arab", "y-d-M"), + ("kk-Cyrl", "dd.MM.y"), + ("kk-KZ", "dd.MM.y"), + ("kkj", "dd/MM y"), + ("kl", "y-MM-dd"), + ("kln", "y-MM-dd"), + ("km", "d/M/y"), + ("kn", "d/M/y"), + ("ko", "y. M. d."), + ("ko-CN", "y. M. d."), + ("ko-KP", "y. M. d."), + ("kok", "d-M-y"), + ("kok-Deva", "d-M-y"), + ("kok-Latn", "d-M-y"), + ("kpe", "y-MM-dd"), + ("kpe-GN", "y-MM-dd"), + ("ks", "M/d/y"), + ("ks-Arab", "M/d/y"), + ("ks-Deva", "M/d/y"), + ("ksb", "y-MM-dd"), + ("ksf", "d/M/y"), + ("ksh", "y-MM-dd"), + ("ku", "dd.MM.y"), + ("ku-Arab", "y-MM-dd"), + ("ku-Arab-IR", "y-MM-dd"), + ("ku-Latn", "dd.MM.y"), + ("ku-Latn-IQ", "dd.MM.y"), + ("ku-Latn-SY", "dd.MM.y"), + ("ku-TR", "dd.MM.y"), + ("kw", "y-MM-dd"), + ("kxv", "d/M/y"), + ("kxv-Deva", "d/M/y"), + ("kxv-Latn", "d/M/y"), + ("kxv-Orya", "d/M/y"), + ("kxv-Telu", "d/M/y"), + ("ky", "y-dd-MM"), + ("la", "y-MM-dd"), + ("lag", "y-MM-dd"), + ("lb", "d.M.y"), + ("lg", "y-MM-dd"), + ("lij", "d/M/y"), + ("lkt", "y-MM-dd"), + ("lld", "d.M.y"), + ("lmo", "y-MM-dd"), + ("ln", "d/M/y"), + ("ln-AO", "d/M/y"), + ("ln-CF", "d/M/y"), + ("ln-CG", "d/M/y"), + ("lo", "d/M/y"), + ("lrc", "y-MM-dd"), + ("lrc-IQ", "y-MM-dd"), + ("lt", "y-MM-dd"), + ("ltg", "y-MM-dd"), + ("lu", "d/M/y"), + ("luo", "y-MM-dd"), + ("luy", "y-MM-dd"), + ("lv", "d.MM.y."), + ("lzz", "y-MM-dd"), + ("mai", "d/M/y"), + ("mas", "y-MM-dd"), + ("mas-TZ", "y-MM-dd"), + ("mdf", "y-MM-dd"), + ("mer", "y-MM-dd"), + ("mfe", "y-MM-dd"), + ("mg", "y-MM-dd"), + ("mgh", "d/M/y"), + ("mgo", "y-MM-dd"), + ("mhn", "y-MM-dd"), + ("mi", "dd-MM-y"), + ("mic", "y-MM-dd"), + ("mk", "d.M.y 'г'."), + ("ml", "d/M/y"), + ("mn", "y.MM.dd"), + ("mn-Mong", "y-MM-dd"), + ("mn-Mong-MN", "y-MM-dd"), + ("mni", "d/M/y"), + ("mni-Beng", "d/M/y"), + ("mni-Mtei", "y-MM-dd"), + ("moh", "y-MM-dd"), + ("mr", "d/M/y"), + ("ms", "d/M/y"), + ("ms-Arab", "d/M/y"), + ("ms-Arab-BN", "d/M/y"), + ("ms-BN", "d/M/y"), + ("ms-ID", "d/M/y"), + ("ms-SG", "d/M/y"), + ("mt", "M/d/y"), + ("mua", "d/M/y"), + ("mus", "y-MM-dd"), + ("mww", "y-MM-dd"), + ("mww-Hmnp", "y-MM-dd"), + ("my", "d/M/y"), + ("myv", "y-MM-dd"), + ("mzn", "y-MM-dd"), + ("naq", "y-MM-dd"), + ("nb", "d.M.y"), + ("nb-SJ", "d.M.y"), + ("nd", "y-MM-dd"), + ("nds", "d.M.y"), + ("nds-NL", "d.M.y"), + ("ne", "y-MM-dd"), + ("ne-IN", "y-MM-dd"), + ("nl", "d-M-y"), + ("nl-AW", "d-M-y"), + ("nl-BE", "d/M/y"), + ("nl-BQ", "d-M-y"), + ("nl-CW", "d-M-y"), + ("nl-SR", "d-M-y"), + ("nl-SX", "d-M-y"), + ("nmg", "d/M/y"), + ("nn", "d.M.y"), + ("nnh", "d/M/y"), + ("no", "d.M.y"), + ("nqo", "y / dd / MM"), + ("nr", "y-MM-dd"), + ("nso", "dd-MM-y"), + ("nus", "d/M/y"), + ("nv", "y-MM-dd"), + ("ny", "y-MM-dd"), + ("nyn", "y-MM-dd"), + ("oc", "dd/MM/y"), + ("oc-ES", "dd/MM/y"), + ("oka", "y-MM-dd"), + ("oka-US", "y-MM-dd"), + ("om", "M/d/y"), + ("om-KE", "M/d/y"), + ("or", "M/d/y"), + ("os", "y-MM-dd"), + ("os-RU", "y-MM-dd"), + ("osa", "y-MM-dd"), + ("pa", "d/M/y"), + ("pa-Arab", "y-MM-dd"), + ("pa-Guru", "d/M/y"), + ("pap", "y-MM-dd"), + ("pap-AW", "y-MM-dd"), + ("pcm", "d/M/y"), + ("pi", "y-MM-dd"), + ("pi-Latn", "y-MM-dd"), + ("pis", "y-MM-dd"), + ("pl", "d.MM.y"), + ("pms", "dd/MM/y"), + ("prg", "d.M.y"), + ("ps", "y-MM-dd"), + ("ps-PK", "y-MM-dd"), + ("pt", "dd/MM/y"), + ("pt-AO", "dd/MM/y"), + ("pt-CH", "dd/MM/y"), + ("pt-CV", "dd/MM/y"), + ("pt-GQ", "dd/MM/y"), + ("pt-GW", "dd/MM/y"), + ("pt-LU", "dd/MM/y"), + ("pt-MO", "dd/MM/y"), + ("pt-MZ", "dd/MM/y"), + ("pt-PT", "dd/MM/y"), + ("pt-ST", "dd/MM/y"), + ("pt-TL", "dd/MM/y"), + ("qu", "dd-MM-y"), + ("qu-BO", "dd-MM-y"), + ("qu-EC", "dd-MM-y"), + ("quc", "y-MM-dd"), + ("raj", "y-MM-dd"), + ("rhg", "y-MM-dd"), + ("rhg-Rohg", "y-MM-dd"), + ("rhg-Rohg-BD", "y-MM-dd"), + ("rif", "y-MM-dd"), + ("rm", "d-M-y"), + ("rn", "d/M/y"), + ("ro", "dd.MM.y"), + ("ro-MD", "dd.MM.y"), + ("rof", "y-MM-dd"), + ("ru", "dd.MM.y"), + ("ru-BY", "dd.MM.y"), + ("ru-KG", "dd.MM.y"), + ("ru-KZ", "dd.MM.y"), + ("ru-MD", "dd.MM.y"), + ("ru-UA", "dd.MM.y"), + ("rw", "dd-MM-y"), + ("rwk", "y-MM-dd"), + ("sa", "d/M/y"), + ("sah", "y-MM-dd"), + ("saq", "y-MM-dd"), + ("sat", "y-MM-dd"), + ("sat-Deva", "y-MM-dd"), + ("sat-Olck", "y-MM-dd"), + ("sbp", "M/d/y"), + ("sc", "dd/MM/y"), + ("scn", "d/M/y"), + ("sd", "y-MM-dd"), + ("sd-Arab", "y-MM-dd"), + ("sd-Deva", "M/d/y"), + ("sdh", "y-MM-dd"), + ("sdh-IQ", "y-MM-dd"), + ("se", "y-MM-dd"), + ("se-FI", "dd.MM.y"), + ("se-SE", "y-MM-dd"), + ("seh", "y-MM-dd"), + ("ses", "y-MM-dd"), + ("sg", "y-MM-dd"), + ("sgs", "y-MM-dd"), + ("shi", "y-MM-dd"), + ("shi-Latn", "y-MM-dd"), + ("shi-Tfng", "y-MM-dd"), + ("shn", "y-MM-dd"), + ("shn-TH", "y-MM-dd"), + ("si", "y-M-d"), + ("sid", "y-MM-dd"), + ("sk", "d. M. y"), + ("skr", "y-MM-dd"), + ("sl", "d. M. y"), + ("sma", "y-MM-dd"), + ("sma-NO", "y-MM-dd"), + ("smj", "y-MM-dd"), + ("smj-NO", "y-MM-dd"), + ("smn", "d.M.y"), + ("sms", "y-MM-dd"), + ("sn", "y-MM-dd"), + ("so", "M/d/y"), + ("so-DJ", "M/d/y"), + ("so-ET", "M/d/y"), + ("so-KE", "M/d/y"), + ("sq", "d.M.y"), + ("sq-MK", "d.M.y"), + ("sq-XK", "d.M.y"), + ("sr", "d. M. y."), + ("sr-Cyrl", "d. M. y."), + ("sr-Cyrl-BA", "d. M. y."), + ("sr-Cyrl-ME", "d. M. y."), + ("sr-Cyrl-XK", "d. M. y."), + ("sr-Latn", "d. M. y."), + ("sr-Latn-BA", "d. M. y."), + ("sr-Latn-ME", "d. M. y."), + ("sr-Latn-XK", "d. M. y."), + ("ss", "y-MM-dd"), + ("ss-SZ", "y-MM-dd"), + ("ssy", "y-MM-dd"), + ("st", "y-MM-dd"), + ("st-LS", "y-MM-dd"), + ("su", "d/M/y"), + ("su-Latn", "d/M/y"), + ("suz", "y-MM-dd"), + ("suz-Deva", "y-MM-dd"), + ("suz-Sunu", "y-MM-dd"), + ("sv", "y-MM-dd"), + ("sv-AX", "y-MM-dd"), + ("sv-FI", "d.M.y"), + ("sw", "d/M/y"), + ("sw-CD", "d/M/y"), + ("sw-KE", "d/M/y"), + ("sw-UG", "d/M/y"), + ("syr", "d/M/y"), + ("syr-SY", "d/M/y"), + ("szl", "dd.MM.y"), + ("ta", "d/M/y"), + ("ta-LK", "d/M/y"), + ("ta-MY", "d/M/y"), + ("ta-SG", "d/M/y"), + ("te", "d/M/y"), + ("teo", "y-MM-dd"), + ("teo-KE", "y-MM-dd"), + ("tg", "d.M.y"), + ("th", "d/M/y"), + ("ti", "M/d/y"), + ("ti-ER", "M/d/y"), + ("tig", "y-MM-dd"), + ("tk", "dd.MM.y"), + ("tn", "y-MM-dd"), + ("tn-BW", "y-MM-dd"), + ("to", "d/M/y"), + ("tok", "#y)#M)#d"), + ("tpi", "d/M/y"), + ("tr", "dd.MM.y"), + ("tr-CY", "dd.MM.y"), + ("trv", "y-MM-dd"), + ("trw", "y-MM-dd"), + ("ts", "y-MM-dd"), + ("tt", "dd.MM.y"), + ("twq", "d/M/y"), + ("tyv", "y-MM-dd"), + ("tzm", "y-MM-dd"), + ("ug", "y-d-M"), + ("uk", "dd.MM.y"), + ("und", "y-MM-dd"), + ("ur", "d/M/y"), + ("ur-IN", "d/M/y"), + ("uz", "dd/MM/y"), + ("uz-Arab", "y-MM-dd"), + ("uz-Cyrl", "dd/MM/y"), + ("uz-Latn", "dd/MM/y"), + ("vai", "y-MM-dd"), + ("vai-Latn", "M/d/y"), + ("vai-Vaii", "y-MM-dd"), + ("ve", "y-MM-dd"), + ("vec", "dd/MM/y"), + ("vi", "d/M/y"), + ("vmw", "d/M/y"), + ("vo", "y-MM-dd"), + ("vun", "y-MM-dd"), + ("wa", "y-MM-dd"), + ("wae", "y-MM-dd"), + ("wal", "y-MM-dd"), + ("wbp", "y-MM-dd"), + ("wo", "dd-MM-y"), + ("xh", "M/d/y"), + ("xnr", "d/M/y"), + ("xog", "y-MM-dd"), + ("yav", "d/M/y"), + ("yi", "d-M-y"), + ("yo", "d/M/y"), + ("yo-BJ", "d/M/y"), + ("yrl", "dd/MM/y"), + ("yrl-CO", "dd/MM/y"), + ("yrl-VE", "dd/MM/y"), + ("yue", "y/M/d"), + ("yue-Hans", "y/M/d"), + ("yue-Hant", "y/M/d"), + ("yue-Hant-CN", "y/M/d"), + ("yue-Hant-MO", "y/M/d"), + ("za", "y/M/d"), + ("zgh", "y-MM-dd"), + ("zh", "y/M/d"), + ("zh-Hans", "y/M/d"), + ("zh-Hans-HK", "d/M/y"), + ("zh-Hans-MO", "y年M月d日"), + ("zh-Hans-MY", "y/M/d"), + ("zh-Hans-SG", "y年M月d日"), + ("zh-Hant", "y/M/d"), + ("zh-Hant-HK", "d/M/y"), + ("zh-Hant-MO", "d/M/y"), + ("zh-Hant-MY", "y/M/d"), + ("zh-Latn", "y-MM-dd"), + ("zu", "y-MM-dd"), +]; diff --git a/crates/perry-runtime/src/node_vm.rs b/crates/perry-runtime/src/node_vm.rs index 92efa9495b..ad6ea8374e 100644 --- a/crates/perry-runtime/src/node_vm.rs +++ b/crates/perry-runtime/src/node_vm.rs @@ -230,7 +230,12 @@ pub(crate) fn compiled_function_source_for_closure(closure: usize) -> Option String { compiled_function_source_for_closure(closure).unwrap_or_else(|| { - let func_ptr = unsafe { (*(closure as *const ClosureHeader)).func_ptr as usize }; + let closure_ptr = closure as *const ClosureHeader; + let func_ptr = unsafe { + crate::closure::bound_method_source_func_ptr(closure_ptr) + .or_else(|| crate::object::class_accessor_source_func_ptr(closure_ptr)) + .unwrap_or((*closure_ptr).func_ptr as usize) + }; crate::builtins::function_source_for_func_ptr(func_ptr) }) } diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index d711b8ca8a..074c00871c 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -171,8 +171,8 @@ pub(crate) use gc_roots::{ // ── registration.rs ───────────────────────────────────────────────────────── pub(crate) use registration::{ - class_accessor_function_value, class_own_accessor_ptrs, class_own_static_accessor_ptrs, - invalidate_class_string_member_order, + class_accessor_function_value, class_accessor_source_func_ptr, class_own_accessor_ptrs, + class_own_static_accessor_ptrs, invalidate_class_string_member_order, }; pub use registration::{ is_class_id_registered, js_register_class_getter, js_register_class_method, diff --git a/crates/perry-runtime/src/object/class_registry/registration.rs b/crates/perry-runtime/src/object/class_registry/registration.rs index a0158f6a92..a6dcfb9deb 100644 --- a/crates/perry-runtime/src/object/class_registry/registration.rs +++ b/crates/perry-runtime/src/object/class_registry/registration.rs @@ -236,6 +236,25 @@ extern "C" fn class_accessor_setter_thunk( f(this, value) } +/// Return the raw getter/setter body wrapped by a descriptor-reflection +/// accessor closure. The wrapper has its own calling-convention thunk, while +/// Function.prototype.toString must consult the original MethodDefinition. +pub(crate) unsafe fn class_accessor_source_func_ptr( + closure: *const crate::closure::ClosureHeader, +) -> Option { + if closure.is_null() || crate::closure::real_capture_count((*closure).capture_count) < 1 { + return None; + } + let thunk = (*closure).func_ptr; + if thunk != class_accessor_getter_thunk as *const u8 + && thunk != class_accessor_setter_thunk as *const u8 + { + return None; + } + let raw = crate::closure::js_closure_get_capture_ptr(closure, 0) as usize; + (raw != 0).then_some(raw) +} + /// Wrap a raw class accessor func_ptr as a callable function VALUE for /// descriptor reflection (`Object.getOwnPropertyDescriptor(C.prototype, /// "x").get`). Built-in-shaped: `.length` 0/1, no `.prototype`, native diff --git a/test-files/test_class_name_and_source_9413.ts b/test-files/test_class_name_and_source_9413.ts index 57133699e3..5789443aba 100644 --- a/test-files/test_class_name_and_source_9413.ts +++ b/test-files/test_class_name_and_source_9413.ts @@ -66,6 +66,40 @@ console.log("expr-toString:", String(class Anon { y = 2; })); console.log("subclass-toString:", String(class ExtNamed extends Named {})); console.log("objmethod-toString:", String(({ m() { return 1; } }).m)); +// #9468: class member values retain their exact MethodDefinition source even +// though Perry compiles them as raw vtable/static/accessor symbols rather than +// ordinary closure bodies. +class MemberSource { + m() { return 1; } + static sm() { return 2; } + get value() { return 3; } + set value(v) { void v; } + async am() { return 4; } + *gm() { yield 5; } + async *agm() { yield 6; } +} +const memberDescriptor = Object.getOwnPropertyDescriptor(MemberSource.prototype, "value")!; +console.log("method-String:", String(MemberSource.prototype.m)); +console.log("method-toString:", MemberSource.prototype.m.toString()); +console.log("method-template:", `${MemberSource.prototype.m}`); +console.log("static-method:", String(MemberSource.sm)); +console.log("getter-source:", String(memberDescriptor.get)); +console.log("setter-source:", String(memberDescriptor.set)); +console.log("async-method:", String(MemberSource.prototype.am)); +console.log("generator-method:", String(MemberSource.prototype.gm)); +console.log("async-generator-method:", String(MemberSource.prototype.agm)); + +// Object-literal accessors use SetFunctionName with the `get`/`set` prefix. +const objectAccessors = { + get g() { return 1; }, + set s(v) { void v; }, +}; +console.log( + "object-accessor-names:", + Object.getOwnPropertyDescriptor(objectAccessors, "g")!.get!.name, + Object.getOwnPropertyDescriptor(objectAccessors, "s")!.set!.name, +); + // util.inspect / console.log of a class object. console.log("direct:", Klass); console.log("inspect:", inspect(Klass)); diff --git a/test-files/test_class_name_cjs_9413.cts b/test-files/test_class_name_cjs_9413.cts index ed84776d7c..06c9d525f1 100644 --- a/test-files/test_class_name_cjs_9413.cts +++ b/test-files/test_class_name_cjs_9413.cts @@ -3,16 +3,10 @@ // ESM (`"type": "module"`), so this file is the only place the CJS lowering // path is exercised. // -// Deliberately NOT covered here: `module.exports = class {}` and -// `exports.Foo = class {}`. Both are member assignments, which per spec get no -// NamedEvaluation (node: `""`), but perry's CJS source rewrite turns the first -// into a NAMED declaration (`__perry_cjs_default__`) before parsing, and drops -// the binding-name inference for the local-`const` form. Those are defects of -// `crates/perry/src/commands/compile/cjs_wrap/hoist_classes.rs`, upstream of -// anything class metadata can reach — reported separately. class Named {} function scopeA() { class Made { } return Made.name; } class Made {} +const LocalAnon = class {}; console.log("decl:", Named.name); console.log("ctor:", new Named().constructor.name); @@ -21,3 +15,10 @@ console.log("new-anon:", new (class {})().constructor.name); console.log("new-named:", new (class Zed {})().constructor.name); console.log("String:", String(Named)); console.log("inspect:", Named); +console.log("local-anon:", LocalAnon.name); + +// #9468: a member assignment is not a NamedEvaluation context. The CJS +// pre-parse rewrite may give this class a synthetic registration key, but that +// key must not become the constructor's observable name. +module.exports = class {}; +console.log("module-exports-anon:", module.exports.name); diff --git a/test-files/test_gap_intl_component_locale.ts b/test-files/test_gap_intl_component_locale.ts index 9f85a40acb..1865bfb464 100644 --- a/test-files/test_gap_intl_component_locale.ts +++ b/test-files/test_gap_intl_component_locale.ts @@ -2,13 +2,49 @@ // spelled `month` or a `weekday`) must localize the field names AND the field // order — `5. Januar 2026`, `2026年1月5日`, `lundi 5 janvier` — not the old // US-hardcoded `January 5, 2026`. Perry routes these name-bearing combos -// through icu4x's dynamic FieldSetBuilder; numeric-only combos keep the -// existing assembly and aren't asserted here. Both entry points are covered: +// through icu4x's dynamic FieldSetBuilder. #9451 extends the same CLDR route +// to the default numeric Y/M/D field set, including semantic parts. Both entry +// points are covered: // `Intl.DateTimeFormat(...).format()` and `Date.prototype.toLocaleDateString`. // // Compared byte-for-byte against `node --experimental-strip-types`. const d = new Date(Date.UTC(2026, 0, 5, 14, 37, 9)); +// The ECMA-402 no-fields default is numeric year/month/day. Cover both sides +// of the padding boundary: January 5 has single-digit month/day, while +// November 15 has double-digit month/day. +const defaultDates = [ + new Date(Date.UTC(2026, 0, 5, 14, 37, 9)), + new Date(Date.UTC(2026, 10, 15, 14, 37, 9)), +]; +for (const loc of ["de-DE", "fr-FR", "ja-JP", "en-GB", "en-US"]) { + for (const date of defaultDates) { + const dtf = new Intl.DateTimeFormat(loc, { timeZone: "UTC" }); + console.log(loc + " | default | " + dtf.format(date)); + console.log(loc + " | parts | " + JSON.stringify(dtf.formatToParts(date))); + console.log( + loc + " | method | " + date.toLocaleDateString(loc, { timeZone: "UTC" }), + ); + } +} + +// Controls: style-driven requests already use ICU's CLDR patterns and must +// remain unchanged when the numeric component path is enabled. +const styleControls: Array<[string, Opt]> = [ + ["de-DE", { dateStyle: "short" }], + ["fr-FR", { dateStyle: "long" }], + ["ja-JP", { timeStyle: "short" }], + ["en-GB", { dateStyle: "medium", timeStyle: "short" }], + ["en-US", { dateStyle: "full" }], +]; +for (const [loc, opt] of styleControls) { + console.log( + loc + + " | style | " + + new Intl.DateTimeFormat(loc, { ...opt, timeZone: "UTC" }).format(d), + ); +} + type Opt = Intl.DateTimeFormatOptions; const cases: Array<[string, Opt]> = [ ["de", { year: "numeric", month: "long", day: "numeric" }],